Tessellating SPLINE Entities with ezdxf
To tessellate a DXF SPLINE with ezdxf, call entity.flattening(distance), which returns adaptive Vec3 points whose maximum deviation from the true curve never exceeds distance drawing units. A SPLINE is stored as a NURBS curve — control points, a knot vector, optional weights, and a degree — not as an explicit list of vertices, so any GIS or mesh consumer needs it converted into a polyline first. ezdxf evaluates the NURBS mathematics for you, either through the entity’s own flattening() method or through the BSpline object returned by entity.construction_tool(). This page is part of the ezdxf Deep Dive reference and shares its tolerance-driven flattening model with extracting LWPOLYLINE vertices, where bulge arcs are flattened the same way.
How ezdxf Handles SPLINE Tessellation
A SPLINE entity encodes a Non-Uniform Rational B-Spline. Its definition lives in four ezdxf attributes: entity.dxf.degree (the polynomial degree, commonly 3), entity.control_points (the defining hull), entity.knots (the non-decreasing knot vector that parameterises the curve), and entity.weights (per-control-point weights that make the curve rational when present). A separate list, entity.fit_points, holds coordinates the curve is required to pass through; a spline may be defined by control points, by fit points, or by both.
Because the curve is a continuous mathematical object, there is no single “correct” vertex list — you choose a fidelity. ezdxf offers two adaptive routes. The direct route, entity.flattening(distance, segments=4), walks the curve and subdivides recursively until the straight chord between successive samples deviates from the true curve by no more than distance; segments sets the minimum samples per knot span so that low-curvature spans still get a baseline resolution. The lower-level route, entity.construction_tool(), returns an ezdxf.math.BSpline you can drive directly with .flattening(distance) for the same sag-bounded output or .approximate(n) for a fixed count of n evenly parameterised points.
What ezdxf does not do is guess a tolerance for you or clean up degenerate inputs. A distance chosen without regard to the drawing’s units produces either a coarse chord approximation or a runaway vertex count. Splines that carry only fit points, zero-length control hulls, or duplicate knots still evaluate, but the result may collapse to a point or a straight line — cases you must detect, not assume away.
Production-Ready Script
The script tessellates every SPLINE in a DXF at a configurable sag tolerance, inspects the NURBS definition for reporting, and returns one vertex array per spline suitable for conversion of CAD polylines to GeoJSON or a mesh pipeline.
# ezdxf>=1.1.0, Python 3.9+
import ezdxf
from typing import List, Dict, Any
def tessellate_splines(
dxf_path: str,
sag_tolerance: float = 0.05,
min_segments: int = 4,
) -> List[Dict[str, Any]]:
"""Tessellate all SPLINE entities in a DXF into polyline vertex arrays.
``sag_tolerance`` is the maximum deviation between the true NURBS curve and
the approximating line segments, in drawing units. ``min_segments`` sets a
floor on samples per knot span so gentle curves still get baseline detail.
"""
doc = ezdxf.readfile(dxf_path)
msp = doc.modelspace()
results: List[Dict[str, Any]] = []
for spline in msp.query("SPLINE"):
# flattening() yields Vec3 points at an adaptive, sag-bounded density.
points = [tuple(p) for p in spline.flattening(sag_tolerance,
segments=min_segments)]
# A closed/periodic spline: make the ring explicit for polygon consumers.
if spline.closed and len(points) >= 3 and points[0] != points[-1]:
points.append(points[0])
# weights present -> rational NURBS; absent -> all weights implicitly 1.0
weights = list(spline.weights)
results.append({
"handle": spline.dxf.handle,
"layer": spline.dxf.layer,
"degree": spline.dxf.degree,
"closed": bool(spline.closed),
"rational": len(weights) > 0,
"n_control_points": len(spline.control_points),
"n_fit_points": len(spline.fit_points),
"vertex_count": len(points),
"coordinates": points, # list of (x, y, z) tuples
})
return results
if __name__ == "__main__":
for s in tessellate_splines("input.dxf", sag_tolerance=0.05):
flavour = "rational" if s["rational"] else "non-rational"
print(f"{s['handle']}: degree {s['degree']} {flavour} spline "
f"-> {s['vertex_count']} vertices on layer {s['layer']}")
Key implementation notes:
spline.flattening(sag_tolerance, segments=...)is the primary entry point. It returnsVec3objects; wrap them intuple()for plain coordinate output.- The
sag_tolerance(thedistanceargument) is a maximum deviation, not a segment count. Halving it roughly quadruples vertices in curved regions while leaving straight regions untouched. spline.weightsis empty for non-rational splines. Testing its length is a reliable way to report whether the curve is rational without touching the knot vector.spline.control_pointsandspline.fit_pointscan both be populated.flattening()evaluates the actual curve regardless of which representation drives it, so you do not choose between them for tessellation.- Closed splines are periodic;
flattening()already covers the full loop. Append the first point only to make the ring explicit for aPolygon.
Compatibility Matrix
| Component | Supported Range | Notes |
|---|---|---|
ezdxf version |
>=1.1.0 |
SPLINE.flattening() and construction_tool() are stable and recommended at 1.1.0+. |
| Python | 3.9+ |
Uses typing generics and f-strings only. |
| DXF format | R2000 (AC1015) — R2018 (AC1032) |
SPLINE supported across the full range. |
| Curve type | Rational + non-rational | weights present => rational; honoured by flattening() and BSpline. |
| Degree | Any (commonly 3) | Read from entity.dxf.degree; higher degrees tessellate identically. |
| Closed / periodic | Supported | flattening() traverses the periodic curve; check entity.closed. |
For the group-code-level view of how control points (10), knots (40), and weights (41) are stored on a SPLINE, see the DXF Entity Structure Breakdown.
Fallback Strategies
1. Choosing sag tolerance versus segment count
Prefer a sag tolerance over a fixed segment count whenever geometric fidelity matters. flattening(distance) adapts to curvature, so tight bends get more points and straight runs get fewer. Reach for construction_tool().approximate(n) only when a downstream consumer requires a fixed number of vertices per curve regardless of shape (some GPU buffers and fixed-stride formats do). Set distance as a fraction of the smallest feature you care about — for metre-unit survey data, 0.05 (5 cm) is a sensible default.
2. Rational versus non-rational splines
Do not reconstruct the curve yourself from control points assuming unit weights — a rational spline (non-empty weights) will be wrong if you ignore them. Always tessellate through flattening() or the BSpline returned by construction_tool(), both of which apply weights correctly. Use len(spline.weights) > 0 to record which curves are rational for audit.
3. Periodic and closed splines
A closed spline wraps around; its start and end coincide by construction. flattening() returns the full loop, so no manual seam handling is needed. When building a polygon ring, append the first point to close it explicitly and validate the result with Shapely before treating it as an area.
4. Degenerate or zero-length splines
Splines with fewer control points than degree + 1, coincident control points, or a collapsed knot vector can flatten to a single point or a straight line. Guard against silent emptiness:
pts = [tuple(p) for p in spline.flattening(sag_tolerance)]
if len(pts) < 2:
# Degenerate spline: log the handle and skip rather than emit a bad vertex.
print(f"Degenerate SPLINE {spline.dxf.handle}: {len(pts)} point(s)")
5. Fit-point-only splines from other CAD tools
Some exporters write a SPLINE with fit points but an empty or minimal control-point hull. flattening() still evaluates it, but if you see n_control_points == 0 alongside populated fit_points, record it — a few exotic writers omit the interpolated hull, and confirming the tessellation visually is worthwhile before trusting the geometry in a coordinate transformation and alignment step.
FAQ
What does the distance argument of spline.flattening mean?
distance is the maximum allowed deviation (the sag or sagitta) between the true spline and the straight segments approximating it, expressed in drawing units. ezdxf subdivides adaptively so that no chord ever departs from the curve by more than distance, adding points only where curvature demands them.
What is the difference between fit points and control points on a SPLINE?
Fit points are coordinates the curve passes through exactly; control points are the weighted NURBS hull that defines the curve but which it generally does not touch. A SPLINE may store one, the other, or both. When control points are present, tessellate from them via the construction tool; fit points alone require interpolation, which ezdxf handles internally.
Does ezdxf handle rational (weighted) splines?
Yes. When entity.weights is non-empty the spline is rational, and both entity.flattening() and the construction_tool() BSpline honour the weights when evaluating the curve. If weights are absent the spline is non-rational and all control points carry an implicit weight of 1.0.
How do I tessellate a closed or periodic spline?
Read entity.closed. ezdxf.flattening() already traverses the full periodic curve, so no manual wrap-around is needed. When assembling a closed ring for a polygon consumer, append the first tessellated point to the end so the ring is explicitly closed.
Related Pages
- ezdxf Deep Dive: Production-Grade DXF Parsing — parent reference covering entity traversal and geometry extraction
- Extracting LWPOLYLINE Vertices with ezdxf — the same sag-tolerance flattening applied to bulge arcs
- Reading 3D Solids with ezdxf Python — sibling workflow for
3DSOLIDACIS payloads - Converting CAD Polylines to GeoJSON — writing tessellated spline vertices as GIS features
- Aligning BIM Models with GIS Survey Data — aligning the coordinates produced after tessellation with GIS survey control