Skip to content

2. Repairing the surface ​

Chapter 1 found that the dragon is neither watertight nor manifold. Before it goes into a grid, sanitize repairs what can be repaired, and the validity predicates say what is left.

f90
use fossil, only : surface_stl_object
use penf,   only : I4P, R8P

implicit none
type(surface_stl_object) :: surface, original
integer(I4P)             :: facets_before
real(R8P)                :: lower, upper

call surface%load_from_file(file_name='dragon.stl', guess_format=.true.)
facets_before = surface%get_facets_number()
f90
call surface%sanitize(fill_holes=.true.)
  • sanitize runs the repair passes in order: remove_degenerate_facets (zero-area and sliver facets), connect_nearby_vertices (only when facets have disconnected edges), analyze, remove_duplicate_facets (the same triangle twice, in any winding), then sanitize_normals (consistent winding, outward by the sign of the volume). The surface is analyzed again after every pass that changes it.
  • fill_holes=.true. adds fill_holes after the orientation pass: every closed loop of boundary edges with at most max_hole_edges edges (default 200) and a bounding-box diagonal at most max_hole_size times the surface's (default 0.1) is closed by a minimum-weight patch. Larger loops stay open, so a genuine opening (an inlet, a symmetry cut) survives. refine_holes=.true. and fair_holes=.true. refine the patches and fair them to a smooth surface. The default is fill_holes=.false..

a round hole cut into the repaired dragon, its boundary loop in ember: the facet edges without a neighbour

the same hole closed by fill_holes with refine and fair: the patch in ember, refined to the density of the loop and faired to a smooth surface

A hole is a loop of boundary edges, the facet edges without a neighbour (fcon_edge == 0); fill_holes triangulates it, refine matches the density of the loop and fair smooths the patch into the surface around it.

a sphere with four round holes of different sizes, closed one per frame by fill_holes(refine, fair) as max_hole_size grows past each loop's size: the patches in ember, the loops still open outlined

  • Each defect class left or removed is reported once on stderr, as a WARNING: sanitize: line.
f90
print '(A,I0,A,I0)', 'facets      ', facets_before, ' -> ', surface%get_facets_number()
print '(A,I0)',      'degenerate  ', surface%get_degenerate_facets_removed()
print '(A,I0)',      'duplicate   ', surface%get_duplicate_facets_removed()
print '(A,I0)',      'non-manif.  ', surface%get_non_manifold_edges_number()
print '(A,F12.4)',   'volume      ', surface%get_volume()
print '(A,L1,A,L1,A,L1)', 'watertight  ', surface%is_watertight(), '   manifold ', surface%is_manifold(), &
                          '   volume ', surface%is_volume()
call surface%save_into_file(file_name='dragon-clean.stl')
print '(A)', 'saved dragon-clean.stl'
  • get_degenerate_facets_removed and get_duplicate_facets_removed count what the last passes dropped.
  • is_volume() is true when the surface is manifold, its signed volume positive and its centroid finite: the condition for the pseudo-normal sign of chapter 4 to be exact.
  • save_into_file without is_ascii writes ASCII.

How far did the repair move the dragon?

f90
call original%load_from_file(file_name='dragon.stl', guess_format=.true.)
call surface%deviation(original, lower=lower, upper=upper)
print '(A,ES10.3,A,ES10.3)', 'repaired -> original ', lower, ' <= h <= ', upper
call original%deviation(surface, lower=lower, upper=upper)
print '(A,ES10.3,A,ES10.3)', 'original -> repaired ', lower, ' <= h <= ', upper
  • deviation(other, lower=, upper=) brackets the one-sided Hausdorff distance h(self, other): how far the farthest point of self is from other. The bracket is certified, to 1e-3 of the bounding-box diagonal by default.
  • The two directions differ. The repaired dragon's farthest points are in the middle of the hole patches. The original's farthest points are on the facets sanitize dropped as degenerate or duplicate. See surface deviation.

Running it ​

$ tut02_repair
WARNING: sanitize: 54 hole(s) filled
WARNING: sanitize: 136 boundary edge(s) not forming a closed loop
WARNING: sanitize: 62 duplicate facet(s) removed
WARNING: sanitize: 188 facet(s) with 1 disconnected edge remain
WARNING: sanitize: 14 facet(s) with 2 disconnected edges remain
WARNING: sanitize: 54 non-manifold edge(s) (3+ incident facets) detected
facets      6588 -> 6586
degenerate  0
duplicate   62
non-manif.  54
volume          465.9329
watertight  F   manifold F   volume F
saved dragon-clean.stl
repaired -> original  9.439E-02 <= h <=  1.190E-01
original -> repaired  3.009E-03 <= h <=  2.968E-02

The repair removed 62 duplicate facets and filled 54 holes, but the dragon is still not watertight: 54 edges carry three or more facets, and 136 boundary edges do not close into a loop, so fill_holes cannot patch them. sanitize fixes local defects; it does not rebuild a surface. The rest of the tutorial uses this cleaned but open dragon on purpose: it shows where the choice of sign algorithm matters (chapters 4 and 5) and which cut cells cannot be trusted (chapter 10). For a watertight surrogate of a broken surface see Alpha wrapping.

What you learned

sanitize (with fill_holes=.true. for small holes) repairs local defects and reports them on stderr; is_watertight, is_manifold and is_volume tell whether the result is a closed solid, and deviation how far the repair moved it. Reference: Hole filling, Surface deviation.

Next: 3. Placing the body.