Link Search Menu Expand Document

Surfaces from Points

Given a set of 3D points, OCCTSwift can fit a smooth B-spline Surface through them. There are two cases, and they take different functions:

  • A regular grid of samples (rows × columns, a height field, a scan) → Surface.fromPointGrid (GeomAPI_PointsToBSplineSurface).
  • A scattered cloud with no grid structure → Surface.plateThrough (GeomPlate).

Both return a Surface; call .toFace() to get a renderable / sewable Shape.

A grid of samples → fromPointGrid

When the points form a topological grid, pass them row-major (point[v*uCount + u]) with the row and column counts. The result approximates the points within tolerance:

var pts = [SIMD3<Double>]()
let n = 6
for v in 0..<n {
    for u in 0..<n {
        let x = Double(u) * 4, y = Double(v) * 4
        pts.append(SIMD3(x, y, 3 * sin(x * 0.3) * cos(y * 0.3)))   // a wavy height field
    }
}

guard let surface = Surface.fromPointGrid(points: pts, uCount: n, vCount: n,
                                          degMin: 3, degMax: 8,
                                          continuity: 2, tolerance: 1e-3) else { return }
let face = surface.toFace()

It approximates (not strictly interpolates): tighten tolerance to pull the surface closer to the samples, raise degMax for more flexibility. points.count must equal uCount * vCount.


fromPointGrid, a B-spline through a 6×6 height field

A scattered cloud → plateThrough

When the points have no grid order, the plate surface (GeomPlate_BuildPlateSurface) builds a smooth, energy-minimizing B-spline through them, minimum 3 points:

let points: [SIMD3<Double>] = [
    SIMD3(0, 0, 0), SIMD3(10, 0, 1), SIMD3(10, 10, 2),
    SIMD3(0, 10, 1), SIMD3(5, 5, 3),     // a high point in the middle
]
guard let plate = Surface.plateThrough(points, degree: 3, tolerance: 0.01) else { return }

plateThrough doesn’t need any ordering or counts, it’s the right tool for an irregular set of constraint points (probe data, feature points). The trade-off is less direct control over the parametrization than the grid fit gives you.

Like the grid fit, it approximates, so check the result against tolerance rather than assuming it, especially on a cloud with real curvature in it:

let worst = points.compactMap { plate.projectPoint($0)?.distance }.max() ?? 0
if worst > 0.01 { /* the plate could not be fitted this tightly */ }

Before #571 that check could never pass on a demanding cloud: the fit was capped at a single Bezier patch, which is the one setting that stops the approximator acting on its own error estimate, so a tolerance: 0.01 request could return a surface 0.072 away and report success. If plate.uPoleCount is at most degree + 1, the fit never subdivided.

Deform an existing surface to hit target points

If you already have a surface and want to pull it through specific positions, the non-linear plate solver deforms it to meet (u, v) → target constraints:

let plane = Surface.plane(origin: SIMD3(100, 0, 0), normal: SIMD3(0, 0, 1))!
let bumped = plane.nlPlateDeformed(
    constraints: [(uv: SIMD2(0, 0), target: SIMD3(100, 0, 5))],  // lift that point to z = 5
    resolutionOrder: 4, tolerance: 1e-3)!
let atTheConstraint = bumped.point(atU: 0, v: 0)   // the constraint target, (100, 0, 5)
let (uMin, uMax, vMin, vMax) = bumped.domain      // -10, 10, -10, 10: the working domain

The constraint is written in this surface’s own (u, v), and the result answers to the same (u, v), so a parameter you constrained is a parameter you can go back and evaluate. The result is still a fresh B-spline fitted to a sample grid of the deformation rather than the input surface with a displacement applied to it: see occtswift-wrapping-gaps.md for what that refit does not preserve, notably periodicity. (A G0+G1 variant also takes tangent constraints, and G2, G3 and incremental variants take higher derivatives.)

The parameter rectangle the result spans is the input’s own range in a direction the input bounds, and the constraint span padded by 10 in a direction it leaves unbounded. A plane bounds neither, so one constraint gives the 20-wide square above; a cylinder bounds u at [0, 2pi] and keeps it.

Which to use

You have Use
Points on a regular grid (rows × cols) Surface.fromPointGrid
A scattered point cloud Surface.plateThrough
An existing surface to pull through target points surface.nlPlateDeformed
A wireframe of curves (profiles × guides) Surface.gordon

See also