Contents

secondmouseau/occtswiftmesh

πŸ“– Documentation & cookbook: <https://secondmouseau.github.io/OCCTSwiftMesh/>

Why a separate package

OCCT's open-source distribution provides BRepMesh_* for mesh generation but no decimation, simplification, smoothing, hole-filling, remeshing, or other mesh-side post-processing. OCCT-Components ships a paywalled "Mesh Decimation" module β€” this package fills the same role with permissive, vendored implementations.

OCCTSwift itself stays focused on its mission as an OCCT wrapper. Mesh algorithms that happen to consume OCCT-produced meshes live here.

Status

βœ… v1.7.1 β€” SemVer-stable. Ships Mesh.simplified(:) (decimation, vendored meshoptimizer v1.1), Mesh.crossSection(plane:) (planar slicing into closed contours), the mesh connectivity toolkit (welded, faceNormals, vertexNormals, triangleAdjacency, connectedComponents, subMesh, boundaryLoops, integrityReport), Mesh.segmented(:) (dihedral region-growing + primitive-fit merge, with opt-in curvature-ordered seeding) and Mesh.segmentedRANSAC(_:)/segmentedAutoSelect(dihedral:ransac:) (Schnabel-style RANSAC alternative + bake-off), Mesh.vertexCurvatures() (Rusinkiewicz per-face curvature tensor), Mesh.aligned(to:options:) (point-to-plane ICP registration), Mesh.slippage(forTriangles:maxSamples:) (Gelfand-Guibas slippage analysis), Mesh.creaseEdges(minAngleDegrees:) (dihedral-fold ring/path detection), and Mesh.windingNumber(at:)/orientationReport(samples:) (generalized winding number). Requires OCCTSwift v1.15.10 or later. See docs/CHANGELOG.md.

API

Decimation β€” Mesh.simplified(_:)

import OCCTSwift
import OCCTSwiftMesh

let mesh: Mesh = shape.mesh()  // from OCCTSwift
let simplified = mesh.simplified(.init(
    targetTriangleCount: 5_000,
    preserveBoundary: true,
    preserveTopology: true
))

if let result = simplified {
    print("\(result.beforeTriangleCount) β†’ \(result.afterTriangleCount)")
    print("Hausdorff: \(result.hausdorffDistance)")
    let reducedMesh = result.mesh
}

Slicing β€” Mesh.crossSection(plane:)

Intersect a mesh with a plane and recover the closed contours where it cuts the surface β€” the perimeter step a 3D-printer slicer performs. Works directly on open / unwelded scan meshes (no B-Rep sewing first). A thin-walled tube slices into separate outer and inner loops, so wall thickness is just their offset; inner-vs-outer comes from contour nesting, not triangle winding.

let section = mesh.crossSection(plane: CutPlane(point: p, normal: n))
for c in section!.contours {
    // c.depth == 0 β†’ outer solid boundary; c.isHole β†’ inner wall / pocket
    print(c.points.count, "pts, area", c.area, c.isHole ? "(hole)" : "")
}

// Or a whole slicer layer stack along an axis:
let stack = mesh.crossSections(axis: axis, through: p, spacing: 2.0)

Mesh foundations β€” weld, connectivity, integrity

Raw OCCT tessellation and STL import both produce (near-)unshared vertices β€” three unique positions per triangle even where triangles are geometrically edge-adjacent. welded(tolerance:) merges coincident vertices; every adjacency-based operation below needs that welded substrate to see real connectivity.

let welded = mesh.welded()                 // 0 auto-derives 1e-6 Γ— the bbox diagonal

welded.faceNormals()                       // [SIMD3<Float>], one per triangle
welded.vertexNormals()                     // area-weighted, per vertex
welded.triangleAdjacency()                 // [[Int]] β€” edge-adjacent triangles
welded.connectedComponents()               // [MeshRegion], largest-first
welded.subMesh(triangleIndices: [0, 1])    // extract a compact standalone Mesh
welded.boundaryLoops()                     // [[UInt32]] β€” open-edge rings

let report = mesh.integrityReport()        // welds internally β€” safe to call on raw input
print(report.isWatertight, report.nonManifoldEdgeCount, report.boundaryLoopCount)
print(report.eulerCharacteristic, report.genus as Any)

Segmentation β€” Mesh.segmented(_:)

Dihedral region-growing splits a mesh into smoothly-connected surface patches, then a primitive-fit merge pass undoes coarse-tessellation "confetti" (a low-poly cylinder's facets, each past the dihedral threshold, growing back into one cylinder + end caps). Welds internally, so unwelded input doesn't silently degrade to one region per triangle.

let segmented = mesh.segmented()           // Mesh.SegmentOptions() defaults
for (region, fit) in zip(segmented.regions, segmented.fits) {
    print(region.triangleIndices.count, "triangles β†’", fit.kind, fit.residualRMS)
}
// segmented.truncatedTriangleCount reports anything dropped by maxRegions / minRegionTriangles β€”
// never silent. segmented.fitMergeSkipped reports when the fit-gated merge pass itself couldn't
// run (raw region count still over an internal cap after the cheap coplanar pre-merge) β€” also
// never silent.

SegmentOptions.curvatureSeeding (opt-in, false default) orders seeds by ascending per-face curvature AND switches growing to seed-relative absorption, so flat regions claim their full extent before a fillet/blend strip is absorbed arbitrarily by whichever neighbour reaches it first. See docs/algorithms/segmentation.md.

RANSAC segmentation + auto-selection β€” Mesh.segmentedRANSAC(_:), Mesh.segmentedAutoSelect(dihedral:ransac:)

Schnabel-style iterative primitive extraction: claims a candidate's inliers across the WHOLE mesh, not just triangles contiguous with the sample β€” the right model for a scene where the same primitive kind shows up in several disconnected patches, complementing segmented(_:)'s single-integrated-part strength. Returns the same SegmentedMesh result type.

let result = mesh.segmentedRANSAC()      // Mesh.RANSACSegmentOptions() defaults
let auto = mesh.segmentedAutoSelect()    // runs both strategies, keeps the higher-scoring one
print(auto.strategy, auto.dihedralScore, auto.ransacScore)

See docs/algorithms/ransac-segmentation.md.

Alignment β€” Mesh.aligned(to:options:)

Point-to-plane ICP registration: recovers the rigid transform that best aligns this (source) mesh onto a reference mesh. PCA/bbox pre-align (tries all 4 sign-valid orientations, keeps the best) runs before the iterative refinement; normal-space sampling (on by default) keeps a small feature's rare normal direction from being drowned out by a flat majority; trimmed correspondence handles partial overlap between the two meshes.

if let result = scan.aligned(to: cad) {
    print(result.transform)       // simd_double4x4 β€” maps scan's original vertices into cad's frame
    print(result.residualRMS, result.iterations, result.converged)
}

Curvature β€” Mesh.vertexCurvatures()

Per-vertex principal curvatures and directions (Rusinkiewicz per-face tensor method, chosen over Meyer's cotan-Laplacian for robustness on noisy/irregular tessellation β€” no obtuse-triangle clamp anywhere in the pipeline). Requires welded input, same precondition as triangleAdjacency() /connectedComponents().

let welded = mesh.welded()
for c in welded.vertexCurvatures() {
    print(c.k1, c.k2, c.mean, c.gaussian)  // c.d1/c.d2 β€” unit principal directions
}
// k1 is signed positive for a convex bulge (e.g. an outward-facing sphere), matching
// vertexNormals()'s own outward-normal convention. A face too degenerate/sliver-thin for a
// stable fit is excluded from it entirely; a vertex touched only by such faces reports
// k1 == k2 == 0, never NaN.

Slippage analysis β€” Mesh.slippage(forTriangles:maxSamples:)

Classifies a segmented region's surface kind (plane / sphere / cylinder / extrusion / revolution / helix / freeform) and recovers its characteristic axis, by local slippage analysis (Gelfand & Guibas, SGP 2004). Like triangleAdjacency(), operates on this mesh's own vertex/normal arrays β€” pass an already-welded mesh.

let result = mesh.slippage(forTriangles: region.triangleIndices)
print(result.kind, result.axisPoint as Any, result.axisDirection as Any)
// .helix additionally sets result.pitch (translation per radian of rotation)

Crease-edge detection β€” Mesh.creaseEdges(minAngleDegrees:)

Finds dihedral-fold edges (fold angle >= threshold, default 30Β°) and chains them into rings (closed loops, e.g. a door outline) and paths (open chains, e.g. a crease running off an open boundary) β€” outlining recessed/raised features on raw scan meshes. Like triangleAdjacency(), requires a welded mesh.

let result = mesh.creaseEdges()
for ring in result.rings {
    print(ring.closed, ring.vertexIndices.count, ring.length, ring.meanFoldAngleDegrees)
}
print(result.unchainedCreaseEdgeCount)   // never silent

See docs/algorithms/crease-detection.md.

Winding number β€” Mesh.windingNumber(at:), Mesh.orientationReport(samples:)

Generalized winding number (Jacobson, Kavan, Sorkine-Hornung, SIGGRAPH 2013): a direct solid-angle sum that stays well-behaved on open shells, soup, and self-intersecting input, unlike parity/ray tests. No welding precondition β€” every triangle contributes independently.

print(mesh.windingNumber(at: SIMD3(0, 0, 0)))   // ~1 inside a closed mesh, ~0 outside
let report = mesh.orientationReport()
print(report.looksInverted, report.meanExteriorWinding)

See docs/algorithms/winding-number.md β€” including an important caveat: this exterior-sampling diagnostic is provably powerless to detect inversion on a closed, watertight mesh (its real value is on open shells).

Installation

// Package.swift
dependencies: [
    .package(url: "https://github.com/SecondMouseAU/OCCTSwiftMesh.git", from: "1.1.2"),
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [
            .product(name: "OCCTSwiftMesh", package: "OCCTSwiftMesh"),
        ]
    )
]

Documentation

Full docs & cookbook: <https://secondmouseau.github.io/OCCTSwiftMesh/>

| Document | Description | |----------|-------------| | API Reference | Per-type function reference β€” signatures, parameters, examples | | Decimation algorithm notes | QEM backend, vendored meshoptimizer, Hausdorff units | | Mesh foundations | Welding, connectivity, integrity reporting | | Segmentation Β· RANSAC segmentation | Dihedral region-growing + primitive-fit merge; Schnabel-style RANSAC alternative | | Curvature | Rusinkiewicz per-face curvature tensor method | | Alignment | Point-to-plane ICP registration | | Slippage analysis | Gelfand-Guibas surface-kind classification | | Crease detection | Dihedral-fold ring/path detection | | Winding number | Generalized winding number, orientation diagnostics | | Cookbook | Task-oriented, example-rich guides | | Changelog | Release-by-release history | | Vendoring | Re-vendoring procedure for the bundled meshoptimizer |

License

LGPL-2.1, matching OCCTSwift. Vendored components retain their own permissive licenses (notably meshoptimizer under MIT). See NOTICE.md.

Roadmap

Beyond initial decimation:

  • Subdivision (Catmull-Clark, Loop)
  • Laplacian / Taubin smoothing
  • Mesh repair (non-manifold cleanup, hole filling)
  • Remeshing (uniform / adaptive)
  • glTF mesh-export niceties (LOD chains, meshopt-encoded streams)
  • GPU-accelerated mesh ops where worthwhile

Community needs drive priority β€” file an issue if you want one of these (or something else) sooner.

Package Metadata

Repository: secondmouseau/occtswiftmesh

Default branch: main

README: README.md