11use numpy:: prelude:: * ;
22use numpy:: { Element , PyArray3 , PyUntypedArray } ;
3+ use pyo3:: IntoPyObjectExt ;
34use pyo3:: prelude:: * ;
45use pyo3_stub_gen:: derive:: * ;
56use splashsurf_lib:: nalgebra:: Vector3 ;
@@ -53,24 +54,47 @@ pub fn check_mesh_consistency<'py>(
5354}
5455
5556/// Performs a standard marching cubes triangulation of a 3D array of values
57+ ///
58+ /// The array of values has to be a contiguous array with shape ``(nx, ny, nz)``.
59+ /// The iso-surface threshold defines which value is considered to be "on" the surface.
60+ /// The cube size and translation parameters define the scaling and translation of the resulting
61+ /// mesh. Without translation, the value ``values[0, 0, 0]`` is located at coordinates ``(0, 0, 0)``.
62+ ///
63+ /// The values are interpreted as a "density field", meaning that values higher than the iso-surface
64+ /// threshold are considered to be "inside" the surface and values lower than the threshold are
65+ /// considered to be "outside" the surface. This is the opposite convention to an SDF (signed distance field).
66+ /// However, even if values of an SDF are provided as an input, the marching cubes algorithm
67+ /// will still work and produce a watertight surface mesh (if the surface is fully contained in the
68+ /// array).
69+ ///
70+ /// If ``return_grid`` is set to ``True``, the function will return a tuple of the mesh and the
71+ /// uniform grid that was used for the triangulation. This can be used for other functions such as
72+ /// :py:func:`check_mesh_consistency`. Otherwise, only the mesh is returned.
73+ ///
74+ /// The function is currently single-threaded. The SPH surface reconstruction functions :py:func:`reconstruction_pipeline`
75+ /// and :py:func:`reconstruct_surface` improve performance by processing multiple patches in parallel.
5676#[ gen_stub_pyfunction]
5777#[ pyfunction]
5878#[ pyo3( name = "marching_cubes" ) ]
59- #[ pyo3( signature = ( values, * , cube_size, iso_surface_threshold, translation = None ) ) ]
79+ #[ pyo3( signature = ( values, * , iso_surface_threshold, cube_size, translation = None , return_grid = false ) ) ]
80+ #[ gen_stub( override_return_type( type_repr="typing.Union[TriMesh3d, tuple[TriMesh3d, UniformGrid]]" , imports=( ) ) ) ]
6081pub fn marching_cubes < ' py > (
6182 values : & Bound < ' py , PyUntypedArray > ,
62- cube_size : f64 ,
6383 iso_surface_threshold : f64 ,
84+ cube_size : f64 ,
6485 translation : Option < [ f64 ; 3 ] > ,
65- ) -> PyResult < ( PyTriMesh3d , PyUniformGrid ) > {
86+ return_grid : bool ,
87+ ) -> PyResult < Py < PyAny > > {
6688 assert_eq ! ( values. shape( ) . len( ) , 3 , "values must be a 3D array" ) ;
6789
6890 fn triangulate_density_map_generic < ' py , R : Real + Element > (
6991 values : & Bound < ' py , PyArray3 < R > > ,
70- cube_size : R ,
7192 iso_surface_threshold : R ,
93+ cube_size : R ,
7294 translation : Option < [ R ; 3 ] > ,
73- ) -> PyResult < ( PyTriMesh3d , PyUniformGrid ) > {
95+ return_grid : bool ,
96+ ) -> PyResult < Py < PyAny > > {
97+ let py = values. py ( ) ;
7498 let shape = values. shape ( ) ;
7599 let translation = Vector3 :: from ( translation. unwrap_or ( [ R :: zero ( ) ; 3 ] ) ) ;
76100 let n_cells_per_dim = [
@@ -82,31 +106,42 @@ pub fn marching_cubes<'py>(
82106 let grid = UniformGrid :: new ( & translation, & n_cells_per_dim, cube_size)
83107 . map_err ( anyhow:: Error :: from) ?;
84108
85- // TODO: Replace with borrow
86- let values = values. try_readonly ( ) ?. as_slice ( ) ?. to_vec ( ) ;
87- let density_map = DensityMap :: from ( values) ;
109+ let values = values. try_readonly ( ) ?;
110+ let density_map = DensityMap :: from ( values. as_slice ( ) ?) ;
88111
89112 let mesh = splashsurf_lib:: marching_cubes:: triangulate_density_map (
90113 & grid,
91114 & density_map,
92115 iso_surface_threshold,
93116 )
94117 . map_err ( anyhow:: Error :: from) ?;
95- Ok ( (
96- PyTriMesh3d :: try_from_generic ( mesh) ?,
97- PyUniformGrid :: try_from_generic ( grid) ?,
98- ) )
118+
119+ let mesh = PyTriMesh3d :: try_from_generic ( mesh) ?;
120+ let grid = PyUniformGrid :: try_from_generic ( grid) ?;
121+
122+ if return_grid {
123+ ( mesh, grid) . into_py_any ( py)
124+ } else {
125+ mesh. into_py_any ( py)
126+ }
99127 }
100128
101129 if let Ok ( values) = values. downcast :: < PyArray3 < f32 > > ( ) {
102130 triangulate_density_map_generic (
103131 & values,
104- cube_size as f32 ,
105132 iso_surface_threshold as f32 ,
133+ cube_size as f32 ,
106134 translation. map ( |t| t. map ( |t| t as f32 ) ) ,
135+ return_grid,
107136 )
108137 } else if let Ok ( values) = values. downcast :: < PyArray3 < f64 > > ( ) {
109- triangulate_density_map_generic ( & values, cube_size, iso_surface_threshold, translation)
138+ triangulate_density_map_generic (
139+ & values,
140+ iso_surface_threshold,
141+ cube_size,
142+ translation,
143+ return_grid,
144+ )
110145 } else {
111146 Err ( utils:: pyerr_unsupported_scalar ( ) )
112147 }
0 commit comments