Skip to main content
This example loads a .implicit file (a heat sink) and meshes it directly using the library’s native ntop_core_generate_mesh_by_DC dual contouring function, collecting vertices and triangle indices through callbacks (including a progress/cancellation callback with a configurable timeout). Unlike the Mesh example, no external meshing library is used — dual contouring is performed entirely inside nTop Core. The resulting mesh is written to an STL file and also rendered to a PNG with Matplotlib.
"""Example using the nTop Core meshing by DC call."""
# region Imports
from ctypes import (
    CDLL,
    CFUNCTYPE,
    pointer,
    POINTER,
    c_bool,
    c_char_p,
    c_double,
    c_uint32,
    c_void_p,
    Structure
)
import os
from pathlib import Path
from typing import Any

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import numpy as np
import numpy.typing as npt
from skimage import measure
import datetime
# endregion Imports

#########
#########
# Setup #
#########
#########
# region Preamble Setup

########################
# nTop Core Primitives #
########################
# region nTop Core Primitives

class Vec3(Structure):
    """X, Y, Z components representing a point or vector."""
    _fields_ = [("x", c_double), ("y", c_double), ("z", c_double)]

class BoundingBox(Structure):
    """
    Minimal, axis-aligned bounding box of an Implicit.

    The representation is comprised of two points (min_x, min_y, min_z) and (max_x, max_y, max_z).
    """
    _fields_ = [("min", Vec3), ("max", Vec3)]

# endregion

###########################################################
# Setup library functions and data marshaling definitions #
###########################################################
# region Library wrapping setup 

# Load the nTop Core library
NTOP_CORE_LIB_ENVVAR = "NTOP_CORE_LIB"
if NTOP_CORE_LIB_ENVVAR in os.environ:
    env_lib_path = Path(os.environ[NTOP_CORE_LIB_ENVVAR])
    if env_lib_path.exists() and env_lib_path.is_file():
        path_to_lib = env_lib_path
    else:
        raise FileNotFoundError(env_lib_path)

ntop_core = CDLL(str(path_to_lib))
# In order to successfully load the nTop Core Library the dependent libraries of
# the `ntop_core` library must be on the PATH or the current working directory must contain
# those libraries not found on the PATH.
# For simplicity here we are choosing, for the moment, to append to the PATH environment
if str(path_to_lib.parent) not in os.environ["PATH"]:
    os.environ["PATH"] += os.pathsep + str(path_to_lib.parent)

# Define data marshalling specification for loading implicit
ntop_core.ntop_core_import_from_file.restype = c_uint32
ntop_core.ntop_core_import_from_file.argtypes = [
    c_char_p,
    POINTER(c_void_p)
]

def callback_import_from_file_error_check(result: c_uint32, func, arguments: Any) -> bool:
    """
    Callback function which happens after the function is called but before 
    control is returned to the caller enabling error handling.

    :param result: Return value of `ntop_core_import_from_file`
    :param arguments: Tuple of parameters passed to the invocation
        of `ntop_core_import_from_file`
    :returns: True if loading the implicit was successful.
    :raises: RuntimeError if any error state is reported by nTop Core.
    """
    # Check to see if the implicit loaded correctly
    if result != 0:
        raise RuntimeError(f"Cannot load implicit: {arguments[0]}.")

    return True

# Setup error check when loading the file.
ntop_core.ntop_core_import_from_file.errcheck = callback_import_from_file_error_check

# Define data marshalling specification for releasing the memory used for the `.implicit`
ntop_core.ntop_core_release.restype = None
ntop_core.ntop_core_release.argtypes = [c_void_p]

# Define data marshalling specification for getting the bounding box of an implicit
ntop_core.ntop_core_query_bounding_box.restype = None
ntop_core.ntop_core_query_bounding_box.argtypes = [
    c_void_p,
    POINTER(BoundingBox)
]

# Define the signatures for the callbacks in the `ntop_core_generate_mesh_by_DC``
CFUNCTYPE_MESH_VERTEX_CALLBACK = CFUNCTYPE(
    None,
    c_void_p,
    POINTER(Vec3),
    c_uint32
)

CFUNCTYPE_MESH_TRIANGLE_CALLBACK = CFUNCTYPE(
    None,
    c_void_p,
    c_uint32,
    c_uint32,
    c_uint32
)

CFUNCTYPE_MESH_CONTINUATION_CALLBACK = CFUNCTYPE(c_bool, c_void_p, c_uint32)

ntop_core.ntop_core_generate_mesh_by_DC.restype = None
ntop_core.ntop_core_generate_mesh_by_DC.argtypes = [c_void_p, c_double, c_double, c_void_p, c_void_p, c_void_p, CFUNCTYPE_MESH_VERTEX_CALLBACK, CFUNCTYPE_MESH_TRIANGLE_CALLBACK,  CFUNCTYPE_MESH_CONTINUATION_CALLBACK]

# Example callbacks that can be used with `ntop_core_generate_mesh_by_DC` invocations.
def mesh_vertex_callback(scope_pass) -> CFUNCTYPE_MESH_VERTEX_CALLBACK:
    """Callback for the ntop_core_generate_mesh_by_DC to smuggle out the vertex data."""

    def gather_vertex(context: c_void_p, pt:POINTER(Vec3), index:c_uint32):
        scope_pass.append([pt.contents.x, pt.contents.y, pt.contents.z])

    return CFUNCTYPE_MESH_VERTEX_CALLBACK(gather_vertex)

def mesh_triangle_callback(scope_pass) -> CFUNCTYPE_MESH_TRIANGLE_CALLBACK:
    """Callback for the ntop_core_generate_mesh_by_DC to smuggle out the triangle index data."""

    def gather_triangle(context: c_void_p, a:c_uint32, b:c_uint32, c:c_uint32):
        scope_pass.append([a,b,c])

    return CFUNCTYPE_MESH_TRIANGLE_CALLBACK(gather_triangle)

class mesh_continuation:
    completed: bool
    timeout: int
    reporting_frequency: int

def mesh_continuation_callback(scope_pass) -> CFUNCTYPE_MESH_CONTINUATION_CALLBACK:
    """Callback for the ntop_core_generate_mesh_by_DC to report progress and optionally cancel."""
    global last_report
    now = datetime.datetime.now()
    stop_after = now + datetime.timedelta(seconds = scope_pass.timeout)
    last_report = now
    
    def proceed(context: c_void_p, percent: c_uint32):
        global last_report
        now =  datetime.datetime.now() 
        scope_pass.completed = now < stop_after
        if (now > last_report + datetime.timedelta(seconds = scope_pass.reporting_frequency)):
            print(f"Meshing {percent}% complete.")
            last_report = datetime.datetime.now()
        return scope_pass.completed

    return CFUNCTYPE_MESH_CONTINUATION_CALLBACK(proceed)

# endregion Library wrapping setup 

# endregion Preamble Setup

######################
######################
# Utility Functions #
######################
######################
# region Utilities

###################
# STL File Writer #
###################
# region STL writer
def write_stl(triangles: npt.ArrayLike, filename: Path) -> None:
    """
    Write a mesh stored as triangles in STL format.
    
    Note: degenerate faces where vertices are colinear are filtered out.

    :param triangles: numpy Array of dimensions Nx3x3 (N is the number of triangles in
                    the mesh) with each vertex in 3D space. 
    :param filename: File name of the STL to write.
    """
    with open(filename, "w") as f:
        f.write("solid object\n")
        for triangle in triangles:
            normal = np.cross(triangle[1] - triangle[0], triangle[2] - triangle[0])
            #if (np.sum(normal ** 2) < 0): 
            #    print(normal)
            #    print(triangle)
            normal_length = np.sqrt(np.sum(normal ** 2))
            if (normal_length == 0):
                # Skip degenerate faces
                continue

            unit_normal = normal / normal_length

            f.write(f"  facet normal {unit_normal[0]:.6f}  {unit_normal[1]:.6f}  {unit_normal[2]:.6f}\n")
            f.write(f"    outer loop\n")
            for point in triangle:
                f.write(f"      vertex {point[0]:.6f}  {point[1]:.6f}  {point[2]:.6f}\n")
            f.write(f"    endloop\n")
            f.write(f"  endfacet\n")
        f.write("endsolid object\n")

# endregion STL writer

# endregion Utilities

################################################
################################################
# Functional example of meshing with nTop Core #
################################################
#################################################
# region Example meshing code with nTop Core

# Load the implicit
gyroid_implicit_file_path = Path(__file__).parent.parent / "assets" / "heat-sink.implicit"
print(f"Reading {gyroid_implicit_file_path}.")
gyroid_handle = c_void_p()
ntop_core.ntop_core_import_from_file(str(gyroid_implicit_file_path).encode("utf8"), gyroid_handle)


# Setup meshing parameters and callbacks
#
# Lists to store meshing callback values dynamically
vertices = []
triangle_indices = []
# Granularity, in meters, at which the body's field is sampled. 
# Make this smaller to get a finer mesh.
feature_size = 0.00025 
# Controls deimation of the mesh. 
# Make this smaller to get a finer mesh. 0 for no deimation.
adaptivity = 1
continuation = mesh_continuation()
continuation.completed = True        # Set false by callback if computation was cancelled
continuation.timeout = int(os.environ.get('FUNC_TIMEOUT', 10))            # Seconds until meshing computation is cancelled
continuation.reporting_frequency = 1 # Minimum seconds between progress reports

# Mesh the implicit body
ntop_core.ntop_core_generate_mesh_by_DC(gyroid_handle, 
                                        feature_size, 
                                        adaptivity, 
                                        0, 
                                        0, 
                                        0, 
                                        mesh_vertex_callback(vertices), 
                                        mesh_triangle_callback(triangle_indices), 
                                        mesh_continuation_callback(continuation))

if (not continuation.completed):
    print("Meshing took too long.  Adjust feature_size, timeout, or model to allow meshing to complete.")
    exit(1)
else: 
    print("Meshing completed.")
    print("Gathering vertices and faces.")

# Copy values from lists to numpy arrays.
np_vertices = np.asarray(vertices)
np_triangle_indices = np.asarray(triangle_indices) 

# Use fancy indexing to make the triangles
triangles = np_vertices[np_triangle_indices]

# Get the bounding box to know the extent of the points to render
bounding_box = BoundingBox()
ntop_core.ntop_core_query_bounding_box(gyroid_handle, pointer(bounding_box))

# Free the memory associated with the implicit as no more calls are needed
ntop_core.ntop_core_release(gyroid_handle)

# Make a folder to capture the output
output_folder = Path(__file__).parent / "output"
output_folder.mkdir(exist_ok=True)

# Write the mesh as an STL
generated_mesh_filepath = output_folder / "mesh_by_DC.stl"
print(f"Writing stl to {generated_mesh_filepath}")
write_stl(triangles, generated_mesh_filepath)

# Display resulting mesh using Matplotlib.
print("Generating Matplotlib display.")
fig = plt.figure(figsize=(25, 25))
ax = fig.add_subplot(111, projection='3d')

# Add the mesh visualization
# Note: Matplotlib is not very performant for this rendering task and should
# primarily be used for proof-of-concept using other mesh visualization tools
# for deeper analysis.
mesh = Poly3DCollection(triangles)
mesh.set_edgecolor('k')
ax.add_collection3d(mesh)

# Label the plot
ax.set_title("Mesh visualization of implicit")
ax.set_xlabel("x-axis (meters)")
ax.set_ylabel("y-axis (meters)")
ax.set_zlabel("z-axis (meters)")

# Set reasonable limits to scale the part appropriately for viewing
ax.set_xlim(bounding_box.min.x, bounding_box.max.x)
ax.set_ylim(bounding_box.min.y, bounding_box.max.y)
ax.set_zlim(bounding_box.min.z, bounding_box.max.z)

# Render the mesh and save to a PNG
print("Rendering as png.")
mesh_render_filepath = output_folder / "mesh_by_DC.png"
plt.savefig(mesh_render_filepath)
print(f"Render of mesh saved to {mesh_render_filepath}")

# Uncomment the line below to render the mesh in an interactive window
#plt.show()

# endregion Example meshing code with nTop Core