RasmahRasmah

BRep Cleanup: Segmentation, Defeaturing, and Continuity

Overview

An imported STEP file or a merged assembly is often a single BRep that actually contains several disjoint solids, or a solid littered with tiny featurelets (fillets, holes, logos) that complicate meshing. Rasmah provides three cleanup operations on the exact BRep:

  • Segmentation — split a multi-solid BRep into its connected solids (num_solids, separate_solids, extract_solid, connected_components).
  • Defeaturing — drop small solids/components below a size threshold (defeature), or fill small holes in faces (fill_face_holes).
  • Continuity — measure the geometric smoothness across an edge (edge_continuity).

This tutorial walks the cleanup the way it happens in practice: an imported 80 × 60 × 8 mm mounting plate arrives with a stray fragment, is split into solids, defeatured, and its edges classified. Dimensions are millimetres.

using Rasmah

A multi-solid import

The union of the plate and a stray 6 mm fragment — a left-over construction cube — produces a BRep with two solids that do not touch:

m = brep(union(box(80.0, 60.0, 8.0), translate(box(6.0, 6.0, 6.0), 60.0, 0.0, 0.0)))

println("solids: ", num_solids(m), ", components: ", length(connected_components(m)),
        ", watertight: ", is_watertight(m))
solids: 2, components: 2, watertight: true
vtk(m)

Segmentation

separate_solids splits the BRep into one watertight BRep per connected solid; extract_solid pulls out a single one by index:

parts = separate_solids(m)
println("separated into ", length(parts), " solids")
for (i, p) in enumerate(parts)
    println("  solid ", i, ": volume ", round(volume(surface_mesh(p; n = 32)); digits = 1),
            " mm³, watertight ", is_watertight(p))
end

cube = extract_solid(m, 2)
println("stray fragment volume = ", round(volume(surface_mesh(cube; n = 32)); digits = 1),
        " mm³  (6³ = 216)")
separated into 2 solids
  solid 1: volume 38400.0 m^3 mm³, watertight true
  solid 2: volume 216.0 m^3 mm³, watertight true
stray fragment volume = 216.0 m^3 mm³  (6³ = 216)

The plate is the 38 400 mm³ solid and the fragment the 216 mm³ one — each now a valid closed body.

Defeaturing

defeature removes solids of volume < tol³ — the “virtual topology” cleanup before meshing. A 10 mm threshold drops the 216 mm³ stray fragment while keeping the plate:

clean = defeature(m; tol = 10.0)
println("after defeature: ", num_solids(clean), " solid, volume ",
        round(volume(surface_mesh(clean; n = 32)); digits = 1), " mm³")
after defeature: 1 solid, volume 38400.0 m^3 mm³

Filling small holes

The same idea applies inside a face. A plate with a Ø0.5 locator hole — scan noise or a feature the mesh should ignore — has three hole loops (top face, bottom face, and the hole wall); fill_face_holes fills holes whose parameter-space area is ≤ tol²:

drilled = brep(difference(box(80.0, 60.0, 8.0), cylinder(0.25, 9.0)))
println("drilled plate: ", length(drilled.faces), " faces, ",
        sum(length(f.loops) - 1 for f in drilled.faces), " hole loops, watertight ",
        is_watertight(drilled))

filled = fill_face_holes(drilled; tol = 2.0)
println("after fill_face_holes(tol = 2): ", sum(length(f.loops) - 1 for f in filled.faces),
        " hole loops, watertight ", is_watertight(filled))
drilled plate: 7 faces, 3 hole loops, watertight true
after fill_face_holes(tol = 2): 1 hole loops, watertight true

With tol = 2 the Ø0.5 hole (≈0.2 mm²) is filled while an intentional Ø6 mounting hole (≈28 mm²) would be kept — small-feature cleanup that respects the drawing.

Edge continuity

edge_continuity(b, ei) classifies an edge as G0 (position-continuous, i.e. sharp), G1 (tangent-continuous), or G2 (curvature-continuous). The drilled plate’s edges are sharp, a sphere’s seam is fully smooth, and a cylinder’s cap rim is sharp:

plate = brep(difference(box(80.0, 60.0, 8.0), cylinder(3.0, 9.0)))
println("plate edges (sharp):   ", [edge_continuity(plate, ei) for ei in 1:2], " …")

sph_b = brep(sphere(1.0))
println("sphere seams (smooth): ", [edge_continuity(sph_b, ei) for ei in 1:2])

cyl_b = brep(cylinder(1.0, 2.0))
println("cylinder cap rim:      ", edge_continuity(cyl_b, 2), " (G0: perpendicular)")
plate edges (sharp):   [0, 0] …
sphere seams (smooth): [3, 3]
cylinder cap rim:      0 (G0: perpendicular)

This is the classification a fillet/chamfer or a mesher uses to decide whether an edge is a sharp feature to preserve.

Next steps

  • BRep booleans — the boolean and editing operations these cleanups follow.
  • STEP — the import format that most often needs this cleanup.
  • Meshing algorithms — meshing the cleaned solid.