Initializing 3D Canvas...

The Surface

1 min read1 page

The simple Definition

The 2D World in 3D Space

A Surface is a 2D topological space embedded in a 3D environment. Think of it as a sheet of paper that can be bent, twisted, and warped in any direction. Like curves, surfaces are defined by NURBS mathematics.

Every point on a surface is defined by a unique coordinate pair: U and V.

Fabric of Geometry: In algorithmic design, we treat a surface like a fabric. We can measure its area, calculate its curvature to find "weak spots", or even "unwrap" it for pattern fabrication.
1 min read1 page

Data Structure

Surfaces use UV parametrization which parallels Curve's T parameter.

python
1class Surface:
2 """
3 Represents a NURBS surface.
4 """
5 def __init__(self, control_grid: list[list]):
6 # A 2D list of Point3d objects
7 self.ControlGrid = control_grid
8 self.DegreeU = 3
9 self.DegreeV = 3

The Control Point Grid

Just as a curve is defined by a 1D sequence of control points, a Surface is defined by a 2D Grid of Control Points.

  • U direction: The "rows" of the grid.
  • V direction: The "columns" of the grid.

By moving a single control point in the grid, you warp the entire surface locally.

4 min read1 page

Area():

Surface area is computed via numerical integration over the UV parameter domain.
python
1import math
2
3
4# Surface Area is the double integral of the magnitude
5# of the cross product |du x dv| over the UV domain.
6
7u_min, u_max = surface.domain_u()
8v_min, v_max = surface.domain_v()
9
10steps_u = 50
11steps_v = 50
12
13du_step = (u_max - u_min) / steps_u
14dv_step = (v_max - v_min) / steps_v
15
16approx_area = 0.0
17
18# Numerical integration (Riemann sum) over the UV domain
19for i in range(steps_u):
20 for j in range(steps_v):
21 # Sample at the center of the patch
22 u = u_min + (i + 0.5) * du_step
23 v = v_min + (j + 0.5) * dv_step
24
25 # Evaluate 1st derivatives (tangent vectors) at this UV coordinate
26 pt, du, dv = surface.evaluate_derivatives(u, v)
27
28 # Area of this tiny patch is |du x dv| * du_step * dv_step
29 # |du x dv| is the Jacobian determinant of the surface
30 patch_area = cross_product_length(du, dv) * du_step * dv_step
31
32 approx_area += patch_area
33
34# approx_area approaches the exact mathematical area as steps increase
For flat surfaces the formula is exact; for curved surfaces it uses Gaussian quadrature internally. Used heavily in architectural workflows to estimate cladding material quantities, glazing area calculations, and acoustic analysis.
Surface Scale
3.00
2 min read1 page

Flip():

Inverts the structural UV directions, causing the surface's outward-facing Normal to flip to the other side.
python
1def Flip(self) -> 'Surface':
2 """
3 Reverses the direction of the surface normal.
4 A naive implementation reverses the U direction by reversing the order
5 of control points in every row of the control grid.
6 """
7 new_srf = self.copy()
8
9 # By reversing the control points along one direction (U),
10 # the Cross Product of U x V will point in the opposite direction.
11 for row in new_srf.ControlGrid:
12 row.reverse()
13
14 return new_srf

Every surface has a "Front" and a "Back". The front is defined by the Normal Vector. But unlike a Mesh where you can just flip the vertex winding order, a Surface's normal is mathematically derived from its U and V parameter directions using the Right-Hand Rule (U × V = Normal).

To flip a surface inside-out, you must reverse the direction of either its U or its V domain. This is frequently required when projecting curves onto surfaces, or extruding thicknesses, as you need to ensure all surfaces point "outward".

Flip Surface (Reverse U)
3 min read1 page

Evaluate():

Extracts the 3D position AND the local curvature (derivatives) at a specific UV coordinate.
python
1def Evaluate(self, u: float, v: float) -> tuple['Point3d', 'Vector3d', 'Vector3d', 'Vector3d']:
2 """
3 Evaluates the surface to return the point and its partial derivatives.
4 A naive implementation uses finite differences to estimate the tangent vectors.
5 """
6 # 1. Evaluate the central point
7 pt = self.PointAt(u, v)
8
9 # 2. Evaluate points slightly ahead in U and V directions
10 du = 0.001
11 dv = 0.001
12 pt_u = self.PointAt(u + du, v)
13 pt_v = self.PointAt(u, v + dv)
14
15 # 3. Calculate Tangent vectors (Derivatives)
16 u_tangent = (pt_u - pt) / du
17 v_tangent = (pt_v - pt) / dv
18
19 # 4. Calculate the Normal vector using the Cross Product
20 normal = Vector3d.CrossProduct(u_tangent, v_tangent)
21 normal.Unitize()
22
23 return pt, u_tangent, v_tangent, normal

While `PointAt(u, v)` only gives you the physical `(x, y, z)` location, `Evaluate(u, v, numDerivatives)` gives you the entire mathematical context of that point.

By requesting 1 derivative, you get the tangent vectors travelling along the U and V directions. This is exactly how a 3D engine calculates the surface Normal—by taking the cross product of these two tangents. Requesting 2 derivatives gives you information about the sharpness of the bend (curvature) at that exact spot.

U Parameter
0.50
V Parameter
0.50
4 min read1 page

NormalAt(u, v):

Returns the unit vector pointing perfectly perpendicular to the surface at the given UV point. This is essential for calculating how light hits the surface (shading).
python
1import math
2
3# --- HOW DERIVATIVES ARE CALCULATED ---
4# We use the finite difference method to approximate tangent vectors.
5# eps (epsilon) is a very small number representing a tiny step.
6eps = 0.001
7
8# 1. Get the 3D point exactly at (u, v)
9pt = surface.evaluate(u, v)
10
11# 2. Get points slightly further along the U and V directions
12pt_u_stepped = surface.evaluate(u + eps, v)
13pt_v_stepped = surface.evaluate(u, v + eps)
14
15# 3. Calculate tangent vectors (rate of change = rise / run)
16du = (
17 (pt_u_stepped.x - pt.x) / eps,
18 (pt_u_stepped.y - pt.y) / eps,
19 (pt_u_stepped.z - pt.z) / eps
20)
21
22dv = (
23 (pt_v_stepped.x - pt.x) / eps,
24 (pt_v_stepped.y - pt.y) / eps,
25 (pt_v_stepped.z - pt.z) / eps
26)
27
28# 4. Cross product yields a vector perpendicular to both tangents
29nx = du[1]*dv[2] - du[2]*dv[1]
30ny = du[2]*dv[0] - du[0]*dv[2]
31nz = du[0]*dv[1] - du[1]*dv[0]
32
33# 5. Unitize makes its length exactly 1.0 (a unit vector)
34length = math.sqrt(nx**2 + ny**2 + nz**2)
35unit_normal = (nx/length, ny/length, nz/length)
36
37# unit_normal now points directly "away" from the surface!
U Parameter
0.50
V Parameter
0.50
2 min read1 page

PointAt(u, v):

Evaluating a surface requires two parameters: u and v, both usually ranging from 0.0 to 1.0.
python
1def PointAt(self, u: float, v: float) -> Point3d:
2 """Evaluates the surface at UV coordinates using bilinear interpolation."""
3 p00, p10, p01, p11 = self.corners
4
5 # Interpolate along U
6 p_u0 = p00 * (1.0 - u) + p10 * u
7 p_u1 = p01 * (1.0 - u) + p11 * u
8
9 # Interpolate along V
10 return p_u0 * (1.0 - v) + p_u1 * v
U Parameter
0.50
V Parameter
0.50
1 min read1 page

DistanceTo(test_point):

Calculates the minimum distance between the surface and a given test point.
python
1def DistanceTo(self, test_point: 'Point3d') -> float:
2 """Computes the shortest distance from the surface to the point."""
3 success, u, v = self.ClosestPoint(test_point)
4 if success:
5 closest_pt = self.PointAt(u, v)
6 return closest_pt.DistanceTo(test_point)
7 return -1.0

Distance checks are fundamental for collision detection and proximity analysis. Under the hood, this usually performs a ClosestPoint search to find the nearest UV coordinate, then calculates the Euclidean distance.

Point X
2.00
Point Y
3.00
Point Z
2.00
2 min read1 page

Translate(vector):

Moving a surface involves shifting its entire Control Point grid. Just like with curves and points, the underlying mathematical representation is updated by adding a vector to every coordinate.
python
1def Translate(self, vector: 'Vector3d') -> 'Surface':
2 """
3 Shifts the entire Control Grid by a given translation vector.
4 Since NURBS surfaces are defined by their control points,
5 moving the points moves the entire surface identically.
6 """
7 # 1. Create a copy to avoid mutating the original surface
8 new_srf = self.copy()
9
10 # 2. Iterate through the 2D grid of control points (U and V directions)
11 for row in new_srf.ControlGrid:
12 for cp in row:
13 # 3. Add the vector components to each control point's coordinates
14 cp.x += vector.x
15 cp.y += vector.y
16 cp.z += vector.z
17
18 return new_srf
Move X
2.00
Move Y
1.00
1 min read1 page

Scale(factor):

Scaling a surface multiplies the coordinates of every point in the control grid. This results in a proportional expansion or contraction of the entire topological sheet.
python
1def scale_surface(srf, factor):
2 new_srf = srf.copy()
3
4 for row in new_srf.control_points:
5 for pt in row:
6 # Multiply each coordinate by the scale factor
7 pt.x *= factor
8 pt.y *= factor
9 pt.z *= factor
10
11 return new_srf
Scale Factor
0.50
2 min read1 page

Rotate(angle):

Rotating a surface spins the entire control grid around an axis. In NURBS geometry, this is done by applying a rotation matrix to every point in the CP grid.
python
1import math
2
3def rotate_surface_z(srf, angle_deg):
4 new_srf = srf.copy()
5
6 rad = math.radians(angle_deg)
7 cos_a = math.cos(rad)
8 sin_a = math.sin(rad)
9
10 for row in new_srf.control_points:
11 for pt in row:
12 # Apply 2D rotation matrix for Z-axis rotation
13 new_x = pt.x * cos_a - pt.y * sin_a
14 new_y = pt.x * sin_a + pt.y * cos_a
15
16 pt.x = new_x
17 pt.y = new_y
18
19 return new_srf
Angle (°)
45.00
3 min read1 page

ClosestPoint(Point3d):

Finding the closest point on a surface is similar to a curve but across two dimensions (U and V). An optimization algorithm searches the UV space for the parameter pair (u, v) that results in the minimum distance to the target point.
python
1def ClosestPoint(surface: Surface, test_pt: Point3d) -> Point3d:
2 """Iteratively finds the closest point on a NURBS surface."""
3 u, v = 0.5, 0.5
4
5 # Run Newton-Raphson iterations to minimize distance
6 for _ in range(5):
7 pt = surface.PointAt(u, v)
8 r = pt - test_pt
9
10 # Tangent vectors (first derivatives)
11 su = surface.DerivativeAtU(u, v)
12 sv = surface.DerivativeAtV(u, v)
13
14 # Second derivatives
15 suu = surface.SecondDerivativeAtU(u, v)
16 svv = surface.SecondDerivativeAtV(u, v)
17
18 # Update steps using Hessian approximations
19 u -= r.DotProduct(su) / (su.SquareLength() + r.DotProduct(suu))
20 v -= r.DotProduct(sv) / (sv.SquareLength() + r.DotProduct(svv))
21
22 u = max(0.0, min(1.0, u))
23 v = max(0.0, min(1.0, v))
24
25 return surface.PointAt(u, v)
This implementation uses the Newton-Raphson method to minimize distance iteratively by projecting along surface derivatives.
Search Pt X
4.00
Search Pt Y
4.00
4 min read1 page

IsoCurve:

Extracts the hidden 1D structural Curves that govern a 2D NURBS surface.
python
1def ExtractIsocurves(surface: 'Surface', u_param: float, v_param: float) -> tuple['Curve', 'Curve']:
2 """
3 Extracts the continuous U and V lines running across the surface at specific parameters.
4 A naive implementation evaluates the surface formula at a fixed parameter while stepping
5 through the other parameter to generate points, then interpolates them into curves.
6 """
7 u_curve_points = []
8 v_curve_points = []
9
10 steps = 50
11
12 # 1. Generate the U-Curve (keep V fixed, vary U from 0 to 1)
13 for i in range(steps + 1):
14 u_step = i / steps
15 # Evaluate 3D point on surface at (u_step, fixed_v)
16 pt = surface.Evaluate(u_step, v_param)
17 u_curve_points.append(pt)
18
19 # 2. Generate the V-Curve (keep U fixed, vary V from 0 to 1)
20 for j in range(steps + 1):
21 v_step = j / steps
22 # Evaluate 3D point on surface at (fixed_u, v_step)
23 pt = surface.Evaluate(u_param, v_step)
24 v_curve_points.append(pt)
25
26 # 3. Create continuous curves from the sampled points
27 u_curve = CreateCurveFromPoints(u_curve_points)
28 v_curve = CreateCurveFromPoints(v_curve_points)
29
30 return u_curve, v_curve

An "Isocurve" (Isoparametric Curve) is a line along which one parameter (U or V) remains constant while the other changes. Imagine the latitude and longitude lines on a globe—those are isocurves.

Extracting isocurves is a fundamental way to deconstruct a surface. You can use them to generate structural ribs for a building facade, extract toolpaths for a CNC mill, or visualize the "flow" of the geometry.

U Parameter (Extracts V-Curve)
0.50
V Parameter (Extracts U-Curve)
0.50
2 min read1 page

Isotrim():

Because surfaces are defined by a 2D mathematical domain (U and V), you can extract a "sub-surface" simply by providing a smaller U and V interval. This is often used in architecture to extract individual glass panels from a larger, continuous facade surface.
python
1dom_u = S.Domain(0)
2dom_v = S.Domain(1)
3
4# Map normalized 0-1 sliders to the actual surface domains
5u0 = dom_u.Min + U_Start * dom_u.Length
6u1 = dom_u.Min + U_End * dom_u.Length
7v0 = dom_v.Min + V_Start * dom_v.Length
8v1 = dom_v.Min + V_End * dom_v.Length
9
10u_domain = rg.Interval(u0, u1)
11v_domain = rg.Interval(v0, v1)
12
13sub_surface = S.Trim(u_domain, v_domain)
U Domain Start
0.25
U Domain End
0.75
V Domain Start
0.25
V Domain End
0.75
2 min read1 page

Panelization:

Extracts a sub-surface (panel) from a NURBS surface by restricting it to a smaller U and V parameter domain. This is essential for architectural panelization.
python
1u_domain = surface.Domain(0)
2v_domain = surface.Domain(1)
3
4u_step = u_domain.Length / U_Count
5v_step = v_domain.Length / V_Count
6
7panels = []
8for i in range(U_Count):
9 for j in range(V_Count):
10 # Create sub-domains for U and V
11 u_interval = rg.Interval(u_domain.Min + i * u_step, u_domain.Min + (i + 1) * u_step)
12 v_interval = rg.Interval(v_domain.Min + j * v_step, v_domain.Min + (j + 1) * v_step)
13
14 # Extract a sub-surface using the U and V parameter intervals
15 panel = surface.Trim(u_interval, v_interval)
16 panels.append(panel)
U Division
10.00
V Division
10.00
6 min read2 pages

CurvatureAt(u, v):

Gaussian curvature (K) classifies the local shape: positive for dome-like regions, negative for saddle regions, and zero for flat or cylindrical areas (developable surfaces).
python
1import math
2
3# Evaluate surface curvature at the given U, V parameter
4curvature = surface.CurvatureAt(u, v)
5
6if curvature:
7 # Built-in way to extract principal curvatures:
8 # k1 is the max curvature, k2 is the min curvature
9 k1 = curvature.Kappa(0)
10 k2 = curvature.Kappa(1)
11
12 # --- HOW IT WORKS UNDER THE HOOD ---
13 # To compute this manually, we evaluate up to the 2nd derivatives
14 success, pt, du, dv, duu, duv, dvv = surface.Evaluate(u, v, 2)
15
16 if success:
17 # First Fundamental Form (E, F, G) - relates to lengths/angles
18 E = du * du
19 F = du * dv
20 G = dv * dv
21
22 # Surface normal vector
23 normal = rg.Vector3d.CrossProduct(du, dv)
24 normal.Unitize()
25
26 # Second Fundamental Form (L, M, N) - relates to shape/bending
27 L = normal * duu
28 M = normal * duv
29 N = normal * dvv
30
31 # Gaussian Curvature K = (LN - M^2) / (EG - F^2)
32 det_1st = (E*G - F*F)
33 K = (L*N - M*M) / det_1st
34
35 # Mean Curvature H = (EN - 2FM + GL) / (2 * (EG - F^2))
36 H = (E*N - 2*F*M + G*L) / (2 * det_1st)
37
38 # Principal Curvatures (k1, k2) are roots of: k^2 - 2H*k + K = 0
39
40 discriminant = max(0, H*H - K)
41
42 computed_k1 = H + math.sqrt(discriminant)
43 computed_k2 = H - math.sqrt(discriminant)
44 # computed_k1 and computed_k2 match curvature.Kappa(0) and Kappa(1)!
45
46 # Naive shape classification
47 if K > 0.001: shape_type = "Synclastic (Dome)"
48 elif K < -0.001: shape_type = "Anticlastic (Saddle)"
49 else: shape_type = "Developable (Flat/Cyl)"
Fabricators love developable surfaces — they can be made from flat sheet metal without stretching. Mean curvature (H) drives minimal surface generation (soap films, tensile roofs). The heatmap below visualizes Gaussian curvature across the surface.
U Parameter
0.50
V Parameter
0.50
3 min read1 page

Offset(distance, tolerance):

Creates a new surface parallel to the original surface at a specified distance.
python
1def Offset(self, distance: float) -> 'Surface':
2 """
3 Creates a new surface parallel to the original at the specified distance.
4 A naive implementation translates each control point along the
5 surface normal by the offset distance.
6 """
7 new_srf = self.copy()
8
9 for u_idx in range(new_srf.GridSizeU):
10 for v_idx in range(new_srf.GridSizeV):
11 # 1. Get the UV parameter for this control point
12 u, v = new_srf.GetUVParams(u_idx, v_idx)
13
14 # 2. Evaluate the normal at this location
15 pt, u_tangent, v_tangent, normal = self.Evaluate(u, v)
16
17 # 3. Move the control point along the normal
18 cv = new_srf.ControlGrid[u_idx][v_idx]
19 offset_cv = cv + (normal * distance)
20
21 new_srf.ControlGrid[u_idx][v_idx] = offset_cv
22
23 return new_srf

Offsetting a surface is crucial for adding thickness to sheet-like geometries or for calculating clearance zones. Note that offsetting complex doubly-curved surfaces can sometimes result in self-intersections depending on the curvature.

Offset Distance
1.00
2 min read1 page

IsPlanar(tolerance):

Checks if the entire surface is completely flat within a given tolerance.
python
1import numpy as np
2
3def is_planar(points: list[tuple[float, float, float]], tolerance: float = 1e-6) -> bool:
4 """
5 Checks if a set of 3D points is planar within a given tolerance
6 using eigenvalues of the covariance matrix.
7 """
8 if len(points) <= 3:
9 return True
10
11 pts = np.array(points)
12 centroid = np.mean(pts, axis=0)
13
14 # Calculate the covariance matrix
15 cov_matrix = np.cov((pts - centroid).T)
16
17 # Eigen decomposition
18 eigenvalues, _ = np.linalg.eigh(cov_matrix)
19
20 # If the smallest eigenvalue is near 0, the points are coplanar
21 return eigenvalues[0] < tolerance

Boolean checks like IsPlanar are vital for fabrication and topology analysis. If a surface is planar, it can be easily cut from flat sheets (like plywood or glass).

Make Planar