Initializing 3D Canvas...

The Curve

1 min read•1 page

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.

Mathematics of Smoothness: NURBS curves use polynomial equations to ensure the transition between parts is perfectly fluid. This allows CAD software to generate smooth surfaces for manufacturing.
1 min read•1 page

Control Points & Knots:

A curve is not stored as a list of points on the curve. Instead, it is stored as a list of Control Points (CPs). The curve "pulls" towards these points but doesn't necessarily pass through them.

Control Hull: The line connecting the control points.
Knots: A sequence of numbers that determines the influence of each CP.
python
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_points
7 self.Degree = degree
8 self.Knots = [] # Mathematical weighting
Control Point 1 (Y)
2.50
3 min read•1 page

PointAt(t):

Because a curve is a mathematical function, you can "evaluate" it at any point along its length. We use a parameter t, which usually ranges from 0.0 (start) to 1.0 (end).
python
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_points
6 mt = 1.0 - t
7
8 # Bernstein polynomials
9 x = mt**3 * p0.X + 3 * mt**2 * t * p1.X + 3 * mt * t**2 * p2.X + t**3 * p3.X
10 y = mt**3 * p0.Y + 3 * mt**2 * t * p1.Y + 3 * mt * t**2 * p2.Y + t**3 * p3.Y
11 z = mt**3 * p0.Z + 3 * mt**2 * t * p1.Z + 3 * mt * t**2 * p2.Z + t**3 * p3.Z
12
13 return Point3d(x, y, z)
Parameter (t)
0.50
3 min read•1 page

TangentAt(t):

A Tangent is a unit vector representing the local direction of the curve at parameter t.
python
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_points
7 mt = 1.0 - t
8
9 # First derivative equation
10 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)
13
14 tangent = Vector3d(dx, dy, dz)
15 tangent.Unitize()
16 return tangent
In calculus terms, this is the first derivative of the curve's mathematical equation.
Parameter (t)
0.50
1 min read•1 page

Translate(vector):

To move a curve, you move its Control Points. The smooth curve follows the CPs like a string attached to magnets.
python
1def Translate(self, vector: 'Vector3d'):
2 """Shifts all control points by the vector."""
3 for cp in self.ControlPoints:
4 cp += vector
Move X
2.00
Move Y
1.00
2 min read•1 page

Scale(factor):

Scaling a curve multiplies the coordinates of its Control Pointsrelative to an origin. The entire control hull expands or contracts, and the NURBS math generates the resulting smooth curve.
python
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 - center
7 scaled_cps.append(center + offset * factor)
8
9 return Curve(scaled_cps, curve.Degree)
Scale Factor
1.50
2 min read•1 page

Rotate(angle, axis):

Rotating a curve spins its entire control hull around an axis. Like scaling and translation, this is performed by applying a rotation matrix to each individual control point coordinate.
python
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 math
4 rad = math.radians(angle_deg)
5 cos_a, sin_a = math.cos(rad), math.sin(rad)
6
7 rotated_cps = []
8 for cp in curve.ControlPoints:
9 dx, dy = cp.X - center.X, cp.Y - center.Y
10 rx = center.X + dx * cos_a - dy * sin_a
11 ry = center.Y + dx * sin_a + dy * cos_a
12 rotated_cps.append(Point3d(rx, ry, cp.Z))
13
14 return Curve(rotated_cps, curve.Degree)
Angle (°)
45.00
2 min read•1 page

Arc Length:

Calculating the true arc length of a parametric NURBS curve is mathematically complex and involves numerical integration (or summing short chord segments). It is different from simply evaluating the parameter domain (from t=0 to t=1).
python
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.0
4 prev_pt = curve.PointAt(0.0)
5
6 for i in range(1, steps + 1):
7 t = (i / steps) * t_max
8 curr_pt = curve.PointAt(t)
9 total_length += curr_pt.DistanceTo(prev_pt)
10 prev_pt = curr_pt
11
12 return total_length
Sub-Domain (t)
1.00
3 min read•1 page

Divide by Count / Length:

Dividing a curve by equal arc length is a fundamental operation in architectural panelization and manufacturing. Because NURBS curves are non-uniform, evaluating points at equal intervals in the t parameter domain will *not* produce points spaced equally in 3D space.
python
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 table
4 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]))
9
10 total_len = arc_lengths[-1]
11 result = []
12
13 # 2. Interpolate parameter t at each target arc length
14 for k in range(count + 1):
15 target = (k / count) * total_len
16 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.0
20 t_val = ts[i] + u * (ts[i + 1] - ts[i])
21 result.append(curve.PointAt(t_val))
22 break
23 return result
Segment Count
5.00
2 min read•1 page

CurvatureAt(t):

Curvature (κ) measures how sharply a curve bends at a point. It equals 1 / radius_of_curvature — the reciprocal of the radius of the Osculating Circle that best hugs the curve at t. A tight corner has a small osculating circle (high curvature), while a gentle arc has a large radius (low curvature).
python
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) # Acceleration
5
6 # k = |d1 x d2| / |d1|^3
7 cross_prod = d1.Cross(d2)
8 k = cross_prod.Length() / (d1.Length() ** 3)
9
10 r = 1.0 / k if k > 1e-6 else float('inf')
11
12 return {
13 "curvature": k,
14 "radius": r
15 }
t Parameter
0.35
2 min read•1 page

Offset(plane, distance):

An offset curve is a new curve at a constant perpendicular distance from the original at every point. Unlike a simple translation, the offset follows the curve's bends — concave regions shrink while convex regions grow. Critical for CNC cutter compensation, building setback lines, and structural clearances.
python
1def OffsetPoint(curve: Curve, t: float, distance: float) -> Point3d:
2
3 """Naive implementation."""
4 """Calculates a single offset point mathematically.
5 An offset curve is formed by moving each point along its normal vector.
6
7 O(t) = C(t) + d * N(t)
8 """
9 pt = curve.PointAt(t)
10 tangent = curve.DerivativeAt(t, 1)
11
12 # In 2D (XY plane), normal is (-y, x)
13 normal = Vector3d(-tangent.Y, tangent.X, 0)
14 normal.Unitize()
15
16 offset_pt = pt + normal * distance
17 return offset_pt
Offset Distance
0.80
2 min read•1 page

Reverse():

Reversing a curve flips its parameter direction — the shape stays identical, but what was the start (t=0) becomes the end (t=1). This matters for CNC toolpaths (cutting direction affects finish quality), curve joining (seam alignment), and offset curves (which side gets the positive offset).
python
1def EvaluateReversed(curve: Curve, t: float) -> Point3d:
2 """Mathematically, reversing a curve flips its parameter space.
3 Assuming a normalized domain [0, 1]:
4
5 C_rev(t) = C(1 - t)
6
7 To construct a new reversed NURBS curve from scratch:
8 1. Reverse the list of Control Points
9 2. Reverse the Knot vector and adjust its bounds
10 3. Reverse the weights
11 """
12 reversed_t = 1.0 - t
13 return curve.PointAt(reversed_t)
Reverse Curve Direction
2 min read•1 page

Curve Intersection:

Finding the intersection between two parametric curves requires an iterative numerical solver, as there is rarely a perfect algebraic solution. A Tolerance value must always be provided to determine how close the curves must be to register a "hit".
python
1def IntersectCurves(crvA: Curve, crvB: Curve, tol: float) -> list[Point3d]:
2 """Finds intersection points using recursive subdivision."""
3
4 # 1. Broad Phase: check bounding boxes
5 if not BoundingBoxesIntersect(crvA.BoundingBox, crvB.BoundingBox, tol):
6 return []
7
8 # 2. Base case: curves are flat/small enough to approximate as lines
9 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 []
15
16 # 3. Recursive step: split curves and test sub-segments
17 A1, A2 = crvA.Split(0.5)
18 B1, B2 = crvB.Split(0.5)
19
20 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))
25
26 return pts
Move Curve B (Y)
0.00
3 min read•1 page

Trim:

Extracts a specific segment of a curve by cutting away everything outside a defined T-parameter interval.
python
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))
10
11def 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.

t0: Trim Start
0.20
t1: Trim End
0.80
2 min read•1 page

JoinCurves:

Fuses multiple separate curve segments into a single contiguous entity if their endpoints touch.
python
1def JoinTwoCurves(crvA: Curve, crvB: Curve, tol: float) -> PolyCurve:
2 """Mathematically joins two curves if their endpoints touch.
3
4 This illustrates the logic under the hood of Curve.JoinCurves().
5 A PolyCurve acts as a container that sequentially routes parameter
6 evaluations to the correct sub-curve.
7 """
8 endA = crvA.PointAtEnd
9 startB = crvB.PointAtStart
10
11 # Check if ends meet within tolerance
12 if endA.DistanceTo(startB) <= tol:
13 poly = PolyCurve()
14 poly.Append(crvA)
15 poly.Append(crvB)
16 return poly
17
18 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.

Endpoint Gap
0.00
3 min read•1 page

IncreaseDegree:

Upgrades the mathematical complexity of a curve without changing its physical shape.
python
1def ElevateBezierDegree(points: list[Point3d]) -> list[Point3d]:
2 """Elevates a Bezier curve of degree n to degree n+1.
3
4 The physical shape remains identical, but a new control point is added,
5 providing more flexibility for future deformations.
6
7 Formula: Q_i = (i / (n+1)) * P_{i-1} + (1 - i / (n+1)) * P_i
8 """
9 n = len(points) - 1
10 new_degree = n + 1
11 new_points = []
12
13 for i in range(new_degree + 1):
14 # Ratio of interpolation between adjacent old control points
15 ratio = i / new_degree
16
17 p_prev = points[i - 1] if i > 0 else points[0]
18 p_curr = points[i] if i < new_degree else points[-1]
19
20 # Q_i is a linear blend of P_i and P_{i-1}
21 q_i = p_curr * (1.0 - ratio) + p_prev * ratio
22 new_points.append(q_i)
23
24 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.

Polynomial Degree
3.00
Deform Base Control Point
0.00
3 min read•1 page

ClosestPoint(pt):

Returns the exact 3D coordinate on the curve that is closest to a given external point.
python
1def ClosestPoint(curve: Curve, test_pt: Point3d, steps: int = 100) -> Point3d:
2 """Finds the closest point on a curve to a test point.
3
4 Naive but robust implementation: brute-force sampling
5 across the curve's domain to find the closest approach.
6 """
7 best_t = 0.0
8 min_dist = float('inf')
9
10 # 1. Broad Phase: Sample the curve at low resolution
11 for i in range(steps + 1):
12 t = i / steps
13 pt = curve.PointAt(t)
14
15 dist = pt.DistanceTo(test_pt)
16 if dist < min_dist:
17 min_dist = dist
18 best_t = t
19
20 # 2. Narrow Phase (Skipped here): Newton-Raphson refinement
21 # best_t = RefineWithDerivatives(curve, test_pt, best_t)
22
23 return curve.PointAt(best_t)
Finding the point on a complex curve that is closest to an external point is an Optimization Problem. Computers use iterative algorithms (like Newton-Raphson) to "walk" along the curve until they find the parameter t where the distance is minimized. Production systems first use brute-force sampling to find a good starting guess.
Search Pt X
4.00
Search Pt Y
4.00
3 min read•1 page

BoundingBox:

Computes the smallest axis-aligned 3D box that completely encloses the curve.
python
1def ComputeBoundingBox(curve: Curve, samples: int = 100) -> tuple[Point3d, Point3d]:
2 """Computes a tight axis-aligned bounding box by sampling the curve.
3
4 Note: A fast 'loose' box can be found by just finding the bounding box
5 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'))
10
11 for i in range(samples + 1):
12 t = i / samples
13 pt = curve.PointAt(t)
14
15 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)
17
18 return min_pt, max_pt
Bounding Boxes (AABB) are most often used as a "broad phase" check before expensive operations like intersections. If the bounding boxes of two curves don't overlap, the curves themselves mathematically cannot overlap, allowing you to skip complex root-finding logic.
Deform Curve
0.00
2 min read•1 page

Rebuild(pt_count, degree):

Reconstructs the curve with a new number of control points and degree.
python
1def RebuildCurve(curve: Curve, pt_count: int) -> list[Point3d]:
2 """Rebuilds a curve by sampling it at evenly spaced intervals.
3
4 A naive rebuild algorithm evaluates the original curve at uniform
5 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 = 2
10
11 new_points = []
12 for i in range(pt_count):
13 # Calculate parameter t from 0.0 to 1.0
14 t = i / (pt_count - 1)
15
16 # Sample the original curve
17 pt = curve.PointAt(t)
18 new_points.append(pt)
19
20 return new_points
Rebuilding is essential for simplifying complex curves or smoothing messy data. By reducing the number of control points, you inherently filter out noise and wiggles. Conversely, increasing the number of points allows a previously rigid curve to bend in more complex ways.
Control Point Count
5.00
4 min read•1 page

Extend(length, style):

Lengthens a curve at either its start or end point.
python
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.PointAtEnd
4 tangent = curve.TangentAtEnd
5
6 if style == 'linear':
7 # Extends purely along the tangent vector (G1)
8 ext_pt = end_pt + tangent * length
9 return [end_pt, ext_pt]
10
11 elif style == 'arc':
12 # Extends by fitting a circular arc to match curvature
13 # 1. Compute curvature vector (k) and radius (1/|k|)
14 curvature_vec = curve.CurvatureAtEnd
15 radius = 1.0 / curvature_vec.Length
16
17 # 2. Find circle center, then sample points along the arc
18 center = end_pt + curvature_vec * (radius ** 2)
19 return SampleArc(center, radius, length)
20
21 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 expansion
24 p0, p1, p2 = curve.ControlPoints[-3:]
25
26 velocity = p2 - p1
27 acceleration = velocity - (p1 - p0)
28
29 ext_pt = p2 + velocity * length + acceleration * (length ** 2) * 0.5
30 return [end_pt, ext_pt]
You can extend it linearly (following the tangent), smoothly (matching curvature and extrapolating), or via an arc. Each style dictates how the new segment respects the mathematical derivatives of the original curve's endpoint.
Extension Length
1.50
Extension Style
2 min read•1 page

PerpendicularFrameAt(t):

Computes a local coordinate system (a Plane) along a 3D space curve.
python
1def PerpendicularFrameAt(curve: Curve, t: float) -> Plane:
2 """Computes the Frenet-Serret perpendicular frame at parameter t."""
3 origin = curve.PointAt(t)
4
5 # First derivative (Velocity) gives the Tangent
6 velocity = curve.DerivativeAt(t, 1)
7 tangent = velocity.Normalize()
8
9 # Second derivative (Acceleration) captures the curvature
10 acceleration = curve.DerivativeAt(t, 2)
11
12 # Binormal is perpendicular to both velocity and acceleration
13 binormal = CrossProduct(velocity, acceleration).Normalize()
14
15 # Normal completes the orthogonal triad
16 normal = CrossProduct(binormal, tangent)
17
18 return Plane(origin, normal, binormal)
The Z-axis of this plane aligns with the curve's tangent vector (T), while the X and Y axes (Normal N and Binormal B) span the perpendicular plane. This Frenet-Serret frame is essential for sweeping 3D profiles along curves.
Parameter t
0.50
2 min read•1 page

IsClosed:

Determines whether a curve forms a continuous loop by checking if its start and end points coincide.
python
1def IsCurveClosed(curve: Curve, tolerance: float = 1e-6) -> bool:
2 """Checks if a curve is geometrically closed.
3
4 Under the hood, this simply verifies whether the start and end
5 points of the curve are coincident within a specific tolerance.
6 """
7 start_pt = curve.PointAtStart
8 end_pt = curve.PointAtEnd
9
10 # Calculate Euclidean distance between start and end
11 distance = start_pt.DistanceTo(end_pt)
12
13 return distance <= tolerance
A curve is closed if its start and end points perfectly coincide within a given tolerance. Periodic curves (like circles) are smoothly closed everywhere (G2 continuity at the seam), while other closed curves might have a sharp corner or kink at the connection point.
Close Curve
0.00

Related Concepts