A beam's start and end caps are planar faces. The plane is the profile plane — perpendicular to the centerline at each end. The face boundary is the profile, a closed loop of curves, and for hollow sections one additional inner loop per inner contour.
Each profile curve becomes one edge in the cap face. Edges sit at the body level (SurfaceBodyDefinition.EdgeDefinitions); each edge has two vertex endpoints and a curve. An edge loop on the cap face references those edges in head-to-tail order via EdgeUseDefinitions.
Profiles
A profile in this series is an IReadOnlyList<ICurve3D>: a closed, head-to-tail list of Line3D, Arc3D, Circle3D or Ellipse3D, drawn in the XY plane and walked counter-clockwise when viewed from +Z. +Z is the beam direction; the profile is moved into place with a rigid transform later. Values are centimetres, Inventor's internal unit.
public static class Profiles
{
public static IReadOnlyList<ICurve3D> Rectangle(double width, double height)
{
double w = width / 2, h = height / 2;
var p1 = new Point3D(-w, -h, 0);
var p2 = new Point3D( w, -h, 0);
var p3 = new Point3D( w, h, 0);
var p4 = new Point3D(-w, h, 0);
return [new Line3D(p1, p2), new Line3D(p2, p3), new Line3D(p3, p4), new Line3D(p4, p1)];
}
// Line, arc, line, arc, line, arc, line, arc — eight curves.
public static IReadOnlyList<ICurve3D> RoundedRectangle(double width, double height, double radius)
{
double w = width / 2, h = height / 2, r = radius;
var z = Vector3D.ZAxis;
var x = Vector3D.XAxis;
const double q = Math.PI / 2;
Arc3D Corner(double cx, double cy, double startAngle) =>
new(new Point3D(cx, cy, 0), z, x, r, startAngle, startAngle + q);
var bottomRight = Corner( w - r, -h + r, -q); // -90° → 0°
var topRight = Corner( w - r, h - r, 0); // 0° → 90°
var topLeft = Corner(-w + r, h - r, q); // 90° → 180°
var bottomLeft = Corner(-w + r, -h + r, 2 * q); // 180° → 270°
return
[
new Line3D(bottomLeft.EndPoint, bottomRight.StartPoint), bottomRight,
new Line3D(bottomRight.EndPoint, topRight.StartPoint), topRight,
new Line3D(topRight.EndPoint, topLeft.StartPoint), topLeft,
new Line3D(topLeft.EndPoint, bottomLeft.StartPoint), bottomLeft,
];
}
public static IReadOnlyList<ICurve3D> Circle(double radius) =>
[new Circle3D(new Point3D(0, 0, 0), radius, Vector3D.ZAxis)];
public static IReadOnlyList<ICurve3D> Ellipse(double semiMajor, double semiMinor) =>
[new Ellipse3D(new Point3D(0, 0, 0), semiMajor, semiMinor, Vector3D.ZAxis, Vector3D.XAxis)];
public static bool IsClosedCurve(ICurve3D c) => c is Circle3D or Ellipse3D;
}
The lines between the corner arcs are taken from the arcs' own StartPoint / EndPoint, so consecutive curves share their endpoints exactly. An I-section with root fillets is built the same way (sixteen curves; the fillets are concave, so those four arcs run clockwise inside the counter-clockwise loop) — it turns up in Post 8.
The bridge grows: arcs, circles, ellipses
The bridge from the construction-tree post gains one converter per curve type, plus a dispatcher that turns any ICurve3D into the object EdgeDefinitions.Add expects.
// Inventor's Arc3d always sweeps counter-clockwise about its normal, so a clockwise managed
// arc is expressed with the normal flipped and a positive sweep. The reference vector points
// at the arc's start point, so the start angle is 0.
public Arc3d Arc(Arc3D a)
{
var toStart = (a.StartPoint - a.Center).Normalized();
var axis = a.SweepAngle >= 0 ? a.Normal : -a.Normal;
return tg.CreateArc3d(Pt(a.Center), Unit(axis), Unit(toStart), a.Radius, 0, Math.Abs(a.SweepAngle));
}
public Circle Circ(Circle3D c) => tg.CreateCircle(Pt(c.Center), Unit(c.Normal), c.Radius);
public EllipseFull Ell(Ellipse3D e) =>
tg.CreateEllipseFull(Pt(e.Center), Unit(e.Normal),
Vec(e.MajorAxisDirection * e.SemiMajorAxis), e.SemiMinorAxis / e.SemiMajorAxis);
public object Curve(ICurve3D c) => c switch
{
Line3D l => Seg(l),
Arc3D a => Arc(a),
Circle3D k => Circ(k),
Ellipse3D e => Ell(e),
_ => throw new NotSupportedException($"No Inventor conversion for {c.GetType().Name}"),
};
CreateArc3d takes the centre, the normal, a reference unit vector, the radius, the start angle and the sweep angle. CreateEllipseFull takes the major axis as a Vector whose length is the semi-major axis, and the minor/major ratio.
Profile edges
Turning a profile into body-level topology is one vertex per curve start and one edge per curve. Consecutive curves share a vertex: the end vertex of curve i is the start vertex of curve i+1, and the last curve closes back to the first. A closed curve (circle, ellipse) is a single edge whose start and end vertex are the same.
public sealed record ProfileTopology(VertexDefinition[] Vertices, EdgeDefinition[] Edges);
public static ProfileTopology AddProfileEdges(
SurfaceBodyDefinition bodyDef, InventorGeometry geo, IReadOnlyList<ICurve3D> profile)
{
int n = profile.Count;
var vertices = new VertexDefinition[n];
var edges = new EdgeDefinition[n];
for (int i = 0; i < n; i++)
vertices[i] = bodyDef.VertexDefinitions.Add(geo.Pt(profile[i].StartPoint));
for (int i = 0; i < n; i++)
{
var start = vertices[i];
var end = Profiles.IsClosedCurve(profile[i]) ? vertices[i] : vertices[(i + 1) % n];
edges[i] = bodyDef.EdgeDefinitions.Add(start, end, geo.Curve(profile[i]));
}
return new ProfileTopology(vertices, edges);
}
The ProfileTopology is kept: the next post's side faces reference the same vertices and edges, which is how the caps and sides end up sharing topology instead of duplicating it.
Loops and cap faces
An edge loop is the profile's edges in order. Walking it "reversed" means the edges in reverse order, each with isOpposedToEdge = true.
public static void AddLoop(FaceDefinition faceDef, EdgeDefinition[] edges, bool reversed)
{
var loop = faceDef.EdgeLoopDefinitions.Add();
if (!reversed)
{
foreach (var e in edges) loop.EdgeUseDefinitions.Add(e, false);
}
else
{
for (int i = edges.Length - 1; i >= 0; i--) loop.EdgeUseDefinitions.Add(edges[i], true);
}
}
A cap face is a plane whose normal is the face's outward normal, an outer loop and any inner loops. The outer loop is walked counter-clockwise about the outward normal, inner loops clockwise. Since profiles are counter-clockwise about +Z, an end cap (outward = +Z) walks the outer profile forward and inner profiles reversed; a start cap (outward = −Z) does the opposite.
public static FaceDefinition AddCapFace(
FaceShellDefinition shellDef, InventorGeometry geo, Plane3D plane,
ProfileTopology outer, IEnumerable<ProfileTopology> inners, bool outwardIsProfileNormal)
{
var faceDef = shellDef.FaceDefinitions.Add(geo.Pln(plane), false);
AddLoop(faceDef, outer.Edges, reversed: !outwardIsProfileNormal);
foreach (var inner in inners)
AddLoop(faceDef, inner.Edges, reversed: outwardIsProfileNormal);
return faceDef;
}
The construction-tree post showed that Inventor corrects loop winding on planar faces of a closed body anyway; the code still walks them consistently because the same loop-walking rule is what makes toroidal faces come out right for curved beams later in the series.
A hollow cap on its own
The end face of a 100 × 50 RHS with 5 mm corner radii and a rectangular inner contour, as a single planar sheet:
var geo = new InventorGeometry(app.TransientGeometry);
var bodyDef = app.TransientBRep.CreateSurfaceBodyDefinition();
var shellDef = bodyDef.LumpDefinitions.Add().FaceShellDefinitions.Add();
var outer = AddProfileEdges(bodyDef, geo, Profiles.RoundedRectangle(10, 5, 0.5));
var inner = AddProfileEdges(bodyDef, geo, Profiles.Rectangle(8, 3));
var plane = new Plane3D(new Point3D(0, 0, 0), Vector3D.ZAxis);
AddCapFace(shellDef, geo, plane, outer, [inner], outwardIsProfileNormal: true);
var body = bodyDef.CreateTransientSurfaceBody(out NameValueMap errors);
Result: 1 face, 12 edges (8 outer, 4 inner), 12 vertices, IsSolid == false.
For a CHS the outer profile is one Circle3D; AddProfileEdges produces one vertex and one self-loop edge, and the loop has one edge use. A thick-walled tube is the same with a second, smaller circle as the inner loop.
Two caps, one body
A beam needs a start cap and an end cap: two planar faces on the same FaceShellDefinition, each with its own vertices and edges (AddProfileEdges is called once per cap on the profile transformed to that end). Nothing is shared between the caps. The body is one lump, open along the sides, until the next post connects the caps with side faces — one per profile curve, with the surface type set by whether the curve is a line, an arc, or a closed curve.
