Skip to content

Commit

Permalink
Merge pull request #2257 from mikedh/tri/2d
Browse files Browse the repository at this point in the history
Release: Fixes And Polygon Exit
  • Loading branch information
mikedh authored Aug 7, 2024
2 parents 85b4bd1 + c107391 commit 0166cdb
Show file tree
Hide file tree
Showing 14 changed files with 73 additions and 51 deletions.
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

0 comments on commit 0166cdb

Please sign in to comment.