Skip to content
Shapemetry

Curves and Surfaces

The trio everyone confuses

Three types, three different subjects — same package family, different wire shapes:

TypeShapeMeaning
Axis{ origin, direction }Infinite 3D carrier: a point + unit direction, oriented
Axis2d{ origin, direction }Infinite 2D carrier — the plane twin
LineCurve{ start, end }Bounded segment (degree-1 curve)

There is exactly one infinite-line type per dimension, and it is called Axis / Axis2d — no separate Line / Line2d type or namespace exists. An axis is infinite in both directions, not a ray; the direction carries orientation, so reversing it gives a different axis, which is what a revolve needs. LineCurve is a real curve (closed under affine maps and offset, tagged type: "line" in a B-Rep document). Rule of thumb: measuring distance → Axis; an edge between two points → LineCurve; spinning a profile → Axis.

The typed members take and return the carrier object (the flat 6-tuple is the wasm wire, not the call signature):

import { Axis, Axis2d, LineCurve } from '@huukhanhnguyen/geometry'

const axis = Axis.fromPointDirection([0, 0, 0], [1, 0, 0])  // { origin, direction }
Axis.distanceToPoint(axis, [5, 5, 0])                       // → 5
Axis.pointAt(axis, 25)                                      // point 25 along the axis

const a2 = Axis2d.fromTwoPoints([0, 0], [10, 10])           // { origin, direction }
const hit = Axis2d.intersect(a2, Axis2d.fromTwoPoints([0, 10], [10, 0]))
// → [x, y] crossing, or undefined when parallel

const seg = LineCurve.fromEndpoints(0, 0, 0, 100, 0, 0)     // NURBS curve JSON — a bounded segment

Axis2d is grouped over the crate's line2d_* exports; the crate's 3D line_* exports are fully duplicated by axis_* and are not bound to any public namespace.

NURBS curves

Curves are JSON documents; evaluate by parameter t (normalized along the knot span):

import { NurbsCurve } from '@huukhanhnguyen/geometry'

const p = NurbsCurve.pointAt(curveJson, 0.5)          // Float64Array [x,y,z] at mid-parameter
const foot = NurbsCurve.closestPoint(curveJson, 10, 20, 30) // nearest point to (10,20,30)

Projection here means onto a plane, and returns a new curve document:

const flat = NurbsCurve.project(
  curveJson,
  0, 0, 0,   // plane point
  0, 0, 1,   // plane normal
) // → curve JSON of the projection onto the XY plane

NURBS surfaces

Surfaces are JSON documents too, evaluated by (u, v):

import { Surface } from '@huukhanhnguyen/geometry'

const pt = Surface.point(surfaceJson, 0.5, 0.5)       // [x,y,z]
const n = Surface.normalAt(surfaceJson, 0.5, 0.5)     // unit normal
const uv = Surface.closestParam(surfaceJson, 10, 20, 30, false, 0, 0)
// → [u, v] of the nearest point; pass use_seed=true + seed u,v when you have a hint

The round trip closestParampoint is how you snap world points onto a curved face; on planar faces, hatchSurfacePoint(surface, uv) from @huukhanhnguyen/io is the cheaper closed-form equivalent.

Where do the JSON documents come from?

You don't author curve/surface JSON by hand — it comes out of other doors: LineCurve.fromEndpoints, NurbsCurve.project, B-Rep face/edge tables (see B-Rep documents), or STEP/IGES import (see Import STEP and IGES).

Where next

Last updated: 📖 2 min readEdit on GitHub