Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release: Fixes And Polygon Exit #2257

Merged
merged 16 commits into from
Aug 7, 2024
9 changes: 5 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ requires = ["setuptools >= 61.0", "wheel"]
[project]
name = "trimesh"
requires-python = ">=3.8"
version = "4.4.3"
version = "4.4.4"
authors = [{name = "Michael Dawson-Haggerty", email = "mikedh@kerfed.com"}]
license = {file = "LICENSE.md"}
description = "Import, export, process, analyze and view triangular meshes."
Expand Down Expand Up @@ -77,8 +77,10 @@ easy = [
"scipy",
"embreex; platform_machine=='x86_64'",
"pillow",
"vhacdx",
"xatlas",
"vhacdx; python_version>='3.9'",
# old versions of mapbox_earcut produce incorrect values on numpy 2.0
"mapbox_earcut >= 1.0.2; python_version>='3.9'",
]

recommend = [
Expand All @@ -91,7 +93,6 @@ recommend = [
"python-fcl", # do collision checks
"openctm", # load `CTM` compressed models
"cascadio", # load `STEP` files
"mapbox-earcut", # BLOCKED FOR NUMPY2

]

Expand All @@ -103,7 +104,7 @@ test = [
"pyright",
"ezdxf",
"pytest",
# "pymeshlab; python_version<='3.11'",
"pymeshlab; python_version<='3.11'",
"pyinstrument",
"matplotlib",
"ruff",
Expand Down
10 changes: 1 addition & 9 deletions tests/test_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,7 @@

class CreationTest(g.unittest.TestCase):
def setUp(self):
engines = []
if g.trimesh.util.has_module("triangle"):
engines.append("triangle")
if g.trimesh.util.has_module("mapbox_earcut"):
engines.append("earcut")
if g.trimesh.util.has_module("manifold3d"):
engines.append("manifold")

self.engines = engines
self.engines = [k for k, exists in g.trimesh.creation._engines if exists]

def test_box(self):
box = g.trimesh.creation.box
Expand Down
3 changes: 2 additions & 1 deletion tests/test_gltf.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,8 @@ def test_specular_glossiness(self):
)
assert metallic_roughness.shape[0] == 84 and metallic_roughness.shape[1] == 71

metallic = metallic_roughness[:, :, 0]
# https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#metallic-roughness-material
metallic = metallic_roughness[:, :, 2]
roughness = metallic_roughness[:, :, 1]

assert g.np.allclose(metallic[0, 0], 0.231, atol=0.004)
Expand Down
2 changes: 1 addition & 1 deletion trimesh/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2667,7 +2667,7 @@ def area_faces(self) -> NDArray[float64]:
area_faces : (n, ) float
Area of each face
"""
return triangles.area(crosses=self.triangles_cross, sum=False)
return triangles.area(crosses=self.triangles_cross)

@caching.cache_decorator
def mass_properties(self) -> MassProperties:
Expand Down
35 changes: 23 additions & 12 deletions trimesh/boolean.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import numpy as np

from . import exceptions, interfaces
from .typed import Callable, Iterable, Optional
from .typed import Callable, Optional, Sequence

try:
from manifold3d import Manifold, Mesh
Expand All @@ -18,7 +18,7 @@


def difference(
meshes: Iterable, engine: Optional[str] = None, check_volume: bool = True, **kwargs
meshes: Sequence, engine: Optional[str] = None, check_volume: bool = True, **kwargs
):
"""
Compute the boolean difference between a mesh an n other meshes.
Expand Down Expand Up @@ -48,7 +48,7 @@ def difference(


def union(
meshes: Iterable, engine: Optional[str] = None, check_volume: bool = True, **kwargs
meshes: Sequence, engine: Optional[str] = None, check_volume: bool = True, **kwargs
):
"""
Compute the boolean union between a mesh an n other meshes.
Expand Down Expand Up @@ -79,7 +79,7 @@ def union(


def intersection(
meshes: Iterable, engine: Optional[str] = None, check_volume: bool = True, **kwargs
meshes: Sequence, engine: Optional[str] = None, check_volume: bool = True, **kwargs
):
"""
Compute the boolean intersection between a mesh and other meshes.
Expand Down Expand Up @@ -108,7 +108,7 @@ def intersection(


def boolean_manifold(
meshes: Iterable,
meshes: Sequence,
operation: str,
check_volume: bool = True,
**kwargs,
Expand Down Expand Up @@ -165,13 +165,21 @@ def boolean_manifold(
return out_mesh


def reduce_cascade(operation: Callable, items: Iterable):
def reduce_cascade(operation: Callable, items: Sequence):
"""
Call a function in a cascaded pairwise way against a
flat sequence of items. This should produce the same
result as `functools.reduce` but may be faster for some
functions that for example perform only as fast as their
largest input.
Call an operation function in a cascaded pairwise way against a
flat list of items.

This should produce the same result as `functools.reduce`
if `operation` is commutable like addition or multiplication.
This will may be faster for an `operation` that runs
with a speed proportional to its largest input which mesh
booleans appear to. The union of a large number of small meshes
appears to be "much faster" using this method.

This only differs from `functools.reduce` for commutative `operation`
in that it returns `None` on empty inputs rather than `functools.reduce`
which raises a `TypeError`.

For example on `a b c d e f g` this function would run and return:
a b
Expand Down Expand Up @@ -200,8 +208,11 @@ def reduce_cascade(operation: Callable, items: Iterable):
"""
if len(items) == 0:
return None
elif len(items) == 1:
# skip the loop overhead for a single item
return items[0]
elif len(items) == 2:
# might as well skip the loop overhead
# skip the loop overhead for a single pair
return operation(items[0], items[1])

for _ in range(int(1 + np.log2(len(items)))):
Expand Down
2 changes: 1 addition & 1 deletion trimesh/convex.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def convex_hull(obj, qhull_options="QbB Pp Qt", repair=True):
crosses = crosses[valid]

# each triangle area and mean center
triangles_area = triangles.area(crosses=crosses, sum=False)
triangles_area = triangles.area(crosses=crosses)
triangles_center = vertices[faces].mean(axis=1)

# since the convex hull is (hopefully) convex, the vector from
Expand Down
1 change: 1 addition & 0 deletions trimesh/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

# get stored values for simple box and icosahedron primitives
_data = get_json("creation.json")
# check available triangulation engines without importing them
_engines = [
("earcut", util.has_module("mapbox_earcut")),
("manifold", util.has_module("manifold3d")),
Expand Down
2 changes: 1 addition & 1 deletion trimesh/parent.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ def apply_obb(self, **kwargs):

if tol.strict:
# obb transform should not have changed volume
if hasattr(self, "volume"):
if hasattr(self, "volume") and getattr(self, "is_watertight", False):
assert np.isclose(self.volume, volume)
# overall extents should match what we expected
assert np.allclose(self.extents, extents)
Expand Down
1 change: 1 addition & 0 deletions trimesh/path/exchange/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def polygon_to_path(polygon):
"entities": entities,
"vertices": np.vstack(vertices) if len(vertices) > 0 else vertices,
}

return kwargs


Expand Down
19 changes: 16 additions & 3 deletions trimesh/path/polygons.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ def enclosure_tree(polygons):
contained by another polygon
"""

# nodes are indexes in polygons
contains = nx.DiGraph()

if len(polygons) == 0:
return np.array([], dtype=np.int64), contains
elif len(polygons) == 1:
# add an early exit for only a single polygon
contains.add_node(0)
return np.array([0], dtype=np.int64), contains

# get the bounds for every valid polygon
bounds = {
k: v
Expand All @@ -59,8 +69,6 @@ def enclosure_tree(polygons):
if len(v) == 4
}

# nodes are indexes in polygons
contains = nx.DiGraph()
# make sure we don't have orphaned polygon
contains.add_nodes_from(bounds.keys())

Expand Down Expand Up @@ -551,13 +559,18 @@ def paths_to_polygons(paths, scale=None):
# non-zero area
continue
try:
polygons[i] = repair_invalid(Polygon(path), scale)
polygon = Polygon(path)
if polygon.is_valid:
polygons[i] = polygon
else:
polygons[i] = repair_invalid(polygon, scale)
except ValueError:
# raised if a polygon is unrecoverable
continue
except BaseException:
log.error("unrecoverable polygon", exc_info=True)
polygons = np.array(polygons)

return polygons


Expand Down
2 changes: 1 addition & 1 deletion trimesh/transformations.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ def rotation_matrix(angle, direction, point=None):
angle : float, or sympy.Symbol
Angle, in radians or symbolic angle
direction : (3,) float
Unit vector along rotation axis
Any vector along rotation axis
point : (3, ) float, or None
Origin point of rotation axis

Expand Down
30 changes: 17 additions & 13 deletions trimesh/triangles.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from .util import diagonal_dot, unitize


def cross(triangles):
def cross(triangles: NDArray) -> NDArray:
"""
Returns the cross product of two edges from input triangles

Expand All @@ -30,13 +30,19 @@ def cross(triangles):
crosses : (n, 3) float
Cross product of two edge vectors
"""
vectors = np.diff(triangles, axis=1)
crosses = np.cross(vectors[:, 0], vectors[:, 1])
vectors = triangles[:, 1:, :] - triangles[:, :2, :]
if triangles.shape[2] == 3:
return np.cross(vectors[:, 0], vectors[:, 1])
elif triangles.shape[2] == 2:
a = vectors[:, 0]
b = vectors[:, 1]
# numpy 2.0 deprecated 2D cross productes
return a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0]

return crosses
raise ValueError(triangles.shape)


def area(triangles=None, crosses=None, sum=False):
def area(triangles=None, crosses=None):
"""
Calculates the sum area of input triangles

Expand All @@ -55,11 +61,8 @@ def area(triangles=None, crosses=None, sum=False):
Individual or summed area depending on `sum` argument
"""
if crosses is None:
crosses = cross(triangles)
areas = np.sqrt((crosses**2).sum(axis=1)) / 2.0
if sum:
return areas.sum()
return areas
crosses = cross(np.asanyarray(triangles, dtype=np.float64))
return np.sqrt((crosses**2).sum(axis=1)) / 2.0


def normals(triangles=None, crosses=None):
Expand All @@ -80,6 +83,8 @@ def normals(triangles=None, crosses=None):
valid : (n,) bool
Was the face nonzero area or not
"""
if triangles is not None and triangles.shape[-1] == 2:
return np.tile([0.0, 0.0, 1.0], (triangles.shape[0], 1))
if crosses is None:
crosses = cross(triangles)
# unitize the cross product vectors
Expand Down Expand Up @@ -271,10 +276,9 @@ def mass_properties(
+ (triangles[:, 2, triangle_i] * g2[:, i])
)

coefficients = 1.0 / np.array(
integrated = integral.sum(axis=1) / np.array(
[6, 24, 24, 24, 60, 60, 60, 120, 120, 120], dtype=np.float64
)
integrated = integral.sum(axis=1) * coefficients

volume = integrated[0]

Expand Down Expand Up @@ -435,7 +439,7 @@ def extents(triangles, areas=None):
raise ValueError("Triangles must be (n, 3, 3)!")

if areas is None:
areas = area(triangles=triangles, sum=False)
areas = area(triangles=triangles)

# the edge vectors which define the triangle
a = triangles[:, 1] - triangles[:, 0]
Expand Down
6 changes: 2 additions & 4 deletions trimesh/typed.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
IO,
Any,
BinaryIO,
Callable,
Mapping,
Optional,
TextIO,
Union,
Expand All @@ -22,9 +20,9 @@
List = list
Tuple = tuple
Dict = dict
from collections.abc import Iterable, Sequence
from collections.abc import Callable, Iterable, Mapping, Sequence
else:
from typing import Dict, Iterable, List, Sequence, Tuple
from typing import Callable, Dict, Iterable, List, Mapping, Sequence, Tuple

# most loader routes take `file_obj` which can either be
# a file-like object or a file path, or sometimes a dict
Expand Down
2 changes: 1 addition & 1 deletion trimesh/visual/gloss.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ def convert_texture_lin2srgb(texture):
# we need to use RGB textures, because 2 channel textures can cause problems
result["metallicRoughnessTexture"] = toPIL(
np.concatenate(
[metallic, 1.0 - glossiness, np.zeros_like(metallic)], axis=-1
[np.zeros_like(metallic), 1.0 - glossiness, metallic], axis=-1
),
mode="RGB",
)
Expand Down
Loading