The Curve
The Curve (NURBS / Spline)
A Curve is a 1D path in 3D space that isn't straight. In computational geometry, we almost always use NURBS(Non-Uniform Rational B-Splines). NURBS can define any shape, from a circle to the complex organic body of a car.
Unlike a polyline, a NURBS curve is mathematically smooth at every single point.
Control Points & Knots:
Control Hull: The line connecting the control points.
Knots: A sequence of numbers that determines the influence of each CP.
1class Curve:2 """3 Represents a NURBS or Spline curve.4 """5 def __init__(self, control_points: list, degree: int = 3):6 self.ControlPoints = control_points7 self.Degree = degree8 self.Knots = [] # Mathematical weighting
PointAt(t):
t, which usually ranges from 0.0 (start) to 1.0 (end).1def PointAt(self, t: float) -> Point3d:2 """3 Evaluates a cubic Bezier curve at parameter t (0.0 to 1.0).4 """5 p0, p1, p2, p3 = self.control_points6 mt = 1.0 - t78 # Bernstein polynomials9 x = mt**3 * p0.X + 3 * mt**2 * t * p1.X + 3 * mt * t**2 * p2.X + t**3 * p3.X10 y = mt**3 * p0.Y + 3 * mt**2 * t * p1.Y + 3 * mt * t**2 * p2.Y + t**3 * p3.Y11 z = mt**3 * p0.Z + 3 * mt**2 * t * p1.Z + 3 * mt * t**2 * p2.Z + t**3 * p3.Z1213 return Point3d(x, y, z)
TangentAt(t):
t.1def TangentAt(self, t: float) -> Vector3d:2 """3 Returns a unit vector representing local direction of curve at t.4 Calculated as first derivative of cubic Bezier curve.5 """6 p0, p1, p2, p3 = self.control_points7 mt = 1.0 - t89 # First derivative equation10 dx = 3 * mt**2 * (p1.X - p0.X) + 6 * mt * t * (p2.X - p1.X) + 3 * t**2 * (p3.X - p2.X)11 dy = 3 * mt**2 * (p1.Y - p0.Y) + 6 * mt * t * (p2.Y - p1.Y) + 3 * t**2 * (p3.Y - p2.Y)12 dz = 3 * mt**2 * (p1.Z - p0.Z) + 6 * mt * t * (p2.Z - p1.Z) + 3 * t**2 * (p3.Z - p2.Z)1314 tangent = Vector3d(dx, dy, dz)15 tangent.Unitize()16 return tangent
Translate(vector):
1def Translate(self, vector: 'Vector3d'):2 """Shifts all control points by the vector."""3 for cp in self.ControlPoints:4 cp += vector
Scale(factor):
1def ScaleCurve(curve: Curve, factor: float, center: Point3d) -> Curve:2 """Scales a curve from a center point by scaling each control point."""3 scaled_cps = []4 for cp in curve.ControlPoints:5 # P' = C + factor * (P - C)6 offset = cp - center7 scaled_cps.append(center + offset * factor)89 return Curve(scaled_cps, curve.Degree)
Rotate(angle, axis):
1def RotateCurve(curve: Curve, angle_deg: float, center: Point3d) -> Curve:2 """Rotates a curve around the Z-axis at a center point by rotating its control points."""3 import math4 rad = math.radians(angle_deg)5 cos_a, sin_a = math.cos(rad), math.sin(rad)67 rotated_cps = []8 for cp in curve.ControlPoints:9 dx, dy = cp.X - center.X, cp.Y - center.Y10 rx = center.X + dx * cos_a - dy * sin_a11 ry = center.Y + dx * sin_a + dy * cos_a12 rotated_cps.append(Point3d(rx, ry, cp.Z))1314 return Curve(rotated_cps, curve.Degree)
Arc Length:
t=0 to t=1).1def CurveLength(curve: 'Curve', t_max: float = 1.0, steps: int = 100) -> float:2 """Approximates the arc length of a curve via polyline chord summation."""3 total_length = 0.04 prev_pt = curve.PointAt(0.0)56 for i in range(1, steps + 1):7 t = (i / steps) * t_max8 curr_pt = curve.PointAt(t)9 total_length += curr_pt.DistanceTo(prev_pt)10 prev_pt = curr_pt1112 return total_length
Divide by Count / Length:
t parameter domain will *not* produce points spaced equally in 3D space.1def DivideCurve(curve: 'Curve', count: int, samples: int = 200) -> list['Point3d']:2 """Divides a curve into equal arc-length segments using an arc-length lookup table."""3 # 1. Build cumulative arc-length table4 ts = [i / samples for i in range(samples + 1)]5 pts = [curve.PointAt(t) for t in ts]6 arc_lengths = [0.0]7 for i in range(1, len(pts)):8 arc_lengths.append(arc_lengths[-1] + pts[i].DistanceTo(pts[i - 1]))910 total_len = arc_lengths[-1]11 result = []1213 # 2. Interpolate parameter t at each target arc length14 for k in range(count + 1):15 target = (k / count) * total_len16 for i in range(len(arc_lengths) - 1):17 if arc_lengths[i] <= target <= arc_lengths[i + 1]:18 seg = arc_lengths[i + 1] - arc_lengths[i]19 u = (target - arc_lengths[i]) / seg if seg > 1e-9 else 0.020 t_val = ts[i] + u * (ts[i + 1] - ts[i])21 result.append(curve.PointAt(t_val))22 break23 return result
CurvatureAt(t):
t. A tight corner has a small osculating circle (high curvature), while a gentle arc has a large radius (low curvature).1def CurvatureAt(curve: Curve, t: float) -> dict:2 """Calculates curvature and osculating circle radius using 1st and 2nd derivatives."""3 d1 = curve.DerivativeAt(t, 1) # Velocity (Tangent)4 d2 = curve.DerivativeAt(t, 2) # Acceleration56 # k = |d1 x d2| / |d1|^37 cross_prod = d1.Cross(d2)8 k = cross_prod.Length() / (d1.Length() ** 3)910 r = 1.0 / k if k > 1e-6 else float('inf')1112 return {13 "curvature": k,14 "radius": r15 }
Offset(plane, distance):
1def OffsetPoint(curve: Curve, t: float, distance: float) -> Point3d:23 """Naive implementation."""4 """Calculates a single offset point mathematically.5 An offset curve is formed by moving each point along its normal vector.67 O(t) = C(t) + d * N(t)8 """9 pt = curve.PointAt(t)10 tangent = curve.DerivativeAt(t, 1)1112 # In 2D (XY plane), normal is (-y, x)13 normal = Vector3d(-tangent.Y, tangent.X, 0)14 normal.Unitize()1516 offset_pt = pt + normal * distance17 return offset_pt
Reverse():
1def EvaluateReversed(curve: Curve, t: float) -> Point3d:2 """Mathematically, reversing a curve flips its parameter space.3 Assuming a normalized domain [0, 1]:45 C_rev(t) = C(1 - t)67 To construct a new reversed NURBS curve from scratch:8 1. Reverse the list of Control Points9 2. Reverse the Knot vector and adjust its bounds10 3. Reverse the weights11 """12 reversed_t = 1.0 - t13 return curve.PointAt(reversed_t)
Curve Intersection:
1def IntersectCurves(crvA: Curve, crvB: Curve, tol: float) -> list[Point3d]:2 """Finds intersection points using recursive subdivision."""34 # 1. Broad Phase: check bounding boxes5 if not BoundingBoxesIntersect(crvA.BoundingBox, crvB.BoundingBox, tol):6 return []78 # 2. Base case: curves are flat/small enough to approximate as lines9 if crvA.Length < tol and crvB.Length < tol:10 ptA = crvA.PointAt(0.5)11 ptB = crvB.PointAt(0.5)12 if ptA.DistanceTo(ptB) <= tol:13 return [ptA]14 return []1516 # 3. Recursive step: split curves and test sub-segments17 A1, A2 = crvA.Split(0.5)18 B1, B2 = crvB.Split(0.5)1920 pts = []21 pts.extend(IntersectCurves(A1, B1, tol))22 pts.extend(IntersectCurves(A1, B2, tol))23 pts.extend(IntersectCurves(A2, B1, tol))24 pts.extend(IntersectCurves(A2, B2, tol))2526 return pts
Trim:
1def SplitBezierDeCasteljau(cps: list[Point3d], t: float) -> tuple[list[Point3d], list[Point3d]]:2 """Splits a Bezier curve at parameter t using De Casteljau's algorithm."""3 left, right = [cps[0]], [cps[-1]]4 pts = list(cps)5 while len(pts) > 1:6 pts = [pts[i] * (1.0 - t) + pts[i + 1] * t for i in range(len(pts) - 1)]7 left.append(pts[0])8 right.append(pts[-1])9 return left, list(reversed(right))1011def TrimCurveDomain(curve: Curve, t0: float, t1: float) -> Curve:12 """Extracts a sub-curve between [t0, t1] via two De Casteljau splits."""13 _, after_t0 = SplitBezierDeCasteljau(curve.ControlPoints, t0)14 u = (t1 - t0) / (1.0 - t0)15 trimmed_cps, _ = SplitBezierDeCasteljau(after_t0, u)16 return Curve(trimmed_cps, curve.Degree)
Because curves are parameterized from [0, 1] (or some start t to end t), we can extract any sub-segment by specifying a domain [t0, t1].
Trimming relies on parameters, not lengths. Cutting from t=0 to t=0.5 does not guarantee you will get exactly 50% of the physical length of a NURBS curve, because parameterization is often non-uniform.
JoinCurves:
1def JoinTwoCurves(crvA: Curve, crvB: Curve, tol: float) -> PolyCurve:2 """Mathematically joins two curves if their endpoints touch.34 This illustrates the logic under the hood of Curve.JoinCurves().5 A PolyCurve acts as a container that sequentially routes parameter6 evaluations to the correct sub-curve.7 """8 endA = crvA.PointAtEnd9 startB = crvB.PointAtStart1011 # Check if ends meet within tolerance12 if endA.DistanceTo(startB) <= tol:13 poly = PolyCurve()14 poly.Append(crvA)15 poly.Append(crvB)16 return poly1718 return None # Gap exceeds tolerance, cannot join
When a Line, an Arc, and a NURBS spline are joined end-to-end, they don't mathematically merge into one uniform spline. Instead, they are wrapped into a container called a PolyCurve.
To the user, a PolyCurve behaves exactly like a single curve (you can extract its length, or evaluate PointAt()). But under the hood, it routes evaluation requests to the correct internal sub-segment. The tolerance parameter dictates how close endpoints must be to successfully fuse together.
IncreaseDegree:
1def ElevateBezierDegree(points: list[Point3d]) -> list[Point3d]:2 """Elevates a Bezier curve of degree n to degree n+1.34 The physical shape remains identical, but a new control point is added,5 providing more flexibility for future deformations.67 Formula: Q_i = (i / (n+1)) * P_{i-1} + (1 - i / (n+1)) * P_i8 """9 n = len(points) - 110 new_degree = n + 111 new_points = []1213 for i in range(new_degree + 1):14 # Ratio of interpolation between adjacent old control points15 ratio = i / new_degree1617 p_prev = points[i - 1] if i > 0 else points[0]18 p_curr = points[i] if i < new_degree else points[-1]1920 # Q_i is a linear blend of P_i and P_{i-1}21 q_i = p_curr * (1.0 - ratio) + p_prev * ratio22 new_points.append(q_i)2324 return new_points
IncreaseDegree() takes a lower-degree curve and upgrades its math to a higher degree (from Degree 2 up to Degree 5) using exact control-point averaging: Q_i = (i / (n+1)) * P_{i-1} + (1 - i / (n+1)) * P_i.
The physical shape of the curve does not change at all! Instead, more control points are added and the control polygon hugs the unchanged curve tighter at each step.
ClosestPoint(pt):
1def ClosestPoint(curve: Curve, test_pt: Point3d, steps: int = 100) -> Point3d:2 """Finds the closest point on a curve to a test point.34 Naive but robust implementation: brute-force sampling5 across the curve's domain to find the closest approach.6 """7 best_t = 0.08 min_dist = float('inf')910 # 1. Broad Phase: Sample the curve at low resolution11 for i in range(steps + 1):12 t = i / steps13 pt = curve.PointAt(t)1415 dist = pt.DistanceTo(test_pt)16 if dist < min_dist:17 min_dist = dist18 best_t = t1920 # 2. Narrow Phase (Skipped here): Newton-Raphson refinement21 # best_t = RefineWithDerivatives(curve, test_pt, best_t)2223 return curve.PointAt(best_t)
t where the distance is minimized. Production systems first use brute-force sampling to find a good starting guess.BoundingBox:
1def ComputeBoundingBox(curve: Curve, samples: int = 100) -> tuple[Point3d, Point3d]:2 """Computes a tight axis-aligned bounding box by sampling the curve.34 Note: A fast 'loose' box can be found by just finding the bounding box5 of the Control Points (thanks to the convex hull property of NURBS),6 but sampling yields the precise physical boundaries.7 """8 min_pt = Point3d(float('inf'), float('inf'), float('inf'))9 max_pt = Point3d(float('-inf'), float('-inf'), float('-inf'))1011 for i in range(samples + 1):12 t = i / samples13 pt = curve.PointAt(t)1415 min_pt.X, min_pt.Y, min_pt.Z = min(min_pt.X, pt.X), min(min_pt.Y, pt.Y), min(min_pt.Z, pt.Z)16 max_pt.X, max_pt.Y, max_pt.Z = max(max_pt.X, pt.X), max(max_pt.Y, pt.Y), max(max_pt.Z, pt.Z)1718 return min_pt, max_pt
Rebuild(pt_count, degree):
1def RebuildCurve(curve: Curve, pt_count: int) -> list[Point3d]:2 """Rebuilds a curve by sampling it at evenly spaced intervals.34 A naive rebuild algorithm evaluates the original curve at uniform5 parameter steps to generate a new, simplified set of control points.6 Advanced rebuilds use least-squares fitting to minimize deviation.7 """8 if pt_count < 2:9 pt_count = 21011 new_points = []12 for i in range(pt_count):13 # Calculate parameter t from 0.0 to 1.014 t = i / (pt_count - 1)1516 # Sample the original curve17 pt = curve.PointAt(t)18 new_points.append(pt)1920 return new_points
Extend(length, style):
1def ExtendCurve(curve: Curve, length: float, style: str) -> list[Point3d]:2 """Extends a curve by a specified length at its end."""3 end_pt = curve.PointAtEnd4 tangent = curve.TangentAtEnd56 if style == 'linear':7 # Extends purely along the tangent vector (G1)8 ext_pt = end_pt + tangent * length9 return [end_pt, ext_pt]1011 elif style == 'arc':12 # Extends by fitting a circular arc to match curvature13 # 1. Compute curvature vector (k) and radius (1/|k|)14 curvature_vec = curve.CurvatureAtEnd15 radius = 1.0 / curvature_vec.Length1617 # 2. Find circle center, then sample points along the arc18 center = end_pt + curvature_vec * (radius ** 2)19 return SampleArc(center, radius, length)2021 elif style == 'smooth':22 # Extends by extrapolating the curve's control points (maintains C2 continuity)23 # We project the last 3 control points forward using Taylor expansion24 p0, p1, p2 = curve.ControlPoints[-3:]2526 velocity = p2 - p127 acceleration = velocity - (p1 - p0)2829 ext_pt = p2 + velocity * length + acceleration * (length ** 2) * 0.530 return [end_pt, ext_pt]
PerpendicularFrameAt(t):
1def PerpendicularFrameAt(curve: Curve, t: float) -> Plane:2 """Computes the Frenet-Serret perpendicular frame at parameter t."""3 origin = curve.PointAt(t)45 # First derivative (Velocity) gives the Tangent6 velocity = curve.DerivativeAt(t, 1)7 tangent = velocity.Normalize()89 # Second derivative (Acceleration) captures the curvature10 acceleration = curve.DerivativeAt(t, 2)1112 # Binormal is perpendicular to both velocity and acceleration13 binormal = CrossProduct(velocity, acceleration).Normalize()1415 # Normal completes the orthogonal triad16 normal = CrossProduct(binormal, tangent)1718 return Plane(origin, normal, binormal)
IsClosed:
1def IsCurveClosed(curve: Curve, tolerance: float = 1e-6) -> bool:2 """Checks if a curve is geometrically closed.34 Under the hood, this simply verifies whether the start and end5 points of the curve are coincident within a specific tolerance.6 """7 start_pt = curve.PointAtStart8 end_pt = curve.PointAtEnd910 # Calculate Euclidean distance between start and end11 distance = start_pt.DistanceTo(end_pt)1213 return distance <= tolerance
Related Concepts
The mathematical definition of perfect curvature — defined by a center point and a radius.
A 2D topological space in a 3D world — defined by a UV control grid to create complex, warpable faces.
A closed planar sequence of vertices that defines an area — the foundation of 2D computational planning.