Skip to content

Usage

All examples use use fossil which re-exports surface_stl_object and facet_object, plus the constants for sign algorithms (SIGN_PSEUDO_NORMAL, SIGN_RAY_INTERSECTIONS, SIGN_SOLID_ANGLE), tree kinds (AABB_TREE_SAH_BVH, AABB_TREE_OCTREE), and status codes (STATUS_OK, STATUS_INVALID_INPUT, …).

Numeric kinds (I4P, R8P) come from PENF; 3D vectors and unit versors (ex_R8P, ey_R8P, ez_R8P) come from VecFor.

Loading an STL file

Auto-detect format

Pass guess_format=.true. to let FOSSIL determine whether the file is ASCII or binary automatically.

fortran
use fossil
use penf, only: R8P

type(surface_stl_object) :: surface

call surface%load_from_file(file_name='dragon.stl', guess_format=.true.)

load_from_file runs analyze internally — bounding box, connectivity, volume, centroid, AABB tree, and pseudo-normals are all populated before it returns. You typically call sanitize next to repair common STL defects.

Explicit format

fortran
! Explicitly ASCII
call surface%load_from_file(file_name='naca0012.stl', is_ascii=.true.)

! Explicitly binary
call surface%load_from_file(file_name='part.stl', is_ascii=.false.)

Load with on-the-fly clipping

Facets whose vertices all lie outside the bounding box are discarded during the load itself, without a separate clip pass:

fortran
use fossil
use penf, only: R8P
use vecfor, only: vector_R8P

type(surface_stl_object) :: surface
type(vector_R8P)         :: bmin, bmax

bmin%x = -15.0_R8P ; bmin%y = -5.0_R8P ; bmin%z = 0.0_R8P
bmax%x =   0.0_R8P ; bmax%y =  5.0_R8P ; bmax%z = 20.0_R8P

call surface%load_from_file(file_name='dragon.stl', guess_format=.true., &
                            clip_min=bmin, clip_max=bmax)

Rejecting bad input

If any vertex coordinate is NaN or Inf, load_from_file refuses the file via the optional status argument rather than letting the bad value propagate:

fortran
use fossil
use penf, only: I4P
integer(I4P) :: stat

call surface%load_from_file(file_name='maybe_bad.stl', guess_format=.true., status=stat)
if (stat == STATUS_INVALID_INPUT) then
   write(*, '(A)') 'STL contains non-finite coordinates; aborting.'
   stop
endif

Printing statistics

fortran
print '(A)', surface%statistics()

Example output (cube fixture, post-sanitize):

X extents: [+0.000000000000000E+000, +0.100000000000000E+001]
Y extents: [+0.000000000000000E+000, +0.100000000000000E+001]
Z extents: [+0.000000000000000E+000, +0.100000000000000E+001]
volume: +0.100000000000000E+001
centroid: [+0.500000000000000E+000, +0.500000000000000E+000, +0.500000000000000E+000]
number of facets: +12
number of facets with 1 edges disconnected: +0
number of facets with 2 edges disconnected: +0
number of facets with 3 edges disconnected: +0
number of non-manifold edges (3+ incident facets): +0
degenerate facets removed (last pass): +0
duplicate facets removed (last pass): +0
number of AABB refinement levels: +1

TIP

Volume is positive for a closed body with outward-pointing normals (the standard divergence-theorem convention; sanitize produces this state). If you see a negative volume after sanitize, the mesh is probably non-watertight — check the non-manifold-edge and disconnected-edge counts.


Saving an STL file

fortran
! Save as ASCII
call surface%save_into_file(file_name='output.stl', is_ascii=.true.)

! Save as binary (default)
call surface%save_into_file(file_name='output.stl')

Sanitize (the full repair pipeline)

sanitize is the orchestrator. It runs every repair pass in the right order and prints one stderr warning per defect class detected.

fortran
use fossil

type(surface_stl_object) :: surface

call surface%load_from_file(file_name='messy.stl', guess_format=.true.)
call surface%sanitize
print '(A)', surface%statistics()

The passes that run inside sanitize:

  1. remove_degenerate_facets — drop zero-area / sliver triangles before their NaN normals contaminate downstream queries.
  2. connect_nearby_vertices (if disconnected edges were detected) — union-find vertex deduplication on a tolerance.
  3. analyze — rebuild metrix, connectivity, vertex occurrences.
  4. remove_duplicate_facets — drop literal duplicates, including reversed-orientation ones.
  5. analyze — rebuild after duplicate removal (only if any duplicates were dropped).
  6. sanitize_normals — BFS-propagate winding consistency, then global flip so volume > 0.

Each individual pass is also callable directly if you want fine-grained control.


Validity predicates

After sanitize, three named predicates summarise the mesh state:

fortran
if (.not. surface%is_watertight()) print '(A)', 'surface has boundary or non-manifold edges'
if (.not. surface%is_manifold())   print '(A)', 'surface has non-manifold edges (3+ incidences)'
if (.not. surface%is_volume()) then
   print '(A)', 'volume is not physically meaningful — do not trust get_volume()/get_centroid()'
endif
PredicateTrue iff
is_watertightEvery edge has exactly 2 incident facets
is_manifoldNo non-manifold edges (boundary edges allowed)
is_volumewatertight AND volume > 0 AND finite centroid

Translate

Move the surface by a vectorial delta or by individual axis components:

fortran
use fossil
use vecfor, only: vector_R8P

type(vector_R8P) :: delta

delta%x = 1.0_R8P ; delta%y = 0.0_R8P ; delta%z = 0.0_R8P

! Translate by a 3D vector
call surface%translate(delta=delta)

! Translate by scalar components (any combination)
call surface%translate(x=2.0_R8P)
call surface%translate(y=1.5_R8P)
call surface%translate(x=1.0_R8P, y=2.0_R8P, z=0.5_R8P)

Rotate

Rotate around an arbitrary axis by an angle in radians:

fortran
use fossil
use vecfor, only: ex_R8P, vector_R8P
use penf, only: R8P

type(vector_R8P) :: axis
real(R8P)        :: angle

axis  = ex_R8P        ! rotate around x-axis
angle = 1.5707963_R8P ! π/2 radians

call surface%rotate(axis=axis, angle=angle)

Alternatively, provide a pre-built rotation matrix:

fortran
use vecfor, only: rotation_matrix_R8P, vector_R8P

real(R8P) :: matrix(3,3)
matrix = rotation_matrix_R8P(axis=ex_R8P, angle=1.5707963_R8P)
call surface%rotate(matrix=matrix)

Mirror

Mirror with respect to a plane defined by its outward normal:

fortran
use fossil
use vecfor, only: ex_R8P, ey_R8P, vector_R8P

type(vector_R8P) :: normal

! Mirror with respect to the YZ-plane (normal = x-axis)
normal = ex_R8P
call surface%mirror(normal=normal)

! Mirror with respect to an oblique plane
normal = ex_R8P + ey_R8P
call surface%mirror(normal=normal)

Or by a pre-built mirror matrix:

fortran
use vecfor, only: mirror_matrix_R8P

real(R8P) :: matrix(3,3)
matrix = mirror_matrix_R8P(normal=ex_R8P)
call surface%mirror(matrix=matrix)

Resize (scale)

Scale all facets by a uniform or per-axis factor. The default pivot is the origin; pass respect_centroid=.true. to scale about the surface centroid instead:

fortran
use fossil
use vecfor, only: ex_R8P, ey_R8P, ez_R8P, vector_R8P
use penf, only: R8P

type(vector_R8P) :: factor

! Uniform scale by a 3D vector factor
factor = 2.0_R8P * ex_R8P + 2.0_R8P * ey_R8P + 2.0_R8P * ez_R8P
call surface%resize(factor=factor)

! Per-axis scalar scale
call surface%resize(x=0.5_R8P, z=1.2_R8P)

! Scale about the centroid
call surface%resize(factor=factor, respect_centroid=.true.)

Clip

Discard all facets outside a given axis-aligned bounding box. The removed facets can be captured as a separate surface:

fortran
use fossil
use vecfor, only: vector_R8P

type(surface_stl_object) :: surface, remainder
type(vector_R8P)         :: bmin, bmax

bmin%x = -15.0_R8P ; bmin%y = -5.0_R8P ; bmin%z = 0.0_R8P
bmax%x =   0.0_R8P ; bmax%y =  5.0_R8P ; bmax%z = 20.0_R8P

call surface%clip(bmin=bmin, bmax=bmax, remainder=remainder)
! `clip` re-runs analyze internally on both `surface` and `remainder`.

Merge

Combine two surfaces into one. The result is stored in the first surface:

fortran
use fossil

type(surface_stl_object) :: surface_1, surface_2

call surface_1%load_from_file(file_name='dragon_part_1.stl', guess_format=.true.)
call surface_2%load_from_file(file_name='dragon_part_2.stl', guess_format=.true.)

call surface_1%merge_solids(other=surface_2)
call surface_1%analyze        ! rebuild connectivity / tree after the merge
call surface_1%save_into_file(file_name='dragon_merged.stl')

Distance queries

Unsigned distance (squared, default)

fortran
use fossil
use vecfor, only: ex_R8P, ey_R8P, ez_R8P
use penf, only: R8P

real(R8P) :: d2

d2 = surface%distance(point=2.0_R8P * ex_R8P + 0.0_R8P * ey_R8P + 0.0_R8P * ez_R8P)

Euclidean (non-squared) distance

fortran
d = surface%distance(point=p, is_square_root=.true.)

Signed distance

Negative inside, positive outside. Default sign algorithm is SIGN_PSEUDO_NORMAL (Bærentzen–Aanæs); pass sign_algorithm= to override.

fortran
real(R8P) :: sd
sd = surface%distance(point=p, is_signed=.true., is_square_root=.true.)

! Override the sign algorithm for benchmarking or for meshes whose orientation
! cannot be sanitised (e.g. open shells where outward is ill-defined).
sd = surface%distance(point=p, is_signed=.true., is_square_root=.true., &
                      sign_algorithm=SIGN_RAY_INTERSECTIONS)

Point-in-polyhedron test

fortran
logical :: inside

inside = surface%is_point_inside(point=0.5_R8P * ex_R8P + 0.5_R8P * ey_R8P + 0.5_R8P * ez_R8P)
if (inside) print *, 'point is inside the surface'

Choosing the AABB tree kind

The default AABB_TREE_SAH_BVH is faster on every measured mesh (see Features). The legacy octree is still selectable:

fortran
call surface%load_from_file(file_name='dragon.stl', guess_format=.true., &
                            aabb_tree_kind=AABB_TREE_OCTREE)

Both kinds produce bit-exact distance results — the difference is purely query speed.


Connect nearby vertices (repair)

sanitize calls this internally when needed; you only invoke it directly if you want to repair a surface without running the full pipeline:

fortran
use fossil

type(surface_stl_object) :: surface

call surface%load_from_file(file_name='disconnected.stl', guess_format=.true.)
print '(A)', surface%statistics()   ! shows disconnected edge counts

call surface%connect_nearby_vertices
call surface%analyze
print '(A)', surface%statistics()   ! disconnected edges should be reduced

Full workflow example

fortran
use fossil
use penf, only: R8P
use vecfor, only: ex_R8P, ey_R8P, ez_R8P

type(surface_stl_object) :: surface

! 1. Load (analyze runs internally).
call surface%load_from_file(file_name='src/tests/dragon.stl', guess_format=.true.)

! 2. Run the full repair pipeline.
call surface%sanitize
print '(A)', surface%statistics()

! 3. Center at origin and rescale to fit in a unit box.
associate(c => surface%get_centroid(), bmin => surface%get_bmin(), bmax => surface%get_bmax())
   call surface%translate(delta=-c)
   call surface%resize(factor=(1.0_R8P / maxval([bmax%x - bmin%x, bmax%y - bmin%y, bmax%z - bmin%z])) * &
                              (ex_R8P + ey_R8P + ez_R8P))
end associate
call surface%analyze

! 4. Save.
call surface%save_into_file(file_name='dragon-normalized.stl')