diff --git a/pyproject.toml b/pyproject.toml index 4d4bdc156..e3e7906f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = ["setuptools >= 61.0", "wheel"] [project] name = "trimesh" requires-python = ">=3.7" -version = "4.2.3" +version = "4.2.4" authors = [{name = "Michael Dawson-Haggerty", email = "mikedh@kerfed.com"}] license = {file = "LICENSE.md"} description = "Import, export, process, analyze and view triangular meshes." diff --git a/tests/test_primitives.py b/tests/test_primitives.py index 7983cf60d..af57b9100 100644 --- a/tests/test_primitives.py +++ b/tests/test_primitives.py @@ -330,6 +330,29 @@ def test_copy(self): assert g.np.allclose(box.center_mass, box_copy.center_mass) assert box.metadata["foo"] == box_copy.metadata["foo"] + def test_sphere_subdivisions(self): + # make sure we don't subdivide when asked not to + a = g.trimesh.creation.icosphere(subdivisions=0) + assert a.faces.shape == g.trimesh.creation.icosahedron().faces.shape + + # now apply the subdivisions ourself + a = a.subdivide().subdivide() + + # should have the same subdivisions as manually doing this + b = g.trimesh.primitives.Sphere(subdivisions=2) + + assert a.faces.shape == b.faces.shape + + def test_sphere_subdivisions_radius(self): + tf = list(g.random_transforms(4)) + for r in [1e-4, 1.1, 3, 1213.22]: + for subd in range(4): + s = g.trimesh.primitives.Sphere( + radius=r, transform=tf[subd], subdivisions=subd + ) + rc = g.np.linalg.norm(s.vertices - s.center_mass, axis=1) + assert g.np.isclose(rc.mean(), r) + if __name__ == "__main__": g.trimesh.util.attach_to_log() diff --git a/trimesh/base.py b/trimesh/base.py index ce6637325..0320530bf 100644 --- a/trimesh/base.py +++ b/trimesh/base.py @@ -1937,8 +1937,9 @@ def compute_stable_poses( def subdivide(self, face_index: Optional[ArrayLike] = None) -> "Trimesh": """ - Subdivide a mesh, with each subdivided face replaced - with four smaller faces. + Subdivide a mesh with each subdivided face replaced + with four smaller faces. Will return a copy of current + mesh with subdivided faces. Parameters ------------ diff --git a/trimesh/creation.py b/trimesh/creation.py index e7afcec23..e194efb17 100644 --- a/trimesh/creation.py +++ b/trimesh/creation.py @@ -855,9 +855,12 @@ def icosphere(subdivisions: Integer = 3, radius: Number = 1.0, **kwargs): ico : trimesh.Trimesh Meshed sphere """ + radius = float(radius) + subdivisions = int(subdivisions) ico = icosahedron() ico._validate = False + for _ in range(subdivisions): ico = ico.subdivide() vectors = ico.vertices @@ -865,6 +868,13 @@ def icosphere(subdivisions: Integer = 3, radius: Number = 1.0, **kwargs): unit = vectors / scalar.reshape((-1, 1)) ico.vertices += unit * (radius - scalar).reshape((-1, 1)) + # if we didn't subdivide we still need to refine the radius + if subdivisions <= 0: + vectors = ico.vertices + scalar = np.sqrt(np.dot(vectors**2, [1, 1, 1])) + unit = vectors / scalar.reshape((-1, 1)) + ico.vertices += unit * (radius - scalar).reshape((-1, 1)) + if "color" in kwargs: warnings.warn( "`icosphere(color=...)` is deprecated and will " diff --git a/trimesh/primitives.py b/trimesh/primitives.py index 64408d293..7718d9c7f 100644 --- a/trimesh/primitives.py +++ b/trimesh/primitives.py @@ -16,6 +16,7 @@ from . import transformations as tf from .base import Trimesh from .constants import log, tol +from .typed import ArrayLike, Integer, Number, Optional # immutable identity matrix for checks _IDENTITY = np.eye(4) @@ -57,7 +58,8 @@ def faces(self): @faces.setter def faces(self, values): - log.warning("primitive faces are immutable: not setting!") + if values is not None: + raise ValueError("primitive faces are immutable: not setting!") @property def vertices(self): @@ -71,7 +73,7 @@ def vertices(self): @vertices.setter def vertices(self, values): if values is not None: - log.warning("primitive vertices are immutable: not setting!") + raise ValueError("primitive vertices are immutable: not setting!") @property def face_normals(self): @@ -550,28 +552,32 @@ def _create_mesh(self): class Sphere(Primitive): def __init__( - self, radius=1.0, center=None, transform=None, subdivisions=3, mutable=True + self, + radius: Number = 1.0, + center: Optional[ArrayLike] = None, + transform: Optional[ArrayLike] = None, + subdivisions: Integer = 3, + mutable: bool = True, ): """ Create a Sphere Primitive, a subclass of Trimesh. Parameters ---------- - radius : float + radius Radius of sphere center : None or (3,) float Center of sphere. transform : None or (4, 4) float Full homogeneous transform. Pass `center` OR `transform. - subdivisions : int + subdivisions Number of subdivisions for icosphere. - mutable : bool + mutable Are extents and transform mutable after creation. """ super().__init__() - defaults = {"radius": 1.0, "transform": np.eye(4), "subdivisions": 3} constructor = {"radius": float(radius), "subdivisions": int(subdivisions)} # center is a helper method for "transform" # since a sphere is rotationally symmetric @@ -586,7 +592,10 @@ def __init__( # create the attributes object self.primitive = PrimitiveAttributes( - self, defaults=defaults, kwargs=constructor, mutable=mutable + self, + defaults={"radius": 1.0, "transform": np.eye(4), "subdivisions": 3}, + kwargs=constructor, + mutable=mutable, ) @property