Initializing 3D Canvas...

The Curve

1 min read1 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 read1 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
3 min read1 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 read1 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 read1 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
1 min read1 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 a uniform factor."""
3 # A factor > 1.0 enlarges the curve, < 1.0 shrinks it
4 xform = Transform.Scale(center, factor)
5
6 curve.Transform(xform)
7 return curve
Scale Factor
1.50
1 min read1 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, axis: Vector3d, center: Point3d) -> Curve:
2 """Rotates a curve around a specific center point and axis."""
3 import math
4 rad = math.radians(angle_deg)
5
6 xform = Transform.Rotation(rad, axis, center)
7 curve.Transform(xform)
8 return curve
Angle (°)
45.00
1 min read1 page

Arc Length:

Calculating the true arc length of a parametric NURBS curve is mathematically complex and involves numeric integration. It is different from simply evaluating the parameter domain (from `t=0` to `t=1`).
python
1def CurveLength(curve: 'Curve', domain: 'Interval' = None) -> float:
2 """Calculates the arc length of a NURBS curve."""
3
4 # Can calculate total length, or the length of a sub-segment
5 if domain:
6 return curve.GetLength(domain)
7 else:
8 return curve.GetLength()
Sub-Domain (t)
1.00
2 min read1 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) -> list['Point3d']:
2 """Divides a curve into equal length segments."""
3
4 # Returns the division points.
5 # Note: DivideByCount ensures segments have equal ARC length,
6 # which is very different from dividing the t-parameter domain evenly.
7
8 t_params = curve.DivideByCount(count, True)
9 return [curve.PointAt(t) for t in t_params]
Segment Count
5.00
2 min read1 page

CurvatureAt(t):

Curvature (κ) measures how sharply a curve bends at a point. It equals 1 / radius_of_curvature — a tight corner has high curvature, a gentle arc has low curvature, and a straight line has zero. In AECO workflows, curvature analysis determines if a glass panel can be bent to fit a surface, or if a structural section exceeds bending limits.
python
1def CurvatureAt(curve: Curve, t: float) -> dict:
2 """Calculates curvature using first and second 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.CrossProduct(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.50
2 min read1 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 read1 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)
2 min read1 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
2 min read1 page

Trim:

Extracts a specific segment of a curve by cutting away everything outside a defined T-parameter interval.
python
1def TrimCurveDomain(curve: Curve, t0: float, t1: float) -> Curve:
2 """Extracts a sub-curve between two t-parameters."""
3
4 # Trim cuts away everything outside the domain [t0, t1]
5 # It returns a new Curve object representing just that segment.
6 trimmed = curve.Trim(t0, t1)
7
8 # Split, on the other hand, breaks the curve at t and returns both pieces
9 # pieces = curve.Split(t)
10
11 return trimmed

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 read1 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 read1 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

A Polyline is a Degree 1 curve. It is rigid, sharp, and has no concept of curvature. If you want to smooth a polyline, you can't just bend it—you have to upgrade its mathematics.

IncreaseDegree() takes a lower-degree curve and upgrades its math to a higher degree (like from 1 to 3). The amazing part? The physical shape of the curve does not change. A degree 3 curve can perfectly replicate sharp corners by stacking control points on top of each other. Once elevated, you have more control points to smoothly bend the lines into complex shapes (like S-curves).

Polynomial Degree
1.00
Deform Control Points
0.00
3 min read1 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 read1 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 read1 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 read1 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 read1 page

PerpendicularFrameAt(t):

Computes a local coordinate system (a Plane) along the 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 # Rhino Plane: (Origin, X-Axis, Y-Axis). Its Z-Axis becomes the Tangent.
19 return Plane(origin, normal, binormal)
The Z-axis of this plane aligns exactly with the curve's tangent vector, while the X and Y axes (Normal and Binormal) are strictly perpendicular to the curve. This mathematical Frenet-Serret frame is essential for sweeping profiles without twisting.
Parameter t
0.50
2 min read1 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