Source code for parq_blockmodel.mesh.types
"""Type definitions and dataclasses for mesh operations."""
from dataclasses import dataclass
from typing import Optional, Union
import numpy as np
from numpy.typing import NDArray
[docs]
@dataclass
class TriangleMesh:
"""Representation of a triangle mesh with optional attributes.
This is the internal mesh format used by the mesh module. It can be
serialized to PLY (lossless) or GLB (derived) formats.
Attributes
----------
vertices : np.ndarray
Vertex coordinates, shape (n_vertices, 3) in world space.
faces : np.ndarray
Triangle face indices, shape (n_faces, 3). Each row is a CCW-ordered
triangle when viewed from the exterior (right-handed coordinates).
vertex_attributes : dict[str, np.ndarray]
Per-vertex scalar attributes, e.g. {"grade": (n_vertices,), "density": (n_vertices,)}.
face_attributes : dict[str, np.ndarray]
Per-face scalar attributes, e.g. {"rock_type": (n_faces,)}.
vertex_ijk : Optional[np.ndarray]
Logical (i, j, k) indices for each vertex, shape (n_vertices, 3).
Useful for tracing back to block model.
face_ijk : Optional[np.ndarray]
Logical (i, j, k) indices for each face (typically the block containing it),
shape (n_faces, 3).
metadata : dict
Arbitrary metadata, e.g. CRS, units, author, etc.
"""
vertices: NDArray[np.floating]
faces: NDArray[np.intp]
vertex_attributes: dict[str, NDArray] = None
face_attributes: dict[str, NDArray] = None
vertex_ijk: Optional[NDArray[np.intp]] = None
face_ijk: Optional[NDArray[np.intp]] = None
metadata: Optional[dict] = None
def __post_init__(self):
"""Initialize optional dictionaries if None."""
if self.vertex_attributes is None:
self.vertex_attributes = {}
if self.face_attributes is None:
self.face_attributes = {}
if self.metadata is None:
self.metadata = {}
@property
def n_vertices(self) -> int:
"""Number of vertices."""
return len(self.vertices)
@property
def n_faces(self) -> int:
"""Number of faces (triangles)."""
return len(self.faces)
[docs]
def validate(self) -> None:
"""Validate mesh integrity.
Raises
------
ValueError
If mesh is malformed (e.g., vertices not 3D, faces have invalid indices).
"""
if self.vertices.ndim != 2 or self.vertices.shape[1] != 3:
raise ValueError("Vertices must have shape (n, 3).")
if self.faces.ndim != 2 or self.faces.shape[1] != 3:
raise ValueError("Faces must have shape (n, 3).")
if np.any(self.faces < 0) or np.any(self.faces >= self.n_vertices):
raise ValueError("Face indices must be in range [0, n_vertices).")
if self.vertex_ijk is not None:
if self.vertex_ijk.shape != (self.n_vertices, 3):
raise ValueError("vertex_ijk must have shape (n_vertices, 3).")
if self.face_ijk is not None:
if self.face_ijk.shape != (self.n_faces, 3):
raise ValueError("face_ijk must have shape (n_faces, 3).")
for name, attr in self.vertex_attributes.items():
if len(attr) != self.n_vertices:
raise ValueError(f"Vertex attribute '{name}' has wrong length ({len(attr)} vs {self.n_vertices}).")
for name, attr in self.face_attributes.items():
if len(attr) != self.n_faces:
raise ValueError(f"Face attribute '{name}' has wrong length ({len(attr)} vs {self.n_faces}).")
MeshMetadata = dict[str, Union[str, float, int, dict]]
"""Type alias for mesh metadata dictionaries."""