Skip to content

Commit

Permalink
feat: add PyVista support for loading and converting meshes
Browse files Browse the repository at this point in the history
- Added new PyVista module for loading and converting meshes
- Updated import statements in the `__init__.py` file to include PyVista functions
- Implemented functions to load and convert meshes using PyVista
- Introduced a new PyVista file for mesh conversion operations
  • Loading branch information
liblaf committed Apr 30, 2024
1 parent 7fe857c commit 6651f68
Show file tree
Hide file tree
Showing 16 changed files with 409 additions and 25 deletions.
11 changes: 10 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
{
"words": [
"astype",
"cotcurv",
"cuda",
"cython",
"dicom",
"disp",
"dmypy",
"dtype",
"fprint",
"gmsh",
"hstack",
"interp",
"intp",
"ipynb",
"ipython",
Expand All @@ -16,15 +21,18 @@
"liblaf",
"meshio",
"mkdocs",
"mkdocstrings",
"mkit",
"mypy",
"optim",
"pipenv",
"pixi",
"pybuilder",
"pycache",
"pydantic",
"pyenv",
"pyflow",
"pymdownx",
"pymeshfix",
"pypa",
"pypackages",
Expand All @@ -37,13 +45,14 @@
"pyvista",
"scrapy",
"sdist",
"smesh",
"taplo",
"trimesh",
"typer",
"venv",
"verts",
"vmap"
],
"ignorePaths": ["**/*-lock.*", "**/*.lock", "**/.cspell.json"],
"ignorePaths": ["**/*-lock.*", "**/*.lock*", "**/.cspell.json"],
"allowCompoundWords": true
}
2 changes: 1 addition & 1 deletion .envrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# shellcheck disable=SC2148
eval "$(pixi shell-hook --frozen)"
eval "$(pixi shell-hook)"
12 changes: 6 additions & 6 deletions pixi.lock
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/88/23/6398b7bca8967c853b90ba2f8da5e3ad1e9b2ca5b9f869a8c26ea41543e2/tifffile-2024.4.24-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/ef/52/032d3ad50cb168cb3fd033f8e0123e5cb1267442b580c02345341ae151fa/trimesh-4.3.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/57/7b/557a411622ff3e144d52bebbf8ad8a28bb18ee55ff68e1eb5ebfce755975/trimesh-4.3.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/38/51/055758e85029bf0dec2fd80cc314863e997429eedc5d5ff7b65c5c2e7108/typeguard-4.2.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/20/b5/11cf2e34fbb11b937e006286ab5b8cfd334fde1c8fa4dd7f491226931180/typer-0.12.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl
Expand Down Expand Up @@ -2290,7 +2290,7 @@ packages:
name: mesh-kit
version: 0.0.1
path: .
sha256: 41749a674efd1e232695f1781d9f9161ac77d58e0f233f778822c9389beec484
sha256: 838bfed318eee1e24985ea8838c1148b032e498a675ec77fd50bb1c61d2b06b7
requires_dist:
- loguru
- meshio
Expand Down Expand Up @@ -2336,7 +2336,7 @@ packages:
requires_dist:
- meshio
- numpy
requires_python: '>=3.7'
requires_python: '>=3.6'
- kind: pypi
name: mkdocs
version: 1.6.0
Expand Down Expand Up @@ -4148,9 +4148,9 @@ packages:
requires_python: '>=3.8'
- kind: pypi
name: trimesh
version: 4.3.1
url: https://files.pythonhosted.org/packages/ef/52/032d3ad50cb168cb3fd033f8e0123e5cb1267442b580c02345341ae151fa/trimesh-4.3.1-py3-none-any.whl
sha256: b3e53cf70dae39040d3d34f105eeff63e57c78cbc38e912d52a5f6f4f98fc70e
version: 4.3.2
url: https://files.pythonhosted.org/packages/57/7b/557a411622ff3e144d52bebbf8ad8a28bb18ee55ff68e1eb5ebfce755975/trimesh-4.3.2-py3-none-any.whl
sha256: 7563182a9379485b88a44e87156fe54b41fb6f8f030001b9b6de39abdef05c22
requires_dist:
- numpy>=1.20
- trimesh[easy,recommend,test] ; extra == 'all'
Expand Down
2 changes: 1 addition & 1 deletion src/mkit/array/mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ def vertex_to_face(
) -> npt.NDArray[np.bool_]:
faces = np.asarray(faces)
vert_mask = np.asarray(vert_mask)
return faces[vert_mask].all(axis=1)
return vert_mask[faces].all(axis=1)
4 changes: 4 additions & 0 deletions src/mkit/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
from mkit.io import _meshio
from mkit.io._meshio import as_meshio as as_meshio
from mkit.io._meshio import load_meshio as load_meshio
from mkit.io._pyvista import as_pyvista as as_pyvista
from mkit.io._pyvista import load_pyvista as load_pyvista
from mkit.io._trimesh import as_trimesh as as_trimesh
from mkit.io._trimesh import load_trimesh as load_trimesh
from mkit.io.types import AnyMesh
from mkit.typing import StrPath

__all__ = [
"as_meshio",
"as_pyvista",
"as_trimesh",
"load_meshio",
"load_pyvista",
"load_trimesh",
"save",
]
Expand Down
2 changes: 1 addition & 1 deletion src/mkit/io/_meshio.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
class Attrs(TypedDict, total=False):
point_data: dict[str, npt.ArrayLike] | None
cell_data: dict[str, list[npt.ArrayLike]] | None
field_data: None
field_data: dict[str, npt.ArrayLike] | None
point_sets: dict[str, npt.ArrayLike] | None
cell_sets: dict[str, list[npt.ArrayLike]] | None
gmsh_periodic: None
Expand Down
24 changes: 24 additions & 0 deletions src/mkit/io/_pyvista.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pyvista as pv
import trimesh

from mkit.io.types import AnyMesh
from mkit.typing import StrPath


def load_pyvista(filename: StrPath) -> pv.PolyData:
raise NotImplementedError # TODO


def as_pyvista(mesh: AnyMesh) -> pv.PolyData:
match mesh:
case pv.PolyData():
return mesh
case trimesh.Trimesh():
return trimesh_to_pyvista(mesh)
case _:
raise NotImplementedError(f"unsupported mesh: {mesh}")


def trimesh_to_pyvista(mesh: trimesh.Trimesh) -> pv.PolyData:
mesh_pv: pv.PolyData = pv.wrap(mesh)
return mesh_pv
12 changes: 9 additions & 3 deletions src/mkit/io/_trimesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,18 @@ def as_trimesh(mesh: AnyMesh) -> trimesh.Trimesh:
case pytorch3d.structures.Meshes():
raise NotImplementedError # TODO
case pv.PolyData():
raise NotImplementedError # TODO
return pyvista_to_trimesh(mesh)
case _:
raise NotImplementedError(f"unsupported mesh: {mesh}")


def meshio_to_trimesh(mesh: meshio.Mesh) -> trimesh.Trimesh:
vertices: npt.NDArray[np.float64] = mesh.points
verts: npt.NDArray[np.floating] = mesh.points
faces: npt.NDArray[np.integer] = mesh.get_cells_type("triangle")
return trimesh.Trimesh(vertices, faces)
return trimesh.Trimesh(verts, faces)


def pyvista_to_trimesh(mesh: pv.PolyData) -> trimesh.Trimesh:
verts: npt.NDArray[np.floating] = mesh.points
faces: npt.NDArray[np.integer] = mesh.regular_faces
return trimesh.Trimesh(verts, faces)
23 changes: 23 additions & 0 deletions src/mkit/ops/mesh_fix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import pymeshfix
import pyvista as pv
import trimesh

from mkit import io as _io


def mesh_fix(
mesh: trimesh.Trimesh,
*,
verbose: bool = False,
joincomp: bool = False,
remove_smallest_components: bool = True,
) -> trimesh.Trimesh:
mesh_pv: pv.PolyData = _io.as_pyvista(mesh)
fixer = pymeshfix.MeshFix(mesh_pv)
fixer.repair(
verbose=verbose,
joincomp=joincomp,
remove_smallest_components=remove_smallest_components,
)
mesh_pv = fixer.mesh
return _io.as_trimesh(mesh_pv)
24 changes: 24 additions & 0 deletions src/mkit/ops/ray.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import numpy as np
import trimesh
from numpy import typing as npt


def find_inner_point(
mesh: trimesh.Trimesh, *, max_retry: int = 8
) -> npt.NDArray[np.float64]:
for _ in range(max_retry):
origin: npt.NDArray[np.float64] = mesh.bounds[0]
end_point: npt.NDArray[np.float64] = mesh.sample(1, return_index=False)[0]
direction: npt.NDArray[np.float64] = end_point - origin
locations: npt.NDArray[np.float64]
index_ray: npt.NDArray[np.intp]
index_tri: npt.NDArray[np.intp]
locations, index_ray, index_tri = mesh.ray.intersects_location(
[origin], [direction]
)
if len(locations) < 2:
continue
result: npt.NDArray = (locations[0] + locations[1]) / 2
if mesh.contains([result])[0]:
return result
raise ValueError("Cannot find inner point")
132 changes: 132 additions & 0 deletions src/mkit/ops/tetgen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import functools
import pathlib
import subprocess
import tempfile
from collections.abc import Iterator
from typing import Any

import meshio
import numpy as np
from numpy import typing as npt

from mkit.typing import StrPath


def tetgen(mesh: meshio.Mesh) -> meshio.Mesh:
"""
Args:
mesh: input mesh
Returns:
tetrahedral mesh
"""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = pathlib.Path(tmpdir)
input_file: pathlib.Path = tmpdir / "mesh.smesh"
save_smesh(input_file, mesh)
subprocess.run(
["tetgen", "-p", "-q", "-O", "-z", "-k", "-C", "-V", input_file],
check=True,
)
tetra_mesh: meshio.Mesh = meshio.read(tmpdir / "mesh.1.vtk")
points: npt.NDArray[np.floating] = tetra_mesh.points
tetra: npt.NDArray[np.intp] = tetra_mesh.get_cells_type("tetra")
faces: npt.NDArray[np.intp]
boundary_marker: npt.NDArray[np.intp]
faces, boundary_marker = load_face(tmpdir / "mesh.1.face")
return meshio.Mesh(
points=points,
cells=[("tetra", tetra), ("triangle", faces)],
cell_data={
"boundary_marker": [np.zeros(len(tetra), np.intp), boundary_marker]
},
)


def load_face(
file: pathlib.Path,
) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]:
lines: list[str] = list(strip_comments(file))
# <# of faces> <boundary marker (0 or 1)>
num_faces, has_boundary_marker = map(int, lines[0].split())
faces: npt.NDArray[np.intp] = np.zeros((num_faces, 3), np.intp)
boundary_marker: npt.NDArray[np.intp] = np.zeros(num_faces, np.intp)
if has_boundary_marker:
for line in lines[1:]:
# <face #> <node> <node> <node> ... [boundary marker] ...
face_id, *face, marker = map(int, line.split())
faces[face_id] = face
boundary_marker[face_id] = marker
return faces, boundary_marker
else:
for line in lines[1:]:
# <face #> <node> <node> <node> ... [boundary marker] ...
face_id, *face = map(int, line.split())
faces[face_id] = face
return faces, boundary_marker


def save_smesh(file: StrPath, mesh: meshio.Mesh) -> None:
file = pathlib.Path(file)
with file.open("w") as f:
fprint = functools.partial(print, file=f)
fprint("# Part 1 - node list")
fprint(
"# <# of points> <dimension (3)> <# of attributes> <boundary markers (0 or 1)>"
)
if "boundary_marker" in mesh.point_data:
fprint(f"{len(mesh.points)} 3 0 1")
fprint("# <point #> <x> <y> <z> [attributes] [boundary marker]")
point_boundary_marker: npt.NDArray[np.intp] = np.asarray(
mesh.point_data["boundary_marker"]
)
for point_id, point in enumerate(mesh.points):
fprint(point_id, *point, point_boundary_marker[point_id])
else:
fprint(f"{len(mesh.points)} 3 0 0")
fprint("# <point #> <x> <y> <z> [attributes] [boundary marker]")
for point_id, point in enumerate(mesh.points):
fprint(point_id, *point)

fprint()
fprint("# Part 2 - facet list")
fprint("# <# of facets> <boundary markers (0 or 1)>")
faces: npt.NDArray[np.intp] = mesh.get_cells_type("triangle")
if "boundary_marker" in mesh.cell_data:
face_boundary_marker: npt.NDArray[np.intp] = mesh.get_cell_data(
"boundary_marker", "triangle"
)
fprint(len(faces), 1)
fprint("# <# of corners> <corner 1> ... <corner #> [boundary marker]")
for face_id, face in enumerate(faces):
fprint(len(face), *face, face_boundary_marker[face_id])
else:
fprint(len(faces), 0)
fprint("# <# of corners> <corner 1> ... <corner #> [boundary marker]")
for face in faces:
fprint(len(face), *face)

fprint()
fprint("# Part 3 - hole list")
fprint("# <# of holes>")
holes: npt.NDArray[np.float64] = mesh.field_data.get("holes", [])
fprint(len(holes))
fprint("# <hole #> <x> <y> <z>")
for hole_id, hole in enumerate(holes):
fprint(hole_id, *hole)

fprint()
fprint("# Part 4 - region attributes list")
fprint("# <# of region>")
fprint(0)


def strip_comments(file: pathlib.Path) -> Iterator[str]:
_: Any
with file.open() as f:
for line in f:
line: str
line, _, _ = line.partition("#")
line = line.strip()
if line:
yield line
Loading

0 comments on commit 6651f68

Please sign in to comment.