Curve3D — Construction
This page documents higher-level Curve3D construction APIs: arcs of conics (hyperbola, parabola), three-point ellipse and hyperbola factories, periodic conversion, parameter-splitting, expanded interpolation variants, and arc-length utilities. For the core type overview, primitives, BSpline/Bezier construction, operations, and transforms, see the main Curve3D page.
Topics
- Arc Construction, Periodic Conversion, Splitting (v0.50.0) · GC_MakeEllipse/Hyperbola Three-Point Constructors (v0.51.0) · Interpolation Expansion, Length, Closest Point (v0.115.0)
Arc Construction, Periodic Conversion, Splitting (v0.50.0)
Curve3D.arcOfHyperbola(center:direction:majorRadius:minorRadius:alpha1:alpha2:sense:)
Creates a trimmed arc of a hyperbola between two parameter values.
public static func arcOfHyperbola(
center: SIMD3<Double> = .zero,
direction: SIMD3<Double> = SIMD3(0, 0, 1),
majorRadius: Double,
minorRadius: Double,
alpha1: Double,
alpha2: Double,
sense: Bool = true
) -> Curve3D?
The hyperbola lies in the plane whose normal is direction. alpha1 and alpha2 are the start and end hyperbolic parameters (not angles). sense: true preserves the natural parameterization direction.
- Parameters:
center— hyperbola centre;direction— plane normal;majorRadius— real semi-axis (a);minorRadius— imaginary semi-axis (b);alpha1— start parameter;alpha2— end parameter;sense— parameterization direction (true= natural). - Returns: Trimmed hyperbolic arc, or
nilon failure. - OCCT:
GC_MakeArcOfHyperbola(gp_Hypr, alpha1, alpha2, sense)→Geom_TrimmedCurve. - Example:
if let arc = Curve3D.arcOfHyperbola( majorRadius: 5, minorRadius: 3, alpha1: -1.0, alpha2: 1.0 ) { let pts = arc.drawAdaptive() }
Curve3D.arcOfParabola(center:direction:focalDistance:alpha1:alpha2:sense:)
Creates a trimmed arc of a parabola between two parameter values.
public static func arcOfParabola(
center: SIMD3<Double> = .zero,
direction: SIMD3<Double> = SIMD3(0, 0, 1),
focalDistance: Double,
alpha1: Double,
alpha2: Double,
sense: Bool = true
) -> Curve3D?
The parabola vertex is at center and the focal distance determines its opening width. alpha1 and alpha2 are the parabolic parameters bounding the arc.
- Parameters:
center— parabola vertex;direction— plane normal;focalDistance— focal distance (must be > 0);alpha1— start parameter;alpha2— end parameter;sense— parameterization direction. - Returns: Trimmed parabolic arc, or
nilon failure. - OCCT:
GC_MakeArcOfParabola(gp_Parab, alpha1, alpha2, sense)→Geom_TrimmedCurve. - Example:
if let arc = Curve3D.arcOfParabola(focalDistance: 4, alpha1: -3.0, alpha2: 3.0) { let mid = arc.point(at: arc.domain.lowerBound + (arc.domain.upperBound - arc.domain.lowerBound) / 2) }
convertToPeriodic()
Converts a closed BSpline curve to its periodic form.
public func convertToPeriodic() -> Curve3D?
The curve must be closed (start point equals end point). The resulting periodic BSpline seamlessly wraps around without a visible join. Use this before surfaces or wires that require periodic input.
- Returns: Periodic BSpline curve, or
nilif the curve is not closed or periodic conversion is not possible. - OCCT:
ShapeCustom_Curve::ConvertToPeriodic. - Example:
if let closed = Curve3D.interpolate(points: [ SIMD3(1, 0, 0), SIMD3(0, 1, 0), SIMD3(-1, 0, 0), SIMD3(0, -1, 0) ], closed: true) { if let periodic = closed.convertToPeriodic() { #expect(periodic.isPeriodic) } }
SplitResult
A pair of curve segments produced by splitAt(parameter:).
public struct SplitResult {
public let first: Curve3D // segment before the split parameter
public let second: Curve3D // segment after the split parameter
}
| Field | Meaning |
|---|---|
first | Curve segment before the split parameter. |
second | Curve segment after the split parameter. |
Curve3D.SplitResult.first
Curve segment before the split parameter.
splitAt(parameter:)
Splits this curve at a parameter value into two segments.
public func splitAt(parameter: Double) -> SplitResult?
The split parameter must lie strictly inside the curve’s domain (domain.lowerBound < parameter < domain.upperBound). Returns two curves whose domains cover [first, parameter] and [parameter, last] respectively.
- Parameters:
parameter— split position within the open interior ofdomain. - Returns:
SplitResultwithfirstandsecondsegments, ornilifparameteris outside the open interior or splitting fails. - OCCT:
ShapeUpgrade_SplitCurve3d::Perform→TColGeom_HArray1OfCurve. - Example:
if let seg = Curve3D.segment(from: SIMD3(0, 0, 0), to: SIMD3(10, 0, 0)), let result = seg.splitAt(parameter: seg.domain.lowerBound + (seg.domain.upperBound - seg.domain.lowerBound) / 2) { #expect(result.first.endPoint.x < result.second.startPoint.x + 1e-6) }
GC_MakeEllipse/Hyperbola Three-Point Constructors (v0.51.0)
Curve3D.ellipseThreePoints(s1:s2:center:)
Creates a full ellipse defined by two axis endpoints and a centre.
public static func ellipseThreePoints(
s1: SIMD3<Double>, s2: SIMD3<Double>, center: SIMD3<Double>
) -> Curve3D?
s1 is the end of the major axis (determines major radius and axis direction); s2 defines the extent of the minor axis. The plane is determined by the three points. This form is convenient when the three geometric points are known but the radii and normal must be derived.
- Parameters:
s1— major axis endpoint;s2— minor axis extent point;center— ellipse centre. - Returns: Full ellipse curve, or
nilif the three points are degenerate orGC_MakeEllipsefails. - OCCT:
GC_MakeEllipse(gp_Pnt s1, gp_Pnt s2, gp_Pnt center). - Example:
if let ellipse = Curve3D.ellipseThreePoints( s1: SIMD3(10, 0, 0), s2: SIMD3(0, 5, 0), center: .zero ) { let pts = ellipse.drawAdaptive() }
Curve3D.hyperbolaThreePoints(s1:s2:center:)
Creates a full hyperbola defined by two axis endpoints and a centre.
public static func hyperbolaThreePoints(
s1: SIMD3<Double>, s2: SIMD3<Double>, center: SIMD3<Double>
) -> Curve3D?
s1 is the end of the real (transverse) axis; s2 defines the extent of the imaginary (conjugate) axis. Analogous to ellipseThreePoints for hyperbolas.
- Parameters:
s1— transverse axis endpoint;s2— conjugate axis extent point;center— hyperbola centre. - Returns: Full hyperbola curve, or
nilon failure. - OCCT:
GC_MakeHyperbola(gp_Pnt s1, gp_Pnt s2, gp_Pnt center). - Example:
if let hyp = Curve3D.hyperbolaThreePoints( s1: SIMD3(5, 0, 0), s2: SIMD3(0, 3, 0), center: .zero ) { let arc = hyp.trimmed(from: -1.0, to: 1.0) }
Interpolation Expansion, Length, Closest Point (v0.115.0)
Curve3D.interpolate(points:startTangent:endTangent:tolerance:)
Interpolates a BSpline through points with constrained endpoint tangents. Fully documented on the main Curve3D page under “BSpline & Bezier”, alongside the sibling interpolate(points:closed:tolerance:) — see that entry for the signature, parameters, OCCT mapping, and an example.
Curve3D.interpolate(points:tangents:tangentFlags:)
Interpolates a BSpline with per-point tangent constraints.
public static func interpolate(
points: [SIMD3<Double>],
tangents: [SIMD3<Double>],
tangentFlags: [Bool]
) -> Curve3D?
Each point can independently opt in or out of its tangent constraint via tangentFlags. All three arrays must have the same length.
- Parameters:
points— interpolation points;tangents— desired tangent at each point;tangentFlags—trueto enforce the corresponding tangent,falseto ignore it. - Returns: Interpolated BSpline, or
nilif array lengths differ or interpolation fails. - OCCT:
GeomAPI_Interpolate::Load(tangents, tangentFlags)+Perform(). - Example:
let pts: [SIMD3<Double>] = [SIMD3(0,0,0), SIMD3(5,3,0), SIMD3(10,0,0)] let tans: [SIMD3<Double>] = [SIMD3(1,1,0), SIMD3(1,0,0), SIMD3(1,-1,0)] let flags = [true, false, true] // enforce only first and last tangents if let curve = Curve3D.interpolate(points: pts, tangents: tans, tangentFlags: flags) { let d = curve.domain #expect(curve.point(at: d.lowerBound).x < 1e-6) }
Curve3D.interpolate(points:parameters:)
Interpolates a BSpline at explicitly specified parameter values.
public static func interpolate(
points: [SIMD3<Double>],
parameters: [Double]
) -> Curve3D?
Controls the parameterization explicitly — each point is placed at the corresponding parameter value. points and parameters must have the same length. Parameters must be strictly increasing.
- Parameters:
points— interpolation points;parameters— parameter value for each point (same count, strictly increasing). - Returns: Interpolated BSpline, or
nilif counts differ or construction fails. - OCCT:
GeomAPI_Interpolate(pts, params, Standard_False, tol)+Perform(). - Example:
let pts: [SIMD3<Double>] = [SIMD3(0,0,0), SIMD3(3,5,0), SIMD3(10,0,0)] let params = [0.0, 0.3, 1.0] if let curve = Curve3D.interpolate(points: pts, parameters: params) { #expect(abs(curve.point(at: 0.3).y - 5.0) < 0.5) }
Curve3D.interpolatePeriodic(points:tolerance:)
Interpolates a periodic (closed) BSpline through the given points.
public static func interpolatePeriodic(points: [SIMD3<Double>],
tolerance: Double = 1e-6) -> Curve3D?
The resulting curve is periodic: it passes through all points and closes smoothly back to the first point without an explicit closing segment. The first and last points need not coincide.
A spelling of interpolate(points:closed:tolerance:) with closed: true, and it delegates to it: the two cannot produce different curves for the same input. Before #493 it was a second, independent GeomAPI_Interpolate call site with the tolerance pinned at 1e-6 and unreachable, and with a stricter minimum point count (3, against the general entry point’s 2) that the two had drifted into. This is the same fix #412 applied to Curve2D.interpolatePeriodic.
- Parameters:
points, interpolation points (minimum 2, typically 3 or more for a non-degenerate loop);tolerance, point coincidence tolerance. Points closer together than this are treated as coincident and the interpolation fails. - Returns: Periodic interpolated BSpline, or
nilon failure. - OCCT:
GeomAPI_Interpolate(pts, Standard_True, tol)+Perform(). - Example:
if let loop = Curve3D.interpolatePeriodic(points: [ SIMD3(1, 0, 0), SIMD3(0, 1, 0), SIMD3(-1, 0, 0), SIMD3(0, -1, 0) ]) { #expect(loop.isPeriodic) } // Identical to spelling it out on the general factory: let same = Curve3D.interpolate(points: [ SIMD3(1, 0, 0), SIMD3(0, 1, 0), SIMD3(-1, 0, 0), SIMD3(0, -1, 0) ], closed: true) #expect(same?.isPeriodic == true)
Curve3D.approximate(points:degMin:degMax:continuity:tolerance:)
Approximates a BSpline through points with degree and continuity control.
public static func approximate(
points: [SIMD3<Double>],
degMin: Int = 3,
degMax: Int = 8,
continuity: Int = 2,
tolerance: Double = 1e-3
) -> Curve3D?
Unlike interpolate, the curve does not pass exactly through every point; it minimises deviation within tolerance. continuity is a ParametricContinuity raw value: 0=C0, 1=C1, 2=C2, 3=C3. (It was documented here as 0=C0, 1=G1, 2=C1 — the analysis-order vocabulary, which this call has never used; corrected in #490.) GeomAPI_PointsToBSpline treats the value as an upper bound and accepts the whole range without failing.
- Parameters:
points— data points (minimum 2);degMin— minimum polynomial degree;degMax— maximum polynomial degree;continuity— minimum continuity order (0=C0, 1=C1, 2=C2, 3=C3);tolerance— maximum allowed deviation. - Returns: Approximating BSpline, or
nilon failure. - OCCT:
GeomAPI_PointsToBSpline(pts, degMin, degMax, continuity, tolerance). - Example:
let pts: [SIMD3<Double>] = [ SIMD3(0,0,0), SIMD3(2,3,0), SIMD3(5,1,0), SIMD3(8,4,0), SIMD3(10,0,0) ] if let curve = Curve3D.approximate(points: pts, degMin: 3, degMax: 8, continuity: 2, tolerance: 1e-3) { let len = curve.totalArcLength }
Curve3D.approximate(points:parameters:degMin:degMax:continuity:tolerance:)
Approximates a BSpline with explicit parameter values at each point.
public static func approximate(
points: [SIMD3<Double>],
parameters: [Double],
degMin: Int = 3,
degMax: Int = 8,
continuity: Int = 2,
tolerance: Double = 1e-3
) -> Curve3D?
Combines approximation with explicit parameterization — useful when the data comes with known parameter stamps (e.g. timestamps or arc-length fractions).
- Parameters:
points— data points;parameters— parameter for each point (same count, strictly increasing);degMin/degMax— degree bounds;continuity— minimum continuity;tolerance— maximum deviation. - Returns: Approximating BSpline, or
nilif counts differ or construction fails. - OCCT:
GeomAPI_PointsToBSpline(pts, params, degMin, degMax, continuity, tolerance). - Example:
let pts: [SIMD3<Double>] = [SIMD3(0,0,0), SIMD3(3,5,0), SIMD3(10,0,0)] let params = [0.0, 0.3, 1.0] if let curve = Curve3D.approximate(points: pts, parameters: params, degMin: 2, degMax: 6) { let len = curve.totalArcLength }
arcLength(from:to:)
The arc length of this curve between two parameter values.
public func arcLength(from u1: Double, to u2: Double) -> Double
Non-optional; delegates to length(from:to:) (the failure-distinguishing entry point on the main page) and returns -1.0 if the curve is invalid, a bound is not finite, or the computation fails. Arc length is otherwise always non-negative, so -1.0 is unambiguous and never collides with a genuine zero-length result (e.g. u1 == u2). Use length(from:to:) directly if you need an optional.
- Parameters:
u1— start parameter;u2— end parameter. Either order; both must be finite. - Returns: Arc length in model units, or
-1.0on failure. - OCCT:
GCPnts_AbscissaPoint::Length(adaptor, u1, u2)(vialength(from:to:)). - Note:
.nanand±.infinityreturn-1.0on every curve type. Seelength(from:to:)for what OCCT did with them (#548), and for what an out-of-domain range measures (#600). - Example:
if let curve = Curve3D.interpolate( points: [SIMD3(0,0,0), SIMD3(10,0,0)], startTangent: SIMD3(1,0,0), endTangent: SIMD3(1,0,0) ) { let d = curve.domain let len = curve.arcLength(from: d.lowerBound, to: d.upperBound) #expect(len > 0) }
parameterAtLength(_:from:)
Finds the parameter at a given arc-length distance from a starting parameter.
public func parameterAtLength(_ arcLength: Double, from startParam: Double? = nil) -> Double
Positive arcLength advances forward; negative reverses. Returns 0 on internal failure.
The travel is measured with the same subdivided quadratures length uses, so this and the length it inverts always agree: curve.parameterAtLength(curve.length!) lands on domain.upperBound. That mattered from #603 onwards — OCCT’s own root finder inverts a single Gauss quadrature over [startParam, u], the very integral length stopped using, and fed the accurate total length of an 8 × 3 ellipse it answered 6.2438 for a domain ending at 6.2832.
- Parameters:
arcLength— distance to advance along the curve;startParam— starting parameter (defaults todomain.lowerBound). - Returns: The parameter value at the specified arc-length distance from
startParam. - OCCT: the accumulated
GeomAbs_CNsub-piece lengths, with the final narrow piece handed toGCPnts_AbscissaPoint(adaptor, remainder, pieceStart)::Parameter. - Note: A distance longer than the curve keeps OCCT’s own answer, which reports success with a parameter outside the curve’s domain rather than failing.
- Example:
let line = Curve3D.segment(from: SIMD3(0,0,0), to: SIMD3(10,0,0))! let midParam = line.parameterAtLength(5.0) let midPt = line.point(at: midParam) #expect(abs(midPt.x - 5.0) < 0.01)
totalArcLength
The total arc length of the curve over its full domain.
public var totalArcLength: Double { get }
Delegates to length (the failure-distinguishing entry point on the main page) and returns -1.0 if the curve is invalid or the computation fails, rather than Double?. Arc length is otherwise always non-negative, so -1.0 is unambiguous and never collides with a genuine zero-length curve. Use length directly if you need an optional.
- Returns: Total arc length in model units, or
-1.0on failure. - OCCT:
GCPnts_AbscissaPoint::LengthperGeomAbs_CNinterval, subdivided to convergence (vialength, #603). - Example:
let circle = Curve3D.circle(center: .zero, normal: SIMD3(0,0,1), radius: 10)! let circumference = circle.totalArcLength #expect(abs(circumference - 2 * Double.pi * 10) < 0.1)
arcLengthBetween(_:_:)
The arc length between two parameter values.
public func arcLengthBetween(_ param1: Double, _ param2: Double) -> Double
Like arcLength(from:to:) but with unlabelled parameters. Delegates to length(from:to:) and returns -1.0 on failure, never 0.0 (which would collide with a genuine zero-length interval, e.g. param1 == param2).
- Parameters:
param1— first parameter;param2— second parameter. Both must be finite. - Returns: Arc length between the two parameters, or
-1.0on failure. - OCCT:
GCPnts_AbscissaPoint::Length(adaptor, param1, param2)(vialength(from:to:)). - Note:
.nanand±.infinityreturn-1.0on every curve type (#548). - Example:
let line = Curve3D.segment(from: SIMD3(0,0,0), to: SIMD3(10,0,0))! let d = line.domain let half = line.arcLengthBetween(d.lowerBound, (d.lowerBound + d.upperBound) / 2) #expect(abs(half - 5.0) < 0.01)
nearestParameter(to:)
Finds the parameter of the nearest point on this curve to a given 3D point.
public func nearestParameter(to point: SIMD3<Double>) -> Double?
- Parameters:
point— the query point in 3D space. - Returns: Parameter
usuch thatself.point(at: u)is the nearest point on the curve topoint, always inside the curve’s own domain, ornilif there is no curve to answer about. - OCCT:
occtNearestPointOnCurveRange— the minimum overShapeAnalysis_Curve::Project, everyGeomAPI_ProjectPointOnCurveextremum in range, and both curve ends. - Example:
if let line = Curve3D.line(through: .zero, direction: SIMD3(1,0,0)) { let param = line.nearestParameter(to: SIMD3(5, 3, 0)) #expect(param.map { abs($0 - 5.0) < 0.1 } == true) } // A curve trimmed to [3, 8]: a point past the end is nearest to that end. if let seg = Curve3D.line(through: .zero, direction: SIMD3(1,0,0))?.trimmed(from: 3, to: 8) { #expect(seg.nearestParameter(to: SIMD3(100, 0, 0)) == 8) } // A half arc queried from below: the near end, not the extremum on the far side. if let arc = Curve3D.circle(center: .zero, normal: SIMD3(0,0,1), radius: 5)? .trimmed(from: 0, to: .pi) { #expect(arc.nearestParameter(to: SIMD3(0, -6, 0)) == 0) // 7.81 away, not 11 }
This is the scalar spelling of projectPoint(_:precision:) and agrees with it exactly. Both report the true nearest point rather than the nearest perpendicular foot: where the point has no foot, the nearest point is an end, and that is the answer.
Until #615 the two disagreed, because this one reported GeomAPI_ProjectPointOnCurve’s extremum: for the half arc above it answered π/2, the far side, 11 away, and for the trimmed segment it answered nil where projectPoint answered 8 at distance 92. Optional remains because no Double can carry a failure signal — every value is a legitimate parameter on some curve — not because a point can fail to have a nearest one.
closestParameter(to:) was the deprecated spelling of this method, returning .nan where this one returns nil (before #500 it returned 0, which is not even inside the domain of a curve trimmed to [3, 8]); removed at v2.0.0 (#784).
splitAtContinuity(continuity:tolerance:maxSegments:)
Splits this curve at continuity discontinuities, returning the resulting segments.
public func splitAtContinuity(
continuity: Int = 1,
tolerance: Double = 1e-6,
maxSegments: Int = 32
) -> [Curve3D]
The curve is first converted to BSpline form, then split at C1 (or higher) discontinuities. For continuity > 1 the implementation returns the single converted BSpline unchanged (higher-order splitting not yet implemented). Returns an empty array on failure.
- Parameters:
continuity— continuity level to split at (0=C0, 1=C1);tolerance— discontinuity detection tolerance;maxSegments— output capacity (default 32), clamped into0...Sampling.maximumSampleCount(10,000,000); 0 or less returns empty (#622). - Returns: Array of BSpline curve segments (at least one on success).
- OCCT:
GeomConvert::C0BSplineToArrayOfC1BSplineCurve(forcontinuity ≤ 1). - Example:
if let curve = Curve3D.fit(points: [SIMD3(0,0,0), SIMD3(5,5,0), SIMD3(10,0,0)]) { let segs = curve.splitAtContinuity() #expect(segs.count >= 1) }
Curve3D.concatenateG1(curves:tolerance:)
Concatenates an array of curves into a single BSpline with G1 continuity.
public static func concatenateG1(curves: [Curve3D], tolerance: Double = 1e-6) -> Curve3D?
Each input curve is converted to BSpline form before joining. Curves must connect end-to-end within tolerance.
- Parameters:
curves— ordered array of curves to join;tolerance— endpoint gap tolerance. - Returns: Joined BSpline curve, or
nilif the array is empty or conversion fails. - OCCT:
GeomConvert_CompCurveToBSplineCurve::Add+BSplineCurve(). - Example:
let pts1: [SIMD3<Double>] = [SIMD3(0,0,0), SIMD3(5,5,0), SIMD3(10,0,0)] let pts2: [SIMD3<Double>] = [SIMD3(10,0,0), SIMD3(15,-5,0), SIMD3(20,0,0)] if let c1 = Curve3D.fit(points: pts1), let c2 = Curve3D.fit(points: pts2), let joined = Curve3D.concatenateG1(curves: [c1, c2]) { #expect(joined.totalArcLength > c1.totalArcLength) }