Skip to content

Spline

A 3D spline / curve primitive (engine PLM-243 / ADR 0080) — the keystone of Block SPL (splines / geometry / scatter). Add control points, pick an interpolation type, then evaluate the curve either by parameter t ∈ [0, 1] or, for even spacing, by arc-length distance s ∈ [0, length]. frameAtDistance / sampleFrames return rotation-minimizing orientation frames — a moving {position, tangent, normal, binormal} basis that never twist-flips along the curve — which is the input a mesh extruder needs to sweep a profile into a ribbon, tube, road, or rail.

import "engine" for Spline
var path = Spline.new()
path.addPoint(-4, 0, -2)
path.addPoint(-2, 2.5, 2)
path.addPoint(0.5, -1.5, 3)
path.addPoint(2.5, 2, -1)
path.addPoint(4, -0.5, -3)
System.print("length = %(path.length), points = %(path.pointCount)")
// Sample 16 evenly-spaced orientation frames along the curve (mesh-extrusion input).
var frames = path.sampleFrames(16)

Two shapes flow across this API — read them once and the rest of the page is straightforward:

  • A vector is an [x, y, z] number List — the same convention as Camera.getViewMatrix. position, tangent, and positionAtDistance each return one.

  • A frame is a flat 13-number List — position, tangent, normal, binormal, and width, concatenated:

    Index0 1 23 4 56 7 89 10 1112
    Fieldposition px py pztangent tx ty tznormal nx ny nzbinormal bx by bzwidth

    frameAtDistance returns one such 13-number list; sampleFrames returns a List of them. The tangent/normal/binormal are unit vectors forming a right-handed basis; width is the interpolated per-point width (see addPointFull, default 1.0), for tapering an extruded ribbon.

Returns: Spline — A new, empty spline (no control points). Add points, then evaluate.

MethodParametersDescription
addPoint(x, y, z)x, y, z (Num)Append a control point. Width defaults to 1.0.
addPointFull(x, y, z, width)x, y, z, width (Num)Append a control point with a per-point width (scale), interpolated along the curve and delivered in each frame’s slot 12 — for tapering ribbons/tubes.
setType(type)type (String)Interpolation: "catmullrom" (default, an interpolating spline that passes through every control point) or "bezier" (control points read as anchor, control, control, anchor, …).
setClosed(closed)closed (Bool)Open curve (default false) or a closed loop (the last point joins back to the first).
setRoll(radians)radians (Num)A constant roll about the tangent, applied to every orientation frame (rotate the extruded profile around the path).
var loop = Spline.new()
loop.setType("bezier")
loop.setClosed(true)
loop.setRoll(0.25) // radians
loop.addPointFull(0, 0, 0, 1.0) // wide at the start …
loop.addPointFull(3, 1, 0, 0.4) // … tapering to 0.4
PropertyReturnsDescription
pointCountNumThe number of control points added so far.
lengthNumTotal arc length of the curve (world units), the upper bound for the s distance parameters below.

t runs 0 → 1 across the whole curve. Both return an [x, y, z] List.

Returns: List — [x, y, z], the point on the curve at parameter t ∈ [0, 1].

Returns: List — [x, y, z], the unit tangent (direction of travel) at t.

t is NOT constant-speed. Equal steps in t are not equal steps in distance — t moves faster where control points are far apart and slower where they bunch up. For even spacing along the curve (sampling a line strip, placing props, extruding a mesh), use the arc-length methods below instead.

s runs 0 → length and is constant-speed: equal steps in s are equal steps along the curve.

Returns: List — [x, y, z], the point at arc-length distance s ∈ [0, length].

Returns: Num — the parameter t corresponding to arc-length distance s (feed it back into position / tangent if you need the raw-t form).

// A line strip sampled at CONSTANT arc-length spacing (never bunches on tight turns).
var L = path.length
var n = 80
var verts = []
for (i in 0...n) {
var p = path.positionAtDistance(L * i / (n - 1))
verts.add(p) // p = [x, y, z]
}

Frames are rotation-minimizing: the normal is transported along the curve without any extraneous twist, so a swept profile never suddenly flips over — the failure mode of a naive Frenet frame at an inflection point.

Returns: List — a flat 13-number frame at arc-length distance s ∈ [0, length] ([px,py,pz, tx,ty,tz, nx,ny,nz, bx,by,bz, width] — see Data formats).

Returns: List — a List of count frame lists (each 13 numbers), evenly spaced by arc length from the start of the curve to the end. This is the direct input to a mesh extruder: at each frame, place your cross-section profile at position, oriented by normal/binormal, scaled by width.

var frames = path.sampleFrames(16)
for (f in frames) {
var pos = [f[0], f[1], f[2]] // position
var tangent = [f[3], f[4], f[5]] // unit tangent
var normal = [f[6], f[7], f[8]] // unit normal
var binormal = [f[9], f[10], f[11]] // unit binormal
var width = f[12] // per-point width
// … emit ring vertices around `pos` in the normal/binormal plane, scaled by width …
}

See the Spline example (apps/spline_demo) — a 5-point Catmull-Rom curve drawn as a line strip, with sampleFrames rendered as gizmos (tangent red, normal green, binormal blue).