11import tetgen
22import pymeshfix
3+ import subprocess
4+ import tempfile
5+ import os
6+ import sys
7+ from tqdm .auto import tqdm
8+ from itertools import cycle
9+ from time import sleep
10+ import time
11+ import numpy as np
12+ import pyvista as pv
313from svv .utils .remeshing import remesh
14+ import shutil
15+ import json
416
17+ filepath = os .path .abspath (__file__ )
18+ dirpath = os .path .dirname (filepath )
19+
20+ def format_elapsed (seconds : float ) -> str :
21+ seconds = int (seconds )
22+ m , s = divmod (seconds , 60 )
23+ h , m = divmod (m , 60 )
24+ if h > 0 :
25+ return f"{ h :02d} :{ m :02d} :{ s :02d} "
26+ else :
27+ return f"{ m :02d} :{ s :02d} "
528
629def triangulate (curve , verbose = False , ** kwargs ):
730 """
@@ -33,8 +56,16 @@ def triangulate(curve, verbose=False, **kwargs):
3356 vertices = mesh .faces .reshape (- 1 , 4 )[:, 1 :]
3457 return mesh , nodes , vertices
3558
59+ def _run_tetgen (surface_mesh ):
60+ tgen = tetgen .TetGen (surface_mesh )
61+ nodes , elems = tgen .tetrahedralize (verbose = 0 )
62+ return nodes , elems
3663
37- def tetrahedralize (surface_mesh , verbose = False , ** kwargs ):
64+ def tetrahedralize (surface : pv .PolyData ,
65+ * tet_args ,
66+ worker_script : str = dirpath + os .sep + "tetgen_worker.py" ,
67+ python_exe : str = sys .executable ,
68+ ** tet_kwargs ):
3869 """
3970 Tetrahedralize a surface mesh using TetGen.
4071
@@ -53,9 +84,111 @@ def tetrahedralize(surface_mesh, verbose=False, **kwargs):
5384 An unstructured grid mesh representing the tetrahedralized
5485 volume enclosed by the surface mesh manifold.
5586 """
56- mesh = pymeshfix .MeshFix (surface_mesh )
57- mesh .repair (verbose = verbose )
58- tet = tetgen .TetGen (mesh .mesh )
59- nodes , vertices = tet .tetrahedralize (** kwargs )
60- mesh = tet .grid
61- return mesh , nodes , vertices
87+ tet_kwargs .setdefault ("verbose" , 0 )
88+
89+ with tempfile .TemporaryDirectory () as tmpdir :
90+ surface_path = os .path .join (tmpdir , "surface.vtp" )
91+ out_path = os .path .join (tmpdir , "tet.npz" )
92+ config_path = os .path .join (tmpdir , "config.json" )
93+
94+ cfg = {
95+ "args" : list (tet_args ),
96+ "kwargs" : tet_kwargs ,
97+ }
98+ with open (config_path , "w" ) as f :
99+ json .dump (cfg , f )
100+
101+ # Save the surface mesh so the worker can read it
102+ surface .save (surface_path )
103+
104+ # Command: call the worker script as a separate Python process
105+ cmd = [python_exe , worker_script , surface_path , out_path , config_path ]
106+
107+ # Start the worker process
108+ proc = subprocess .Popen (
109+ cmd ,
110+ stdout = subprocess .PIPE ,
111+ stderr = subprocess .PIPE ,
112+ text = True , # decode to strings
113+ )
114+
115+ spinner = cycle (["⠋" , "⠙" , "⠹" , "⠸" , "⠼" ,
116+ "⠴" , "⠦" , "⠧" , "⠇" , "⠏" ])
117+ start_time = time .time ()
118+
119+ # Print label once
120+ sys .stdout .write ("TetGen meshing| " )
121+ sys .stdout .flush ()
122+
123+ # Live spinner loop
124+ while proc .poll () is None :
125+ # Compute elapsed time
126+ elapsed = time .time () - start_time
127+ elapsed_str = format_elapsed (elapsed )
128+
129+ # Build left side message
130+ spin_char = next (spinner )
131+ left = f"TetGen meshing| { spin_char } "
132+
133+ # Get terminal width (fallback if IDE doesn't report it)
134+ try :
135+ width = shutil .get_terminal_size (fallback = (80 , 20 )).columns
136+ except Exception :
137+ width = 80
138+
139+ # Compute spacing so elapsed time is right-aligned
140+ # We'll always keep at least one space between left and right
141+ min_gap = 1
142+ total_len = len (left ) + min_gap + len (elapsed_str )
143+ if total_len <= width :
144+ spaces = width - len (left ) - len (elapsed_str )
145+ else :
146+ # If line is longer than terminal, don't try to be clever; just put a single space
147+ spaces = min_gap
148+
149+ line = f"{ left } { ' ' * spaces } { elapsed_str } "
150+
151+ # '\r' to return to the start of the same line and overwrite
152+ sys .stdout .write ("\r " + line )
153+ sys .stdout .flush ()
154+
155+ time .sleep (0.1 )
156+
157+ # Finish line
158+ sys .stdout .write ("\n " )
159+ sys .stdout .flush ()
160+
161+ # Collect output (so the pipes don't hang)
162+ stdout , stderr = proc .communicate ()
163+
164+ if proc .returncode != 0 :
165+ raise RuntimeError (
166+ f"TetGen worker failed with code { proc .returncode } \n "
167+ f"STDOUT:\n { stdout } \n \n STDERR:\n { stderr } "
168+ )
169+
170+ # Load results
171+ data = np .load (out_path )
172+ nodes = data ["nodes" ]
173+ elems = data ["elems" ]
174+
175+ if elems .min () == 1 :
176+ elems = elems - 1
177+
178+ n_cells , n_vertices_per_cell = elems .shape
179+ cells = np .hstack (
180+ [
181+ np .full ((n_cells , 1 ), n_vertices_per_cell , dtype = np .int64 ),
182+ elems .astype (np .int64 ),
183+ ]
184+ ).ravel ()
185+ if n_vertices_per_cell == 4 :
186+ celltypes = np .full (n_cells , pv .CellType .TETRA , dtype = np .uint8 )
187+ elif n_vertices_per_cell == 10 :
188+ celltypes = np .full (n_cells , pv .CellType .QUADRATIC_TETRA , dtype = np .uint8 )
189+ else :
190+ raise ValueError (f"Unexpected number of vertices per cell: { n_vertices_per_cell } " )
191+
192+ grid = pv .UnstructuredGrid (cells , celltypes , nodes )
193+
194+ return grid , nodes , elems
0 commit comments