Construction & Sketching
The construction & sketching types implement Fusion 360-style parametric reference geometry and 2D profile creation. ConstructionEntity types carry recipes (plane, axis, point definitions keyed on TopologyRefs) that are resolved against a live BRepGraph; ConstructionContext is the document-level registry for those entities; ConstructionLayer bridges them to the XCAF document layer system for STEP persistence; Sketch hosts 2D curve elements on a plane and builds a 3D profile wire; and Section2D extends Shape to produce 2D contour drawings from planar section cuts.
Topics
- Placement · ConstructionPlane · ConstructionAxis · ConstructionPoint · ConstructionResolutionError · BRepGraph resolve extensions · BRepGraph.childIndices · ConstructionContext · Document.constructionContext · ConstructionLayer, Document extension · ConstructionContext.materialize · SketchElement · Sketch · Shape.section2D · Shape.SectionView
Placement
A rigid-body placement in 3D space, an origin plus an orthonormal basis. Used as the resolved output of ConstructionPlane queries and as the coordinate frame for sketch hosting.
Placement.init(origin:xAxis:yAxis:zAxis:)
Constructs a placement from an explicit orthonormal frame.
public init(origin: SIMD3<Double>, xAxis: SIMD3<Double>, yAxis: SIMD3<Double>, zAxis: SIMD3<Double>)
All four vectors must be provided by the caller; no normalisation is performed. zAxis is the plane normal when the placement describes a construction plane.
- Parameters:
origin: the origin point of the frame.xAxis: unit vector along the local X axis.yAxis: unit vector along the local Y axis.zAxis: unit vector along the local Z axis (plane normal).
- Example:
let placement = Placement( origin: SIMD3(0, 0, 10), xAxis: SIMD3(1, 0, 0), yAxis: SIMD3(0, 1, 0), zAxis: SIMD3(0, 0, 1) )
Placement.zAxis
Unit vector along the local Z axis: the frame’s third basis vector, and the plane normal for a construction plane.
public let zAxis: SIMD3<Double> // unit
- Returns: The Z basis vector supplied at construction (or derived from
normalbyinit(origin:normal:)); not re-normalised on read.
Placement.lift(_:)
Maps a 2D point in the placement’s local (x, y) plane to 3D world space.
public func lift(_ p: SIMD2<Double>) -> SIMD3<Double>
- Parameters:
p: 2D coordinates wherexscalesxAxisandyscalesyAxis.
- Returns: The corresponding 3D point:
origin + x * xAxis + y * yAxis. - Example:
let placement = Placement(origin: SIMD3(1, 2, 3), normal: SIMD3(0, 0, 1)) let worldPoint = placement.lift(SIMD2(5, 10)) // worldPoint == SIMD3(6, 12, 3)
Placement.init(origin:normal:)
Constructs a placement from an origin and a normal, deriving deterministic X/Y axes perpendicular to the normal.
public init(origin: SIMD3<Double>, normal: SIMD3<Double>)
Derives the X and Y axes using the canonical perpendicularBasis(to:) algorithm, shared with OCCT’s gp_Ax2 constructor. This picks the component of the normal with the smallest magnitude and constructs the perpendicular basis algebraically, with no fallback branches. Use this convenience form when you only know the plane origin and normal and don’t care about a specific X orientation.
- Parameters:
origin: the origin point of the plane.normal: plane normal; will be normalised.
- Example:
let xzPlacement = Placement(origin: .zero, normal: SIMD3(0, 1, 0)) // xAxis ≈ (0,0,1), yAxis ≈ (1,0,0)
ConstructionPlane
A recipe for a construction plane. Each case carries its defining inputs as TopologyRefs (or absolute geometry) that are resolved against a BRepGraph at use time, so the plane tracks model edits automatically.
ConstructionPlane.absolute(origin:normal:)
A fixed plane defined by a world-space point and normal.
case absolute(origin: SIMD3<Double>, normal: SIMD3<Double>)
Resolves immediately without consulting the topology graph, always succeeds.
- Example:
let xy = ConstructionPlane.absolute(origin: .zero, normal: SIMD3(0, 0, 1))
ConstructionPlane.offsetFromFace(face:distance:)
A plane parallel to a topological face, offset by distance along the face normal.
case offsetFromFace(face: TopologyRef, distance: Double)
- Parameters:
face: topology reference resolving to a face node.distance: signed offset along the outward face normal (positive = outward).
The face’s own point/normal come from its UV-domain midpoint (Face.uvMidpointSample()), a face-representative sample, appropriate here, since this case wants a plane parallel to the whole face rather than tangent at a specific point (contrast tangentToFace below, #879).
ConstructionPlane.throughAxis(axis:angleDeg:)
A plane containing an edge-derived axis, rotated angleDeg degrees from the reference perpendicular.
case throughAxis(axis: TopologyRef, angleDeg: Double)
axis resolves through the same resolveEdgeDirection helper alongEdge uses (see that entry above), not a bare endpoint-to-endpoint chord: for a non-linear edge next to a cylindrical or conical face whose axis the edge is a genuine circular cross-section of, this resolves to that face’s true rotation axis, not the edge’s own chord, e.g. axis naming a hole rim’s edge resolves to the hole’s own axis, anchored where that edge sits along it (#894). A linear edge, or a curved one with no qualifying adjacent face, still resolves to its chord as before.
The reference perpendicular is perpendicularBasis(to: dir), the same canonical, gp_Ax2- matching basis every other perpendicular-to-a-direction site in this module now shares (#881), not the previous per-call-site worldUp × axis. Behavior change, same scope as #881’s own: the zero-degree reference plane moved for every axis direction except world ±Z (see #881’s own CHANGELOG entry for the full explanation and a worked example), a throughAxis plane built against a world ±X- or ±Y-direction axis at a given angleDeg is not the same plane a prior release would have built at that angle. Rotation is about the axis direction.
- Parameters:
axis: topology reference to an edge; need not be linear (see above).angleDeg: rotation angle in degrees around the axis, from theperpendicularBasis(to:)reference.
ConstructionPlane.tangentToFace(face:at:)
A plane tangent to a face at a point.
case tangentToFace(face: TopologyRef, at: TopologyRef)
- Parameters:
face: topology reference resolving to the face.at: topology reference resolving to a vertex on that face.
The normal is evaluated at at’s own projected UV location on face (via Face.project(point:) + Face.normal(atU:v:)), not the face’s UV-domain midpoint, so on a curved face (cylinder, cone, sphere, torus, freeform) the plane is genuinely tangent at the requested point, not just coincidentally correct the way a planar face makes any point’s normal agree with any other’s (#879). The Placement’s origin is the actual projected point, not the raw at point verbatim, they can differ when at doesn’t genuinely lie on face (PR #897 review, finding 2). The projection is bounded to face’s UV bounds (a rectangular box in parameter space), not the exact trimmed boundary, so for a non-convex or holed face the origin can land inside that box but outside the real trimmed region (#914 review, finding 3). If either face.project(point:) itself fails to converge, or the projected point is a genuine parametric singularity (e.g. a cone apex, where the two tangent directions coincide rather than merely shrink, a sphere’s pole is not such a point, OCCT still resolves a normal there), the exact local normal is undefined, and this falls back to the face’s UV-midpoint sample instead of failing, so tangentToFace still succeeds for any face with valid uvBounds (PR #897 review, 3rd + xhigh pass).
// Tangent to a cylindrical face at a specific vertex, the plane's normal
// follows the vertex's own local surface normal, not the face's midpoint.
let plane = ConstructionPlane.tangentToFace(face: cylindricalFaceRef, at: vertexRef)
if case .success(let p) = graph.resolve(plane) {
print(p.zAxis) // the true local tangent-plane normal at `at`
}
ConstructionPlane.midPlane(_:_:)
A midplane equidistant between two parallel faces.
case midPlane(TopologyRef, TopologyRef)
The normal is the average (or either half-normal for antiparallel faces) of the two face normals, each taken from Face.uvMidpointSample(), a face-representative sample, appropriate here since both faces contribute as wholes rather than at a specific point.
ConstructionPlane.byThreePoints(_:_:_:)
A plane defined by three vertex-resolved points.
case byThreePoints(TopologyRef, TopologyRef, TopologyRef)
Fails with .degenerate("three points are collinear") if the cross product is near-zero.
ConstructionPlane.normalToEdge(edge:t:)
A plane normal to an edge at a fractional parameter along its length.
case normalToEdge(edge: TopologyRef, t: Double)
- Parameters:
edge: topology reference to the edge.t: parameter in[0, 1]along the edge; clamped automatically.
ConstructionAxis
A recipe for a construction axis. Resolved to (origin: SIMD3<Double>, direction: SIMD3<Double>) against a BRepGraph.
ConstructionAxis.absolute(origin:direction:)
A fixed axis at a world-space origin and direction.
case absolute(origin: SIMD3<Double>, direction: SIMD3<Double>)
ConstructionAxis.alongEdge(_:)
An axis coinciding with a linear edge or the revolution axis of a cylindrical or conical edge.
case alongEdge(TopologyRef)
For a non-linear edge, every adjacent face whose Face.primaryAxis is a cylinder or cone is gathered as a candidate; the axis redirect is taken only when all candidates agree (axesAgree, within tolerance) and the edge itself passes a coaxial-cross-section check, constant radius and height along the candidate axis at several sampled points along the edge. That check is what rejects an elliptical rim (an oblique-plane cut of a cylinder, whose height along the axis varies around the curve) or a helical edge: both fall through to the endpoint-to-endpoint chord instead, the same fallback used for a genuinely linear edge, a curved edge with no qualifying adjacent face, or disagreeing candidates (e.g. at a T-branch). This also resolves a full-circle edge (a hole rim, say), where the endpoints coincide and a chord would be the zero vector. A zero-length fallback result fails with .degenerate("zero-length edge").
Once a qualifying axis is found, its sign is re-derived from the edge’s own start→end parameterization (Face.primaryAxis’s stored sign is the adjacent surface’s convention, unrelated to which end of the edge is “first”), this needs Edge.tangent(at:) at the edge’s own start point, which can fail even where Edge.point(at:) already succeeded (a first-derivative singularity on a reparameterized curve). Where that happens, this fails with .degenerate("edge tangent unavailable at start; cannot determine axis sign") rather than falling back to Face.primaryAxis’s unflipped, possibly-wrong-signed direction, a plausible-looking wrong sign is worse than an explicit failure (#914 review, second round).
ConstructionAxis.normalToFace(face:at:)
An axis perpendicular to a face at a reference vertex, anchored on the face’s true rotation axis line (projected from at) for cylindrical/conical/toroidal/revolved faces, or at at’s own projected on-face location otherwise. See below for the full per-kind breakdown.
case normalToFace(face: TopologyRef, at: TopologyRef)
The direction comes from Face.primaryAxis when the face has one and is genuinely a rotation axis, cylindrical, conical, toroidal, and surface-of-revolution faces (an allow-list, so a future ShapeAxis.Kind this code doesn’t yet know about defaults to the normal-based fallback below rather than being treated as a genuine axis, PR #897 review, finding 8), so it is the surface’s own constant axis of revolution, not a per-point sample (#882). For planar and free-form faces, which have no primaryAxis at all, it falls back to the local surface normal at at’s own projected location on the face, the same point-aware projection tangentToFace uses above, so this varies with at instead of always answering the fixed UV-midpoint normal (PR #897 review, finding 4), falling back further to the UV-midpoint normal if that projection or normal lookup fails.
Surface-of-extrusion faces also have a primaryAxis, but its direction is the sweep direction of Geom_SurfaceOfLinearExtrusion (tangent to the surface, not perpendicular to it), so they’re deliberately excluded from the axis branch. Spherical faces are excluded too, for a different reason: a sphere has no intrinsic rotation axis at all (it’s symmetric about every axis through its center), so Face.primaryAxis reports the arbitrary construction-frame pole, the same fixed direction regardless of which point on the sphere at names, rather than a property of the surface (PR #897 review, 3rd pass). Both fall back to the face’s UNconditional UV-midpoint normal, not the point-aware projection above: a sphere’s true local normal at its own pole is parallel to this very (excluded) axis, the radial direction from center, so a point-aware fallback there would silently reproduce the excluded axis by another route at exactly the vertex the exclusion exists to guard (PR #897 review, xhigh pass).
The returned axis’s origin is never the raw at point verbatim, for the same reason tangentToFace’s origin isn’t (finding 2 above). For cylindrical/conical/toroidal/revolved faces it’s at’s own position projected onto the face’s true rotation axis LINE, axis.origin + ((at − axis.origin) · direction) * direction, kept edge-local (nearest to at along the axis) rather than snapped to the surface’s own placement origin, which can be far away (#914 review, finding 1 corollary; #897 review, third pass). Pairing the correct direction with the raw, generally off-axis at point (a vertex on the surface sits offset from the true centerline by the surface’s own radius) would describe a different line entirely, parallel to but not coincident with the real axis. For the no-primaryAxis fallback, the origin is at’s own projected on-face location, the same point the direction was evaluated at. For the sphere/extrusion exclusion branch, both origin and direction come from the face’s UV midpoint together.
// On a cylindrical face this resolves to the cylinder's own axis (constant
// direction everywhere on the face), anchored ON that axis line, not at the
// vertex's own off-axis position on the surface.
let axis = ConstructionAxis.normalToFace(face: cylindricalFaceRef, at: vertexRef)
if case .success(let a) = graph.resolve(axis) {
print(a.direction) // the cylinder's rotation axis
print(a.origin) // on the true centerline, not on the cylinder's surface
}
ConstructionAxis.throughPoints(_:_:)
An axis through two vertex-resolved points.
case throughPoints(TopologyRef, TopologyRef)
Fails with .degenerate("points coincide") when the two points are within 1e-9 of each other.
ConstructionAxis.intersectionOfPlanes(_:_:)
An axis at the intersection line of two planes.
case intersectionOfPlanes(ConstructionPlane, ConstructionPlane)
Fails with .degenerate("planes are parallel") when the cross product is near-zero.
ConstructionPoint
A recipe for a construction point. Resolved to SIMD3<Double> against a BRepGraph.
ConstructionPoint.absolute(_:)
A fixed world-space point.
case absolute(SIMD3<Double>)
ConstructionPoint.atVertex(_:)
The 3D coordinate of a topology vertex.
case atVertex(TopologyRef)
- OCCT:
OCCTShapeVertexPoint, reads thegp_Pntfrom aTopoDS_Vertex.
ConstructionPoint.midpointOfEdge(_:)
The point at the parametric midpoint (t = 0.5) of an edge.
case midpointOfEdge(TopologyRef)
ConstructionPoint.centroidOfFace(_:)
The face’s real area centroid.
case centroidOfFace(TopologyRef)
Computed via Face.surfaceInertia.centerOfMass, the same moment-based integration Shape.measure().faceCentroids uses, not a UV-parameter midpoint (#884). For a non-uniformly parameterized surface (a sphere, cone, or general NURBS face) this can differ substantially from a UV-midpoint sample, and for a closed or symmetric face the true centroid can lie off the surface entirely (e.g. at a full sphere’s center). Fails with .degenerate("face area is zero, or its inertia could not be computed") rather than reporting a fabricated point, the message doesn’t commit to “zero area” alone because centerOfMass is also nil when the underlying BRepGProp_Sinert computation itself fails (e.g. a self-intersecting face), a distinct cause the Swift API can’t currently tell apart from genuine zero area (#897 review, third pass).
// A cylinder's lateral face has its true area centroid on the cylinder's own axis
// (radial distance 0), a UV-midpoint sample instead sits a full radius off-axis.
let point = ConstructionPoint.centroidOfFace(cylindricalFaceRef)
switch graph.resolve(point) {
case .success(let p):
print(p) // on-axis, at the face's true area centroid
case .failure(.degenerate):
print("zero area, or inertia couldn't be computed") // e.g. a collapsed-fillet sliver
case .failure(let error):
print(error)
}
ConstructionPoint.atEdgeParameter(edge:t:)
The 3D point at a fractional parameter along an edge.
case atEdgeParameter(edge: TopologyRef, t: Double)
- Parameters:
t, in[0, 1]; clamped before use.
ConstructionPoint.intersectionOfAxisAndPlane(_:_:)
The 3D point where an axis intersects a plane.
case intersectionOfAxisAndPlane(ConstructionAxis, ConstructionPlane)
Fails with .degenerate("axis parallel to plane") when the axis direction is perpendicular to the plane normal.
ConstructionResolutionError
Error type returned when a construction entity fails to resolve against the topology graph.
public enum ConstructionResolutionError: Error, Sendable {
case topology(TopologyResolutionError)
case notApplicable(String)
case degenerate(String)
case missingGeometry(BRepGraph.NodeRef)
}
.topology: the underlyingTopologyRefcould not be resolved (e.g. the node was deleted)..notApplicable: the referenced node is the wrong kind (e.g. an edge where a face was expected)..degenerate: the geometry is valid but produces a degenerate result (e.g. collinear points, parallel planes)..missingGeometry: the node exists in the graph but carries no shape geometry.
ConstructionResolutionError.topology
The underlying TopologyRef could not be resolved (e.g. the node was deleted).
case topology(TopologyResolutionError)
ConstructionResolutionError.notApplicable
The referenced node is the wrong kind (e.g. an edge where a face was expected).
case notApplicable(String)
ConstructionResolutionError.degenerate
The geometry is valid but produces a degenerate result (e.g. collinear points, parallel planes).
case degenerate(String)
ConstructionResolutionError.missingGeometry
The node exists in the graph but carries no shape geometry.
case missingGeometry(BRepGraph.NodeRef)
BRepGraph resolve extensions
BRepGraph is extended in ConstructionEntity.swift to resolve the three construction entity types. These are the primary resolution entry points.
BRepGraph.resolve(_:) for ConstructionPlane
Resolves a ConstructionPlane recipe against the current graph state.
public func resolve(_ plane: ConstructionPlane) -> Result<Placement, ConstructionResolutionError>
- Returns: A
Placementencoding the plane’s origin and orthonormal frame, or aConstructionResolutionError. - Example:
let plane = ConstructionPlane.absolute(origin: .zero, normal: SIMD3(0, 0, 1)) switch graph.resolve(plane) { case .success(let p): print(p.origin, p.zAxis) case .failure(let e): print(e) }
BRepGraph.resolve(_:) for ConstructionAxis
Resolves a ConstructionAxis recipe against the current graph state.
public func resolve(_ axis: ConstructionAxis) -> Result<(origin: SIMD3<Double>, direction: SIMD3<Double>), ConstructionResolutionError>
- Returns: A tuple of the axis origin and unit direction vector, or a
ConstructionResolutionError. - Example:
let axis = ConstructionAxis.absolute(origin: .zero, direction: SIMD3(0, 0, 1)) if case .success(let ax) = graph.resolve(axis) { print(ax.origin, ax.direction) }
BRepGraph.resolve(_:) for ConstructionPoint
Resolves a ConstructionPoint recipe against the current graph state.
public func resolve(_ point: ConstructionPoint) -> Result<SIMD3<Double>, ConstructionResolutionError>
- Returns: The resolved 3D coordinate, or a
ConstructionResolutionError. - Example:
let pt = ConstructionPoint.absolute(SIMD3(1, 2, 3)) if case .success(let p) = graph.resolve(pt) { print(p) }
BRepGraph.childIndices
BRepGraph.childIndices(rootKind:rootIndex:targetKind:)
Returns the indices of all descendant nodes of targetKind under a root node.
public func childIndices(rootKind: NodeKind, rootIndex: Int, targetKind: NodeKind) -> [Int]
Complements childCount(rootKind:rootIndex:targetKind:) by giving the actual index values rather than just the count. Used internally by construction-entity resolvers when enumerating sub-topology.
- Parameters:
rootKind: theNodeKindof the root node.rootIndex: the ordinal index of the root node in the graph.targetKind: theNodeKindto collect descendants of.
- Returns: An array of graph indices; empty if there are none.
- OCCT:
OCCTBRepGraphChildIndices, queries the pre-built BRep-graph adjacency tables. - Example:
let faceIndices = graph.childIndices(rootKind: .solid, rootIndex: 0, targetKind: .face)
ConstructionContext
A document-level, thread-safe registry of named construction entities. Entities are stored by value under opaque typed IDs; they are resolved on demand against a BRepGraph. Insertion order is preserved. Thread-safe via an internal NSLock.
Persistence note: Construction entity recipes live in Swift value storage only, they are not serialised into the XCAF/XDE shape tree. STEP round-trip preserves layer tags (see
ConstructionLayer) but loses recipe structure. Serialise theConstructionContextseparately (e.g. as JSON viaCodable) if recipe round-trip is required.
ConstructionContext.PlaneID
Opaque, Hashable, Sendable identifier for a registered construction plane.
public struct PlaneID: Sendable, Hashable {
public let raw: UUID
public init()
}
Each call to init() produces a unique ID backed by a new UUID.
ConstructionContext.PlaneID.raw
The underlying UUID backing this identifier.
public let raw: UUID
Exposed so callers who need a stable, storable key (e.g. for Codable persistence, or as a dictionary key outside ConstructionContext itself) can get at the identifier’s value directly, without ConstructionContext providing any further lookup machinery for it. AxisID.raw and PointID.raw carry the identical role for their respective ID types.
ConstructionContext.AxisID
Opaque, Hashable, Sendable identifier for a registered construction axis.
public struct AxisID: Sendable, Hashable {
public let raw: UUID
public init()
}
ConstructionContext.AxisID.raw
The backing UUID; equality and hashing are UUID’s own.
ConstructionContext.PointID
Opaque, Hashable, Sendable identifier for a registered construction point.
public struct PointID: Sendable, Hashable {
public let raw: UUID
public init()
}
raw: the backingUUID. Eachinit()call generates a fresh one; twoPointIDvalues are equal exactly when theirrawUUIDs match.
ConstructionContext.PointID.raw
ConstructionContext.init()
Creates an empty construction context.
public init()
- Example:
let ctx = ConstructionContext()
ConstructionContext.add(_:name:) for ConstructionPlane
Inserts a construction plane, returning its unique ID.
@discardableResult
public func add(_ plane: ConstructionPlane, name: String? = nil) -> PlaneID
- Parameters:
plane: the plane recipe to register.name: optional human-readable label (e.g."Top","XZ") for display purposes.
- Returns: A new
PlaneID; discard if you don’t need to look up the entity later. - Example:
let ctx = ConstructionContext() let xyId = ctx.add(.absolute(origin: .zero, normal: SIMD3(0, 0, 1)), name: "XY")
ConstructionContext.add(_:name:) for ConstructionAxis
Inserts a construction axis, returning its unique ID.
@discardableResult
public func add(_ axis: ConstructionAxis, name: String? = nil) -> AxisID
- Parameters:
axis: the axis recipe.name: optional label.
- Returns: A new
AxisID.
ConstructionContext.add(_:name:) for ConstructionPoint
Inserts a construction point, returning its unique ID.
@discardableResult
public func add(_ point: ConstructionPoint, name: String? = nil) -> PointID
- Parameters:
point: the point recipe.name: optional label.
- Returns: A new
PointID.
ConstructionContext.plane(_:)
Looks up a registered construction plane by ID.
public func plane(_ id: PlaneID) -> ConstructionPlane?
- Returns: The
ConstructionPlanerecipe, ornilif the ID is not registered.
ConstructionContext.axis(_:)
Looks up a registered construction axis by ID.
public func axis(_ id: AxisID) -> ConstructionAxis?
- Returns: The
ConstructionAxisrecipe, ornilif the ID is not registered.
ConstructionContext.point(_:)
Looks up a registered construction point by ID.
public func point(_ id: PointID) -> ConstructionPoint?
- Returns: The
ConstructionPointrecipe, ornilif the ID is not registered.
ConstructionContext.name(_:) for PlaneID
Returns the human-readable label for a registered plane, if any.
public func name(_ id: PlaneID) -> String?
ConstructionContext.name(_:) for AxisID
Returns the human-readable label for a registered axis, if any.
public func name(_ id: AxisID) -> String?
ConstructionContext.name(_:) for PointID
Returns the human-readable label for a registered point, if any.
public func name(_ id: PointID) -> String?
ConstructionContext.allPlanes
All registered planes in insertion order.
public var allPlanes: [(id: PlaneID, name: String?, plane: ConstructionPlane)] { get }
- Returns: Ordered array of
(id, name, plane)tuples; empty if no planes are registered.
ConstructionContext.allAxes
All registered axes in insertion order.
public var allAxes: [(id: AxisID, name: String?, axis: ConstructionAxis)] { get }
ConstructionContext.allPoints
All registered points in insertion order.
public var allPoints: [(id: PointID, name: String?, point: ConstructionPoint)] { get }
ConstructionContext.remove(plane:)
Removes the plane registered under id.
public func remove(plane id: PlaneID)
No-ops silently if id is not registered.
ConstructionContext.remove(axis:)
Removes the axis registered under id.
public func remove(axis id: AxisID)
ConstructionContext.remove(point:)
Removes the point registered under id.
public func remove(point id: PointID)
ConstructionContext.removeAll()
Removes all registered planes, axes, and points.
public func removeAll()
ConstructionContext.resolve(_:in:) for PlaneID
Resolves a registered plane against a topology graph, returning a Placement.
public func resolve(_ id: PlaneID, in graph: BRepGraph) -> Result<Placement, ConstructionResolutionError>
Delegates to BRepGraph.resolve(_:). Returns .failure(.notApplicable(...)) if id is not registered.
- Parameters:
id: thePlaneIDto resolve.graph: the topology graph to evaluate the recipe against.
- Returns:
Result<Placement, ConstructionResolutionError>. - Example:
let ctx = ConstructionContext() let id = ctx.add(.absolute(origin: .zero, normal: SIMD3(0, 0, 1))) if case .success(let p) = ctx.resolve(id, in: graph) { print(p.origin) }
ConstructionContext.resolve(_:in:) for AxisID
Resolves a registered axis against a topology graph.
public func resolve(_ id: AxisID, in graph: BRepGraph) -> Result<(origin: SIMD3<Double>, direction: SIMD3<Double>), ConstructionResolutionError>
ConstructionContext.resolve(_:in:) for PointID
Resolves a registered point against a topology graph.
public func resolve(_ id: PointID, in graph: BRepGraph) -> Result<SIMD3<Double>, ConstructionResolutionError>
ConstructionContext.BrokenEntities
Container for entities that fail resolution in allBroken(in:).
public struct BrokenEntities: Sendable {
public let planes: [(id: PlaneID, error: ConstructionResolutionError)]
public let axes: [(id: AxisID, error: ConstructionResolutionError)]
public let points: [(id: PointID, error: ConstructionResolutionError)]
public var isEmpty: Bool { get }
public var totalCount: Int { get }
}
isEmpty:truewhen all three lists are empty (no broken entities).totalCount: total count of broken entities across all three types.
BrokenEntities.planes
The registered planes that failed to resolve against the graph passed to allBroken(in:).
public let planes: [(id: PlaneID, error: ConstructionResolutionError)]
Each entry pairs the failing plane’s PlaneID with the ConstructionResolutionError BRepGraph.resolve(_:) returned for it. Empty if every registered plane resolved successfully.
BrokenEntities.axes
The registered axes that failed to resolve against the graph passed to allBroken(in:).
public let axes: [(id: AxisID, error: ConstructionResolutionError)]
Each entry pairs the failing axis’s AxisID with the ConstructionResolutionError BRepGraph.resolve(_:) returned for it. Empty if every registered axis resolved successfully.
ConstructionContext.allBroken(in:)
Inspects every registered entity against graph and returns those that fail resolution.
public func allBroken(in graph: BRepGraph) -> BrokenEntities
Useful in agent workflows after model edits to detect stale construction references before attempting a sketch build or section.
- Parameters:
graph, the topology graph to evaluate against. - Returns: A
BrokenEntitiesvalue listing planes, axes, and points that returned.failure. - Example:
let broken = ctx.allBroken(in: graph) if !broken.isEmpty { print("\(broken.totalCount) broken references") }
ConstructionContext.count
Counts of registered planes, axes, and points.
public var count: (planes: Int, axes: Int, points: Int) { get }
- Example:
let (p, a, pt) = ctx.count print("planes: \(p), axes: \(a), points: \(pt)")
Document.constructionContext
Document.constructionContext
Per-document construction context, created lazily on first access.
public var constructionContext: ConstructionContext { get }
Construction entities live alongside the document’s shapes but are not part of the XDE shape tree. Each Document instance gets exactly one ConstructionContext; repeated access returns the same object.
- Lifetime: the context is tied to the
Documentinstance and is released with it. A newly createdDocumentalways starts with an empty context, and its entities are never visible to any other document. (Before v1.9.1 this did not hold, see #277.) - Example:
let doc = Document.create()! let ctx = doc.constructionContext let xyId = ctx.add(.absolute(origin: .zero, normal: SIMD3(0, 0, 1)), name: "XY")
ConstructionLayer. Document extension
Declared in ConstructionLayer.swift. Provides XCAF layer tagging for construction shapes so that layer membership survives STEP/IGES round-trip.
Document.constructionLayerName
The XCAF layer name used to tag construction geometry.
public static let constructionLayerName = "CONSTRUCTION"
Matches the layer string used by FreeCAD and the AP214 convention for construction geometry.
Document.addConstructionShape(_:)
Adds a shape to the document and immediately tags it with the CONSTRUCTION XCAF layer.
@discardableResult
public func addConstructionShape(_ shape: Shape) -> Int64
- Parameters:
shape, the shape to add (typically a face, edge, or vertex materialised from a recipe). - Returns: The new label ID (≥ 0 on success, negative on failure).
- OCCT:
XCAFDoc_LayerTool::SetLayerviaAssemblyNode.setLayer(_:). - Example:
let vertex = Shape.vertex(at: SIMD3(0, 0, 0))! let labelId = doc.addConstructionShape(vertex)
Document.constructionShapeLabels
The label IDs of all shapes currently tagged with the CONSTRUCTION layer in this document.
public var constructionShapeLabels: [Int64] { get }
Use this after a STEP/IGES load to identify shapes that were tagged as construction geometry on export.
- Returns: Array of label IDs; empty if no construction-tagged shapes exist.
- OCCT: Filters
rootNodesviaAssemblyNode.isLayerSet("CONSTRUCTION"). - Example:
let ids = doc.constructionShapeLabels print("\(ids.count) construction shapes in document")
ConstructionContext.materialize
ConstructionContext.MaterializeOptions
Size parameters for the finite representative shapes produced by materialize(in:graph:options:).
public struct MaterializeOptions: Sendable {
public var planeHalfSize: Double = 100
public var axisHalfLength: Double = 100
public init(planeHalfSize: Double = 100, axisHalfLength: Double = 100)
}
planeHalfSize: half-side of the square face representing each plane (default 100 mm).axisHalfLength: half-length of the edge representing each axis (default 100 mm).
ConstructionContext.MaterializeOptions.axisHalfLength
Half-length of the edge representing each axis.
ConstructionContext.MaterializationResult
Summary returned by materialize(in:graph:options:).
public struct MaterializationResult: Sendable {
public let planeShapes: [(id: PlaneID, labelId: Int64)]
public let axisShapes: [(id: AxisID, labelId: Int64)]
public let pointShapes: [(id: PointID, labelId: Int64)]
public let failures: [MaterializationFailure]
public var totalMaterialized: Int { get }
}
totalMaterialized: combined count of successfully materialised planes, axes, and points.
| Field | Meaning |
|---|---|
planeShapes | (PlaneID, labelId) pairs for each plane successfully materialised. |
axisShapes | (AxisID, labelId) pairs for each axis successfully materialised. |
pointShapes | (PointID, labelId) pairs for each point successfully materialised. |
failures | One MaterializationFailure per entity that failed to resolve or could not become a shape. |
totalMaterialized | planeShapes.count + axisShapes.count + pointShapes.count. |
ConstructionContext.MaterializationResult.totalMaterialized
Combined count of successfully materialised planes, axes, and points.
ConstructionContext.MaterializationFailure
Discriminated union of failure cases from materialize(in:graph:options:).
public enum MaterializationFailure: Sendable {
case planeResolveFailed(PlaneID, ConstructionResolutionError)
case axisResolveFailed(AxisID, ConstructionResolutionError)
case pointResolveFailed(PointID, ConstructionResolutionError)
case planeShapeFailed(PlaneID)
case axisShapeFailed(AxisID)
case pointShapeFailed(PointID)
case planeAddFailed(PlaneID)
case axisAddFailed(AxisID)
case pointAddFailed(PointID)
}
*ResolveFailed cases indicate that the recipe could not be evaluated against the graph; *ShapeFailed cases indicate that the recipe resolved but the representative shape could not be constructed (e.g. degenerate wire). *AddFailed cases are distinct from those: the shape was built, and Document.addConstructionShape then refused to add it, returning a negative label ID.
MaterializationFailure.planeResolveFailed
The plane’s BRepGraph.resolve(_:) call returned .failure; carries the failing PlaneID and the ConstructionResolutionError the graph reported.
MaterializationFailure.axisResolveFailed
The axis’s BRepGraph.resolve(_:) call returned .failure; carries the failing AxisID and the underlying ConstructionResolutionError.
MaterializationFailure.pointResolveFailed
The point’s BRepGraph.resolve(_:) call returned .failure; carries the failing PointID and the underlying ConstructionResolutionError.
MaterializationFailure.planeShapeFailed
The plane recipe resolved to a Placement, but building its representative rectangular face (via the private planeShape(placement:halfSize:) helper) failed; carries the PlaneID.
MaterializationFailure.axisShapeFailed
The axis recipe resolved to an origin and direction, but building its representative edge (via the private axisShape(origin:direction:halfLength:) helper) failed; carries the AxisID.
MaterializationFailure.pointShapeFailed
The point recipe resolved to a coordinate, but building its representative vertex (via the private pointShape(at:) helper, i.e. Shape.vertex(at:)) failed; carries the PointID.
*ResolveFailed cases indicate that the recipe could not be evaluated against the graph; *ShapeFailed cases indicate that the recipe resolved but the representative shape could not be constructed (e.g. degenerate wire).
ConstructionContext.materialize(in:graph:options:)
Materialises all registered construction entities as TopoDS_Shapes on the document’s CONSTRUCTION layer.
@discardableResult
public func materialize(in document: Document,
graph: BRepGraph,
options: MaterializeOptions = MaterializeOptions()) -> MaterializationResult
Each resolved entity becomes a finite representative shape:
- Planes → a square face (side
2 × planeHalfSize) centred on the plane origin. - Axes → an edge of length
2 × axisHalfLengthcentred on the axis origin. - Points → a vertex.
Shapes are added to document via addConstructionShape(_:), which tags them with the CONSTRUCTION XCAF layer.
- Parameters:
document: the document to add shapes to.graph: the topology graph for resolving entity recipes.options: size parameters for the representative shapes.
- Returns: A
MaterializationResultdescribing what succeeded and what failed. - Example:
let result = ctx.materialize(in: doc, graph: graph) print("\(result.totalMaterialized) shapes materialised") for failure in result.failures { print("Failed: \(failure)") }
SketchElement
A single 2D curve element within a Sketch. Elements carry their curve geometry, a construction flag, and a stable UUID.
SketchElement.CurveKind
Discriminated union of supported 2D curve types within a sketch element.
public enum CurveKind: Sendable, Hashable {
case line(from: SIMD2<Double>, to: SIMD2<Double>)
case arc(center: SIMD2<Double>, radius: Double, startAngle: Double, endAngle: Double)
case circle(center: SIMD2<Double>, radius: Double)
case polyline([SIMD2<Double>])
}
Angles for .arc are in radians.
| Case | Meaning |
|---|---|
line(from:to:) | A straight segment between two 2D points. |
arc(center:radius:startAngle:endAngle:) | A circular arc (radians). |
circle(center:radius:) | A full circle. |
polyline([SIMD2<Double>]) | An ordered chain of 2D points, taken as-is (not tessellated). |
(Per-case anchor below, for cross-reference; the table above has the actual meaning of each.)
SketchElement.CurveKind.polyline
SketchElement.CurveKind.tessellate2D(segmentsPerRadian:)
Returns ordered 2D sample points along this curve.
public func tessellate2D(segmentsPerRadian: Int = 16) -> [SIMD2<Double>]
Lines and polylines return their defining points exactly. Arcs and circles are tessellated at the given density.
- Parameters:
segmentsPerRadian, number of line segments per radian of arc; default 16. - Returns: Array of 2D points in order along the curve. Circles include a repeated closing point.
- Example:
let arc = SketchElement.CurveKind.arc(center: .zero, radius: 5, startAngle: 0, endAngle: .pi) let pts = arc.tessellate2D(segmentsPerRadian: 32)
SketchElement.curve
The geometry of this element.
public var curve: CurveKind
SketchElement.isConstruction
Whether this element is construction geometry, excluded from Sketch.buildProfile.
public var isConstruction: Bool
Construction elements are visible in the sketch editor but do not appear in the extruded/revolved profile.
SketchElement.id
Stable identity for the element, used for selection and constraint references.
public var id: UUID
SketchElement.init(curve:isConstruction:id:)
Creates a sketch element.
public init(curve: CurveKind, isConstruction: Bool = false, id: UUID = UUID())
- Parameters:
curve: the 2D curve geometry.isConstruction:trueto mark as construction (defaultfalse).id: stable UUID (default-generated if not provided).
- Example:
let line = SketchElement(curve: .line(from: .zero, to: SIMD2(10, 0))) let guide = SketchElement(curve: .line(from: SIMD2(-5, 0), to: SIMD2(15, 0)), isConstruction: true)
Sketch
A collection of 2D curve elements hosted on a ConstructionPlane, with a buildProfile step that filters construction elements and lifts the result to a 3D Wire. Constraint solving is out of scope, elements carry coordinates directly.
Sketch.hostPlane
The ID of the construction plane on which this sketch lives.
public var hostPlane: ConstructionContext.PlaneID
Sketch.elements
All elements in the sketch, including construction geometry.
public var elements: [SketchElement]
Sketch.name
Optional display name for the sketch.
public var name: String?
Sketch.init(hostPlane:elements:name:)
Creates a sketch on the given construction plane.
public init(hostPlane: ConstructionContext.PlaneID,
elements: [SketchElement] = [],
name: String? = nil)
- Parameters:
hostPlane: thePlaneIDregistered in aConstructionContext.elements: initial element set (default empty).name: optional display name.
- Example:
let ctx = ConstructionContext() let planeId = ctx.add(.absolute(origin: .zero, normal: SIMD3(0, 0, 1))) var sketch = Sketch(hostPlane: planeId, name: "Profile")
Sketch.add(_:)
Appends an element to the sketch.
public mutating func add(_ element: SketchElement)
- Parameters:
element, the element to append. - Example:
sketch.add(SketchElement(curve: .circle(center: .zero, radius: 5)))
Sketch.profileElementCount
Number of non-construction elements, the profile size.
public var profileElementCount: Int { get }
- Returns: Count of elements where
isConstruction == false. - Example:
#expect(sketch.profileElementCount == 1)
Sketch.buildProfile(in:graph:)
Builds a 3D closed profile wire from the sketch’s non-construction elements, placed on the host construction plane.
public func buildProfile(in context: ConstructionContext,
graph: BRepGraph) -> Wire?
Construction elements are filtered at this single site, upstream views (solver, editor) see the full element set. Each 2D point is lifted into 3D via placement.origin + pt.x * placement.xAxis + pt.y * placement.yAxis. The resulting polyline is closed automatically if the first and last 3D points are within 1e-9 of each other.
- Parameters:
context: theConstructionContextthat registeredhostPlane.graph: aBRepGraphto resolve the host plane’s recipe.
- Returns: A closed
Wireon the resolved plane, ornilif the host plane fails to resolve, no profile elements exist, or fewer than 2 distinct 3D points result. - OCCT: Delegates to
Wire.polygon3D(_:closed:),BRepBuilderAPI_MakePolygon. - Example:
let ctx = ConstructionContext() let planeId = ctx.add(.absolute(origin: .zero, normal: SIMD3(0, 0, 1))) var sketch = Sketch(hostPlane: planeId) sketch.add(SketchElement(curve: .circle(center: .zero, radius: 10))) if let wire = sketch.buildProfile(in: ctx, graph: graph) { let solid = Shape.extrude(wire: wire, direction: SIMD3(0, 0, 5)) }
(Internal, not public API: buildProfile(in:graph:) above is implemented with a private func helper on Sketch. approxEqual(_:_:tolerance:) compares two lifted 3D points by squared distance to decide whether the resulting polyline’s first and last points coincide closely enough to close the wire. The 2D-to-3D lifting uses the public Placement.lift(_:) method.)
Shape.section2D
Shape.section2D(planeOrigin:planeNormal:planeU:deflection:)
Slices this shape with a plane and returns the resulting contour as a 2D Drawing in the plane’s own coordinate frame.
public func section2D(planeOrigin: SIMD3<Double>,
planeNormal: SIMD3<Double>,
planeU: SIMD3<Double>? = nil,
deflection: Double = 0.1) -> Drawing?
Computes the 3D section edges via sectionWithPlane, then projects each sample point into the plane’s (u, v) frame. The result is a Drawing whose visibleEdges contain the section contour polylines, ready for annotation, hatching, and export.
- Parameters:
planeOrigin: any point on the cutting plane, in world coordinates.planeNormal: plane normal; will be normalised internally.planeU: explicit X axis for the resulting 2D frame; must be perpendicular toplaneNormal. Whennil(default), a deterministic perpendicular is derived from world-up or world-Y.deflection: tessellation tolerance for edge sampling (default 0.1 mm; use 0.01 for finer detail).
- Returns: A
Drawingwith the 2D contour invisibleEdges, ornilif the plane does not intersect the shape or projection fails. - OCCT:
OCCTShapeSectionWithPlane→BRepAlgoAPI_Section; thenDrawing.projectfor the 2D assembly. - Example:
let box = Shape.box(width: 50, height: 50, depth: 50)! if let drawing = box.section2D(planeOrigin: SIMD3(0, 0, 25), planeNormal: SIMD3(0, 0, 1)) { // drawing.visibleEdges contains the 50×50 square contour at Z=25 try Exporter.writeDXF(drawing: drawing, to: dxfURL) }
Shape.SectionView
Shape.SectionView
A section view spec bundling contour, hatching, and label for placement on a drawing sheet.
public struct SectionView: Sendable {
public let drawing: Drawing
public let label: String?
public let cuttingPlaneOrigin: SIMD3<Double>
public let cuttingPlaneNormal: SIMD3<Double>
}
drawing: theDrawingcontaining the contour and any added hatching and label.label: optional string label (e.g."A-A"), added as a text annotation above the drawing bounds.cuttingPlaneOrigin/cuttingPlaneNormal, the cutting plane that produced this view.
SectionView.drawing
The Drawing containing the contour and any added hatching and label.
public let drawing: Drawing
SectionView.label
Optional string label (e.g. "A-A"), added as a text annotation above the drawing bounds.
public let label: String?
SectionView.cuttingPlaneOrigin
The origin of the cutting plane that produced this view, in world coordinates.
public let cuttingPlaneOrigin: SIMD3<Double>
SectionView.cuttingPlaneNormal
The normal of the cutting plane that produced this view.
public let cuttingPlaneNormal: SIMD3<Double>
Shape.section2DView(planeOrigin:planeNormal:label:hatchAngle:hatchSpacing:deflection:)
ISO 128-40-styled section view: slice + hatching + label bundled into a single Drawing.
public func section2DView(planeOrigin: SIMD3<Double>,
planeNormal: SIMD3<Double>,
label: String? = nil,
hatchAngle: Double = .pi / 4,
hatchSpacing: Double = 3.0,
deflection: Double = 0.1) -> SectionView?
Calls section2D then adds cross-hatch lines (at angle/spacing) over the bounding box of the section contour, and optionally places a text label 5 mm above the top-left corner.
- Parameters:
planeOrigin: any point on the cutting plane, in world coordinates.planeNormal: plane normal; will be normalised.label: optional annotation string (defaultnil); placed above the contour bounds.hatchAngle: hatch line angle in radians (default π/4 = 45°).hatchSpacing: spacing between hatch lines in model units (default 3 mm).deflection: tessellation tolerance for edge sampling (default 0.1 mm).
- Returns: A
SectionView, ornilifsection2Dfails (no intersection or projection error). - Note: Hatching uses the bounding box of the contour as the fill boundary; full contour-interior polygon hatching is planned for a future release.
- Example:
let shaft = Shape.cylinder(radius: 10, height: 100)! if let view = shaft.section2DView(planeOrigin: SIMD3(0, 0, 50), planeNormal: SIMD3(0, 0, 1), label: "A-A", hatchSpacing: 2.0) { print(view.label ?? "") // "A-A" // view.drawing is ready to place on a sheet }