Mesh
A Mesh is a triangulated surface representation — vertices, per-vertex normals, and triangle indices — produced by tessellating a Shape or built directly from raw arrays. It is the bridge between OCCT’s exact B-Rep geometry and renderers (SceneKit, RealityKit, Metal) and mesh file formats (STL, OBJ, PLY). Obtain one by calling Shape.mesh(linearDeflection:) or Shape.mesh(parameters:), or construct one from your own vertices/indices arrays with Mesh.init(vertices:normals:indices:).
Topics
- Initializers · Mesh Data · Statistics · Triangle Access with Face Info · Mesh to Shape Conversion · Mesh Boolean Operations · SceneKit Integration · Metal Integration · RealityKit Integration · Supporting Types
Initializers
Mesh.init(vertices:normals:indices:)
Constructs a Mesh directly from raw vertex-position and triangle-index arrays.
public convenience init?(
vertices: [SIMD3<Float>],
normals: [SIMD3<Float>]? = nil,
indices: [UInt32]
)
Use this when you have vertex/index data from an external algorithm (decimation, smoothing, repair, remeshing, OBJ/STL/glTF import) and no B-Rep source. For meshes derived from OCCT shapes, use Shape.mesh() instead. When normals is nil, per-vertex normals are computed by averaging the face normals of adjacent triangles (smooth shading).
- Parameters:
vertices— per-vertex positions; must not be empty.normals— optional per-vertex normals; if provided must have the same count asvertices. Passnilto auto-compute smooth normals from triangle adjacency.indices— triangle index triples; length must be a multiple of 3, every value< vertices.count.
- Returns:
nilif any input is empty,indices.count % 3 != 0, a normal-count mismatch exists, or any index is out of bounds. - OCCT: Pure-Swift validation; data is stored in the C
OCCTMeshstruct viaOCCTMeshCreateFromArrays. - Example:
// Octahedron — normals auto-computed let v: [SIMD3<Float>] = [ SIMD3(0, 0, 1), SIMD3(1, 0, 0), SIMD3(0, 1, 0), SIMD3(-1, 0, 0), SIMD3(0, -1, 0), SIMD3(0, 0, -1), ] let idx: [UInt32] = [0,1,2, 0,2,3, 0,3,4, 0,4,1, 5,2,1, 5,3,2, 5,4,3, 5,1,4] guard let mesh = Mesh(vertices: v, indices: idx) else { return }
Mesh Data
vertexCount
The number of vertices in the mesh.
public var vertexCount: Int { get }
- Returns: Count of distinct vertex positions.
- OCCT:
OCCTMeshGetVertexCount— returnsvertices.size() / 3from the internal float buffer. - Example:
let mesh = Shape.box(width: 10, height: 5, depth: 3)!.mesh(linearDeflection: 0.1)! print(mesh.vertexCount)
triangleCount
The number of triangles in the mesh.
public var triangleCount: Int { get }
- Returns: Count of triangles; equals
indices.count / 3. - OCCT:
OCCTMeshGetTriangleCount— returnsindices.size() / 3from the internal index buffer. - Example:
print(mesh.triangleCount) // indices.count == mesh.triangleCount * 3
vertices
Vertex positions as an array of SIMD3<Float>.
public var vertices: [SIMD3<Float>] { get }
Each element is the 3D position of one vertex. Array length equals vertexCount.
- Returns: Position array;
[]if the mesh is empty. - OCCT:
OCCTMeshGetVertices— copies the internal contiguous float buffer, then repackages intoSIMD3. - Example:
for p in mesh.vertices { print(p.x, p.y, p.z) }
normals
Per-vertex normals as an array of SIMD3<Float>.
public var normals: [SIMD3<Float>] { get }
Each normal is a unit vector perpendicular to the surface at that vertex. Array length equals vertexCount. Normals are set during tessellation (BRepMesh_IncrementalMesh computes them from face curvature) or, for array-constructed meshes, by averaging adjacent face normals.
- Returns: Normal array;
[]if the mesh is empty. - OCCT:
OCCTMeshGetNormals— copies the internal normal float buffer. - Example:
for n in mesh.normals { // n is approximately unit-length }
indices
Triangle indices as an array of UInt32.
public var indices: [UInt32] { get }
Every three consecutive values define one triangle referencing vertices at those positions in vertices. Array length equals triangleCount * 3.
- Returns: Index array;
[]if the mesh has no triangles. - OCCT:
OCCTMeshGetIndices— copies the internal index buffer. - Example:
let idx = mesh.indices // Triangle 0: idx[0], idx[1], idx[2] // Triangle 1: idx[3], idx[4], idx[5]
vertexData
Raw vertex positions as a contiguous [Float] interleaved array.
public var vertexData: [Float] { get }
Format: [x0, y0, z0, x1, y1, z1, …]. Length is vertexCount * 3. Ready for direct upload to a GPU buffer without repackaging.
- Returns: Flat float array;
[]if the mesh is empty. - OCCT:
OCCTMeshGetVertices— same buffer copy asvertices, withoutSIMD3wrapping. - Example:
let (positions, normals, indices) = mesh.metalBufferData() // positions is equivalent to Data(mesh.vertexData.withUnsafeBytes { … })
normalData
Raw per-vertex normals as a contiguous [Float] interleaved array.
public var normalData: [Float] { get }
Format: [nx0, ny0, nz0, nx1, ny1, nz1, …]. Length is vertexCount * 3. Pair with vertexData and indices for zero-copy GPU uploads.
- Returns: Flat float array;
[]if the mesh is empty. - OCCT:
OCCTMeshGetNormals— same buffer copy asnormals, withoutSIMD3wrapping. - Example:
let posData = Data(bytes: mesh.vertexData, count: mesh.vertexData.count * 4) let nrmData = Data(bytes: mesh.normalData, count: mesh.normalData.count * 4)
Statistics
boundingBox
Axis-aligned bounding box of the mesh.
public var boundingBox: (min: SIMD3<Float>, max: SIMD3<Float>) { get }
Computed by iterating all vertex positions using Swift min/max component-wise reduction.
- Returns:
(min, max)corner tuple;(.zero, .zero)if the mesh is empty. - OCCT: Pure-Swift — iterates
vertices. - Example:
let (lo, hi) = mesh.boundingBox print("X span:", hi.x - lo.x)
size
Extent of the mesh in each dimension.
public var size: SIMD3<Float> { get }
Computed as boundingBox.max − boundingBox.min.
- Returns: Width, height, depth of the AABB.
- OCCT: Pure-Swift — delegates to
boundingBox. - Example:
let s = mesh.size // SIMD3(width, height, depth)
center
Centre point of the mesh bounding box.
public var center: SIMD3<Float> { get }
Computed as (boundingBox.min + boundingBox.max) / 2.
- Returns: Centroid of the AABB.
- OCCT: Pure-Swift — delegates to
boundingBox. - Example:
let c = mesh.center
Triangle Access with Face Info
trianglesWithFaces()
Returns all triangles with their source B-Rep face index and per-triangle normal.
public func trianglesWithFaces() -> [Triangle]
Provides richer data than indices alone: each Triangle carries v1/v2/v3 vertex indices, a faceIndex identifying which B-Rep face this triangle was tessellated from (−1 if unknown, e.g. from Mesh.init(vertices:normals:indices:)), and a normal vector. Use for B-Rep-face-aware picking, CAM operations that need to reason per face, or custom per-triangle shading.
- Returns: Array of
Trianglestructs;[]if the mesh has no triangles. - OCCT:
OCCTMeshGetTrianglesWithFaces— reads the internalfaceIndicesandtriangleNormalsarrays alongside the index buffer. - Example:
for tri in mesh.trianglesWithFaces() { // tri.v1 / tri.v2 / tri.v3 : UInt32 vertex indices // tri.faceIndex : Int32 (source B-Rep face, or -1) // tri.normal : SIMD3<Float> }
Mesh to Shape Conversion
toShape(weldTolerance:)
Converts this mesh back to a B-Rep Shape by sewing triangle faces into a shell.
public func toShape(weldTolerance: Double = 1e-6) -> Shape?
Each triangle becomes a planar B-Rep face; BRepBuilderAPI_Sewing merges shared edges within weldTolerance to produce a connected shell. The resulting shape is a shell (or compound of planar faces), not necessarily a valid manifold solid — run shape healing if you need one. The weld tolerance must scale with the mesh’s coordinate magnitude: a value too small for a large-coordinate mesh leaves edges unmerged and yields an open shell.
- Parameters:
weldTolerance— vertex-merge tolerance in model units (default1e-6; raise for large-coordinate meshes). Must be positive. - Returns: A
Shaperepresenting the tessellated geometry, ornilif the mesh is empty, weldTolerance is ≤ 0, or sewing fails. - OCCT:
OCCTMeshToShapeWithTolerance→BRepBuilderAPI_MakeEdge+BRepBuilderAPI_MakeFaceper triangle +BRepBuilderAPI_Sewing::Perform. - Example:
let shape = mesh.toShape(weldTolerance: 1e-6) // raise for large-coordinate meshes - Note: For an STL file on disk, prefer
Shape.loadSTLRobust(from:sewingTolerance:)which sews and heals as it loads. The result oftoShapeis a faceted shell — run healing if you need a valid solid.
Mesh Boolean Operations
union(with:deflection:)
Performs boolean union with another mesh via a B-Rep roundtrip.
public func union(with other: Mesh, deflection: Double = 0.1) -> Mesh?
Both meshes are lifted to B-Rep shells (BRepBuilderAPI_Sewing), the union is computed (BRepAlgoAPI_Fuse), and the result is re-tessellated at deflection. Because the operation works on tessellations — not exact B-Rep — prefer the B-Rep boolean Shape.union(_:) when exact geometry matters.
- Parameters:
other— mesh to add;deflection— linear deflection for re-tessellating the result (default0.1). - Returns: Union mesh, or
nilif conversion or the boolean operation fails. - OCCT:
OCCTMeshUnion→BRepBuilderAPI_Sewing+BRepAlgoAPI_Fuse+BRepMesh_IncrementalMesh. - Example:
guard let a = Shape.box(width: 12, height: 12, depth: 12)?.mesh(linearDeflection: 0.3), let b = Shape.cylinder(at: SIMD3(6, 6, -1), direction: SIMD3(0, 0, 1), radius: 3, height: 14)?.mesh(linearDeflection: 0.3) else { return } let joined = a.union(with: b, deflection: 0.3) - Note: Mesh booleans are convenient for triangle pipelines but operate on approximations. For exact, valid solids prefer the B-Rep booleans and mesh the result at the end.
subtracting(_:deflection:)
Subtracts another mesh from this mesh via a B-Rep roundtrip.
public func subtracting(_ other: Mesh, deflection: Double = 0.1) -> Mesh?
Both meshes are lifted to B-Rep shells, the subtraction is computed (BRepAlgoAPI_Cut), and the result is re-tessellated at deflection.
- Parameters:
other— mesh to subtract;deflection— linear deflection for re-tessellating the result. - Returns: Difference mesh, or
nilon failure. - OCCT:
OCCTMeshSubtract→BRepBuilderAPI_Sewing+BRepAlgoAPI_Cut+BRepMesh_IncrementalMesh. - Example:
let cut = a.subtracting(b, deflection: 0.3) // box with a drilled hole
intersection(with:deflection:)
Intersects this mesh with another mesh via a B-Rep roundtrip.
public func intersection(with other: Mesh, deflection: Double = 0.1) -> Mesh?
Both meshes are lifted to B-Rep shells, the intersection is computed (BRepAlgoAPI_Common), and the result is re-tessellated at deflection.
- Parameters:
other— mesh to intersect with;deflection— linear deflection for re-tessellating the result. - Returns: Intersection mesh, or
nilon failure. - OCCT:
OCCTMeshIntersect→BRepBuilderAPI_Sewing+BRepAlgoAPI_Common+BRepMesh_IncrementalMesh. - Example:
let common = a.intersection(with: b, deflection: 0.3)
SceneKit Integration
Available on macOS / iOS where SceneKit is importable (#if canImport(SceneKit)). These members are pure-Swift and do not call the OCCT bridge.
sceneKitGeometry()
Creates an SCNGeometry from this mesh.
public func sceneKitGeometry() -> SCNGeometry
Packages vertexData and normalData into SCNGeometrySource objects (float, 3 components, stride 12 bytes) and indices into an SCNGeometryElement with .triangles primitive type. Apply materials separately before adding to a scene.
- Returns:
SCNGeometrywith vertex and normal sources; returns an emptySCNGeometryif the mesh has no vertices. - OCCT: Pure-Swift — reads
vertexData,normalData,indices. - Example:
let geometry = mesh.sceneKitGeometry() let material = SCNMaterial() material.diffuse.contents = UIColor.gray material.metalness.contents = 0.8 geometry.materials = [material] let node = SCNNode(geometry: geometry) scene.rootNode.addChildNode(node)
sceneKitNode(material:)
Creates an SCNNode with this mesh and an optional material applied.
public func sceneKitNode(material: SCNMaterial? = nil) -> SCNNode
Convenience wrapper: calls sceneKitGeometry(), optionally assigns material, then wraps in an SCNNode.
- Parameters:
material— optional material;nilleaves the geometry with no applied material. - Returns:
SCNNodeready to add to a scene. - OCCT: Pure-Swift — delegates to
sceneKitGeometry(). - Example:
let node = mesh.sceneKitNode(material: myMaterial) scene.rootNode.addChildNode(node)
Metal Integration
metalBufferData()
Returns vertex positions, normals, and indices as raw Data objects ready for Metal buffers.
public func metalBufferData() -> (positions: Data, normals: Data, indices: Data)
All three Data objects are interleaved-float (Float, 4 bytes each component). Use with MTLDevice.makeBuffer(bytes:length:options:). Stride for positions and normals is 12 bytes (3 × Float); index buffer uses UInt32 (4 bytes each).
- Returns: Named tuple of
(positions: Data, normals: Data, indices: Data). - OCCT: Pure-Swift — repackages
vertexData,normalData,indicesintoData. - Example:
let (positions, normals, indices) = mesh.metalBufferData() let vertexBuffer = device.makeBuffer( bytes: (positions as NSData).bytes, length: positions.count, options: .storageModeShared )
RealityKit Integration
Available on macOS 15+ / iOS 18+ where RealityKit is importable (#if canImport(RealityKit)). All three methods are @MainActor-isolated (call from the main actor inside an if #available guard).
realityKitMeshResource()
Creates a MeshResource from this mesh.
@available(iOS 18.0, macOS 15.0, *)
@MainActor
public func realityKitMeshResource() throws -> MeshResource
Builds a MeshDescriptor from vertices (positions), normals, and indices, then calls MeshResource.generate(from:). Returns an empty MeshResource if the mesh has no vertices.
- Returns: A
MeshResourcesuitable for a RealityKitModelEntity. - Throws: RealityKit errors from
MeshResource.generate(from:)if the descriptor is invalid. - OCCT: Pure-Swift — reads
vertices,normals,indices. - Note:
@MainActor-isolated and gated to macOS 15 / iOS 18; call from the main actor insideif #available(macOS 15, iOS 18, *). - Example:
if #available(macOS 15, iOS 18, *) { let meshResource = try await MainActor.run { try mesh.realityKitMeshResource() } let material = SimpleMaterial(color: .gray, isMetallic: true) let entity = ModelEntity(mesh: meshResource, materials: [material]) }
realityKitModelEntity(material:)
Creates a ModelEntity from this mesh with a specified material.
@available(iOS 18.0, macOS 15.0, *)
@MainActor
public func realityKitModelEntity(material: RealityKit.Material) throws -> ModelEntity
Calls realityKitMeshResource() then constructs ModelEntity(mesh:materials:).
- Parameters:
material— anyRealityKit.Materialto apply. - Returns:
ModelEntityready to add to a RealityKit scene. - Throws: Propagates errors from
realityKitMeshResource(). - OCCT: Pure-Swift — delegates to
realityKitMeshResource(). - Note:
@MainActor-isolated, macOS 15 / iOS 18 only. - Example:
if #available(macOS 15, iOS 18, *) { let entity = try await MainActor.run { try mesh.realityKitModelEntity( material: SimpleMaterial(color: .blue, isMetallic: true) ) } content.add(entity) }
realityKitModelEntity()
Creates a ModelEntity from this mesh with a default gray metallic material.
@available(iOS 18.0, macOS 15.0, *)
@MainActor
public func realityKitModelEntity() throws -> ModelEntity
Convenience overload: applies SimpleMaterial(color: .gray, isMetallic: true) and delegates to realityKitModelEntity(material:).
- Returns:
ModelEntitywith a gray metallic appearance. - Throws: Propagates errors from
realityKitMeshResource(). - OCCT: Pure-Swift — delegates to
realityKitModelEntity(material:). - Note:
@MainActor-isolated, macOS 15 / iOS 18 only. - Example:
if #available(macOS 15, iOS 18, *) { let entity = try await MainActor.run { try mesh.realityKitModelEntity() } }
Supporting Types
MeshParameters
Fine-grained tessellation parameters for Shape.mesh(parameters:).
public struct MeshParameters: Sendable {
public var deflection: Double
public var angle: Double
public var deflectionInterior: Double
public var angleInterior: Double
public var minSize: Double
public var relative: Bool
public var inParallel: Bool
public var internalVertices: Bool
public var controlSurfaceDeflection: Bool
public var adjustMinSize: Bool
public var allowQualityDecrease: Bool
public static var `default`: MeshParameters { get }
}
All fields correspond directly to IMeshTools_Parameters in OCCT’s BRepMesh_IncrementalMesh API. Start from MeshParameters.default (suitable for interactive display) and adjust as needed.
| Field | Default | Meaning |
|---|---|---|
deflection | 0.1 | Max chord deviation on boundary edges (model units) |
angle | 0.5 rad | Max angle between adjacent facets on boundary edges |
deflectionInterior | 0 | Interior face deflection (0 = same as deflection) |
angleInterior | 0 | Interior face angle (0 = same as angle) |
minSize | 0 | Minimum element size (0 = no minimum) |
relative | false | If true, deflection is a proportion of edge length |
inParallel | true | Multi-threaded meshing |
internalVertices | true | Generate vertices inside faces, not just on edges |
controlSurfaceDeflection | true | Validate surface approximation quality |
adjustMinSize | false | Auto-adjust minSize from edge size |
allowQualityDecrease | false | Allow replacing an existing finer triangulation with a coarser one |
- OCCT:
IMeshTools_Parameters+BRepMesh_IncrementalMesh. - Example:
var params = MeshParameters.default params.deflection = 0.05 // fine mesh params.inParallel = true let mesh = shape.mesh(parameters: params) - Note:
allowQualityDecrease(added in issue #211): when re-meshing an already-tessellated shape at a different deflection, OCCT keeps the existing mesh if it’s “good enough” unless this istrue. Set it when the new deflection must actually take effect.
MeshParameters.allowQualityDecrease
Allow replacing an existing finer triangulation with a coarser one.
- OCCT:
IMeshTools_Parameters+BRepMesh_IncrementalMesh. - Example:
var params = MeshParameters.default params.deflection = 0.05 // fine mesh params.inParallel = true let mesh = shape.mesh(parameters: params) - Note:
allowQualityDecrease(added in issue #211): when re-meshing an already-tessellated shape at a different deflection, OCCT keeps the existing mesh if it’s “good enough” unless this istrue. Set it when the new deflection must actually take effect.
Triangle
A mesh triangle with B-Rep face association and per-triangle normal.
public struct Triangle: Sendable {
public let v1: UInt32
public let v2: UInt32
public let v3: UInt32
public let faceIndex: Int32
public let normal: SIMD3<Float>
}
Returned by Mesh.trianglesWithFaces(). The faceIndex is the 0-based index of the B-Rep face this triangle was tessellated from (−1 for meshes constructed directly from arrays), in the Shape.faces() enumeration — so shape.face(at: Int(tri.faceIndex)) always resolves. Use faceIndex to correlate a picked triangle back to the original solid face for CAM or selection operations.
On a shape where one face is shared by two solids, that face is meshed twice — once per owner, each wound so its normals point out of that owner — and both sets of triangles carry the one index that names it. faceIndex values are therefore not unique per face. Before #613 this was the position in a TopExp_Explorer walk instead, which on a two-solid split compound ran 0…11 over an 11-face shape, so a triangle could claim a faceIndex that face(at:) could not address.
- Fields:
v1,v2,v3— vertex indices intoMesh.vertices.faceIndex— source B-Rep face index; −1 if unknown.normal— per-triangle surface normal (computed from the cross product of two edges).
Polygon2D
A wrapper around Poly_Polygon2D: a standalone 2D polygon (sequence of 2D nodes with a deflection value), independent of Mesh’s own triangulation. Used for pcurve tessellation data attached to an edge, separately from the 3D triangle mesh above.
public final class Polygon2D: @unchecked Sendable
PolygonOnTriangulation
A wrapper around Poly_PolygonOnTriangulation: a polygon defined as a sequence of node indices into a shared Triangulation, rather than owning its own point positions.
public final class PolygonOnTriangulation: @unchecked Sendable