Exports
EngrCAD exports STEP (exact B-Rep), its own lossless .ecb B-Rep archive, STL, OBJ, OFF,
3MF, AMF and glTF 2.0 (meshes), VTU (meshes plus simulation results), and PNG renders. The
snippets on this page run against
a temp directory (Scratch) during the
docs build, so the export paths are exercised on every build — no screenshots needed
here.
STEP (exact geometry)
StepWriter emits AP214 MANIFOLD_SOLID_BREP with analytic surfaces and curves
(including rational NURBS); StepReader imports it back. Round-trip:
var solid = (Shape.Box(30, 20, 10) - Shape.Cylinder(4, 30).Translate(8, 0, 0)).ToBrep();
var path = Path.Combine(Scratch, "plate.step");StepWriter.WriteFile(solid, path, name: "drilled plate");
var result = StepReader.ReadFile(path);if (result.Solids.Count != 1) throw new Exception($"expected 1 solid, got {result.Solids.Count}: " + string.Join("; ", result.Diagnostics));Console.WriteLine($"round-tripped {result.Solids[0].Faces.Count()} faces");Swept surfaces are not exportable yet; Part.Source tracks whether a part is
B-Rep-representable for the --export switch.
The exporter covers the full analytic conic family (circles, ellipses, parabolas,
hyperbolas, planar offset curves) plus rational NURBS — including translated NURBS
profile edges, which export exactly by transforming control points rather than being
sampled. The importer maps the same entities back, and also synthesizes foreign
CONICAL_SURFACE/TOROIDAL_SURFACE entities onto the kernel’s revolved-surface
machinery.
Units
Foreign files declare their length unit, and the importer scales everything to
millimetres — SI prefixes in closed form, CONVERSION_BASED_UNIT chains (inches)
multiplied down — reporting the factor as a diagnostic:
// Simulate a foreign metre-unit file: our own writer always declares millimetres.var metreCube = SolidFactory.MakeBox(new Aabb((0, 0, 0), (1, 1, 1)));string text = StepWriter.Write(metreCube).Replace(".MILLI.,.METRE.", "$,.METRE.");
var imported = StepReader.Read(text);double size = imported.Solids[0].Vertices.Max(v => v.Position.X);Console.WriteLine($"1 m cube imported as {size} mm"); // 1000if (Math.Abs(size - 1000) > 1e-9) throw new Exception("metre units were not scaled");Native B-Rep archive (.ecb)
STEP is the interchange format; the .ecb archive is the lossless one. It round-trips
every curve and surface type the kernel has, including the ones STEP has no entity for —
helical surfaces (modelled threads), lofted surfaces, swept (RMF) surfaces, offset curves,
spiral arcs — plus trimmed edge domains and CurveSegment mappings.
It is a versioned, human-diffable text format: a numbered entity table where every
reference is #n, so shared topology stays shared (an edge used by two faces is written
once and referenced twice), one entity per line, dependencies always defined before use.
// A modelled thread: a HelicalSurface, which STEP cannot represent at all.// (chamferEnds: false is the B-Rep-native form -- the end cones have no exact B-Rep.)var rod = Shape.ExternalThread(8, length: 10, chamferEnds: false).ToBrep();
var path = Path.Combine(Scratch, "rod.ecb");BrepArchive.WriteFile(rod, path, name: "M8 rod");
var result = BrepArchive.ReadFile(path);var restored = result.Single();restored.Validate();
Console.WriteLine($"{result.Name}: {restored.Faces.Count()} faces, " + $"{restored.Edges.Count()} edges, format v{result.Version}");if (restored.Faces.Count() != rod.Faces.Count()) throw new Exception("face count changed across the round trip");
// save -> load -> save is byte-identical: the strong form of "exact".if (BrepArchive.Write(restored, "M8 rod") != File.ReadAllText(path)) throw new Exception("the archive is not a fixed point under round-trip");The file reads like this — Solid at the end referencing shells, referencing faces,
referencing surfaces and loops all the way down:
ENGRCAD-BREP 1UNITS MMNAME 'M8 rod'GENERATOR 'EngrCAD'
#1 = Line((4 0 -0.078125), (4 0 0.078125))#2 = Helical((0 0 0 1 0 0 0 1 0), (4 -0.078125), (4 0.078125), 1.25, (0 50.26548))#3 = Vertex((4 0 -0.078125))...ROOT #57Two contract points. An unknown version is refused by name rather than parsed hopefully — a newer writer may have added entity forms, and a partial parse of a solid we cannot build is worse than a clear message. And units are declared and checked, the lesson the STEP importer paid for.
Reading gives you an ordinary BrepSolid, so it feeds straight back into the modelling
API:
var original = (Shape.Box(30, 20, 10) - Shape.Cylinder(4, 30)).ToBrep();BrepArchive.WriteFile(original, Path.Combine(Scratch, "plate.ecb"));
var reloaded = BrepArchive.ReadFile(Path.Combine(Scratch, "plate.ecb")).Single();var further = Shape.From(reloaded) - Shape.Cylinder(3, 30).Translate(10, 0, 0);
var mesh = further.ToMesh();Console.WriteLine($"reloaded and cut again: {mesh.FaceCount} facets, closed = {mesh.IsClosed}");if (!mesh.IsClosed) throw new Exception("the reloaded solid did not survive a second boolean");Healing imported files
Foreign writers emit each face’s wire separately, so a solid can arrive as a face
soup: duplicated edges, merely-coincident vertices, inconsistent face orientations,
disconnected bodies sharing one shell. ShapeHealing.Heal repairs what it can and
reports everything — including what it could not repair — as a return value:
var body = SolidFactory.MakeBox(new Aabb((0, 0, 0), (10, 10, 10)));var boss = SolidFactory.MakeBox(new Aabb((20, 0, 0), (26, 6, 6)));// A foreign writer's flat face list: two disconnected bodies in ONE shell.var soup = new BrepSolid([new BrepShell([.. body.Faces, .. boss.Faces])]);
var healed = ShapeHealing.Heal(soup);Console.WriteLine(healed.Report); // "1 shells split off; result is a closed manifold."if (healed.Solid.Shells.Count != 2) throw new Exception("expected the disconnected components split into two shells");ShapeHealing.Analyze is the dry run; ShapeHealingOptions switches each pass
(vertex merging, small-edge collapse, sewing, wire re-ordering, shell repair) and
opts into the two passes that adjust geometry or trims (RefitStraightEdges,
RetrimCurvedEdges).
STL (3D printing)
Binary STL, single mesh or multiple parts merged with their transforms applied:
var bracket = Shape.Extrude(Sketch.RoundedRectangle(40, 24, 5), 8) .Drill(StandardHoles.Clearance(5), [new(-14, 0), new(14, 0)], depth: 10, SketchPlane.At((0, 0, 8), Vector3d.UnitX, Vector3d.UnitY));
StlWriter.WriteFile(bracket.ToMesh(), Path.Combine(Scratch, "bracket.stl"));
// Multi-part: merge parts with placement transforms, slicer-ready.StlWriter.WriteFile( [(bracket.ToMesh(), Matrix4d.Identity), (Shape.Cylinder(3, 12).ToMesh(), Matrix4d.CreateTranslation((0, 30, 0)))], Path.Combine(Scratch, "assembly.stl"));
if (new FileInfo(Path.Combine(Scratch, "bracket.stl")).Length < 84) throw new Exception("STL came out empty");OBJ
var mesh = Shape.Torus(12, 4).ToMesh();ObjWriter.WriteFile(mesh, Path.Combine(Scratch, "torus.obj"));if (!File.ReadLines(Path.Combine(Scratch, "torus.obj")).Any(l => l.StartsWith("v "))) throw new Exception("OBJ has no vertices");3MF and AMF (modern printing formats)
3MF is the format today’s slicers prefer: a zip package whose model XML carries per-object names and colors, so a multi-part print arrives as a named object list rather than one anonymous triangle soup. AMF is its ISO/ASTM predecessor — plain XML, same idea. Both writers take the same part list: mesh + transform + name + optional RGB color (transforms are baked into the vertices, mirrored parts keep outward windings):
var body = Shape.Box(30, 20, 8).ToMesh();var cap = Shape.Cylinder(6, 4).ToMesh();
var parts = new List<MeshExportPart>{ new(body, Matrix4d.Identity, "body", (0.55f, 0.68f, 0.84f)), new(cap, Matrix4d.CreateTranslation((0, 0, 6)), "cap", (0.85f, 0.72f, 0.38f)),};ThreeMfWriter.WriteFile(parts, Path.Combine(Scratch, "widget.3mf"));AmfWriter.WriteFile(parts, Path.Combine(Scratch, "widget.amf"));
// The 3MF is a real OPC package: content types, relationships, model XML.using (var archive = System.IO.Compression.ZipFile.OpenRead(Path.Combine(Scratch, "widget.3mf"))){ if (archive.GetEntry("3D/3dmodel.model") is null) throw new Exception("3MF package is missing its model");}glTF 2.0 (the web, AR and DCC route)
GltfWriter writes binary .glb and self-contained .gltf (the buffer rides inline
as a data URI, so there is no sidecar .bin to lose). It is the one mesh format here
that keeps the assembly hierarchy instead of flattening it: glTF has real nodes, so
GltfScene.Plan emits a node per tab, per sub-assembly and per occurrence, with one
mesh per distinct part however many times it is placed. A fastener placed fifty times
is written once — the same “one product, N occurrences” structure the STEP assembly
writer uses, and the property the baking exporters (STL, OBJ, 3MF) structurally cannot
have.
var bolt = new Part("bolt", Shape.Cylinder(2, 12), Palette.Steel);var plate = new Part("plate", Shape.Box(60, 40, 6), Palette.Brass);
var stack = new Assembly("stack");stack.Add(plate);foreach (var x in new[] { -20.0, 0.0, 20.0 }) stack.Add(bolt, Frame3d.FromOrthonormal((x, 0, 6), Vector3d.UnitX, Vector3d.UnitY));
var scene = new Scene();scene.AddTab("Assembly").Add(stack);
var plan = GltfScene.WriteFile(scene, Path.Combine(Scratch, "stack.glb"));
// TWO meshes for four placements: the bolt is written once and instanced.Console.WriteLine($"{plan.Geometries.Count} meshes, {plan.Roots.Count} root node(s)");if (plan.Geometries.Count != 2) throw new Exception($"expected 2 meshes, got {plan.Geometries.Count}");
var bytes = File.ReadAllBytes(Path.Combine(Scratch, "stack.glb"));if (System.Text.Encoding.ASCII.GetString(bytes, 0, 4) != "glTF") throw new Exception("not a GLB");Colours become PBR metallic-roughness materials, translucent parts declare
alphaMode: BLEND, and a part carrying a FieldDisplay exports its simulation-result
colours as the COLOR_0 vertex attribute — the same colours the viewport draws, since
both come from FieldRendering.SourceColors. So an FEA result can be handed to a
browser or a phone as one file.
Three conventions worth knowing:
- Y-up, metres. glTF is Y-up and metric; EngrCAD is Z-up millimetres. The conversion
rides on a single root node built from exact values, so every part transform below it
stays verbatim.
new GltfOptions { YUp = false, Scale = 1 }writes model coordinates. - Winding is not flipped under mirroring. The spec requires the consumer to reverse winding when a node’s transform has a negative determinant, so the transform is written as-is; flipping here as well would double the correction.
- Deformation does not travel. A displacement exaggeration is a viewing parameter and glTF has nowhere to record one, so a file carrying 50×-displaced geometry would be indistinguishable from a model that really is that shape. The colours go, the displacement stays behind.
OFF
The writer twin of the OFF reader — handy for mesh-processing toolchains (Meshlab, CGAL). N-gon faces are written as-is:
var mesh = Shape.Cone(8, 3, 10).ToMesh();OffWriter.WriteFile(mesh, Path.Combine(Scratch, "cone.off"));var read = MeshReader.ReadFile(Path.Combine(Scratch, "cone.off"));if (read.Mesh is null || !read.Mesh.IsClosed) throw new Exception("OFF did not round-trip closed");From a model program
Any program using EngrCad.Run gets headless export and render switches for free —
no code changes, CI-friendly, no window:
dotnet run --project MyDesign -- --export bracket.step # STEP per B-Rep partdotnet run --project MyDesign -- --export bracket.ecb # native lossless B-Rep archivedotnet run --project MyDesign -- --export bracket.stl # merged binary STLdotnet run --project MyDesign -- --export bracket.obj # merged OBJdotnet run --project MyDesign -- --export bracket.off # merged OFFdotnet run --project MyDesign -- --export bracket.3mf # 3MF (named, colored objects)dotnet run --project MyDesign -- --export bracket.amf # AMF (named, colored objects)dotnet run --project MyDesign -- --export bracket.glb # glTF 2.0 (hierarchy + materials)dotnet run --project MyDesign -- --export bracket.gltf # glTF 2.0, self-contained JSONdotnet run --project MyDesign -- --export bracket.vtu # VTK unstructured grid + resultsdotnet run --project MyDesign -- --render bracket.png # offscreen PNG render--render uses the same offscreen renderer that produced every screenshot in these
docs (EngrCad.RenderToImage) — see the viewer.