Skip to content

Commit d7c9985

Browse files
authored
Release: Projected Precise Mode (#2491)
- release #2490 as discussed in #2488 doing minor cleanup below the precision threshold before unioning every triangle in `projected(... precise=True)` - release #2487 cleaning up the logic in `trimesh.grouping.group_distance` to be more coherent.
2 parents ffe3c35 + c85b2bb commit d7c9985

File tree

7 files changed

+71
-52
lines changed

7 files changed

+71
-52
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ requires = ["setuptools >= 61.0", "wheel"]
55
[project]
66
name = "trimesh"
77
requires-python = ">=3.8"
8-
version = "4.10.0"
8+
version = "4.10.1"
99
authors = [{name = "Michael Dawson-Haggerty", email = "[email protected]"}]
1010
license = {file = "LICENSE.md"}
1111
description = "Import, export, process, analyze and view triangular meshes."

tests/test_polygons.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,15 @@ def test_project_backface():
117117

118118

119119
def test_project_multi():
120-
mesh = g.trimesh.creation.box() + g.trimesh.creation.box().apply_translation(
121-
[3, 0, 0]
122-
)
123-
proj = mesh.projected(normal=[0, 0, 1])
120+
for ignore_sign, precise in g.itertools.combinations([True, False] * 2, 2):
121+
mesh = g.trimesh.creation.box() + g.trimesh.creation.box().apply_translation(
122+
[3, 0, 0]
123+
)
124+
proj = mesh.projected(normal=[0, 0, 1], ignore_sign=ignore_sign, precise=precise)
124125

125-
assert mesh.body_count == 2
126-
assert len(proj.root) == 2
127-
assert g.np.isclose(proj.area, 2.0)
126+
assert mesh.body_count == 2
127+
assert len(proj.root) == 2
128+
assert g.np.isclose(proj.area, 2.0)
128129

129130

130131
def test_second_moment():

trimesh/base.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2689,20 +2689,28 @@ def projected(self, normal: ArrayLike, **kwargs) -> Path2D:
26892689
26902690
Parameters
26912691
----------
2692-
check : bool
2693-
If True make sure is flat
26942692
normal : (3,) float
26952693
Normal to extract flat pattern along
26962694
origin : None or (3,) float
26972695
Origin of plane to project mesh onto
2698-
pad : float
2696+
ignore_sign : bool
2697+
Allow a projection from the normal vector in
2698+
either direction: this provides a substantial speedup
2699+
on watertight meshes where the direction is irrelevant
2700+
but if you have a triangle soup and want to discard
2701+
backfaces you should set this to False.
2702+
rpad : float
26992703
Proportion to pad polygons by before unioning
27002704
and then de-padding result by to avoid zero-width gaps.
2705+
apad : float
2706+
Absolute padding to pad polygons by before unioning
2707+
and then de-padding result by to avoid zero-width gaps.
27012708
tol_dot : float
27022709
Tolerance for discarding on-edge triangles.
2703-
max_regions : int
2704-
Raise an exception if the mesh has more than this
2705-
number of disconnected regions to fail quickly before unioning.
2710+
precise : bool
2711+
Use the precise projection computation using shapely.
2712+
precise_eps : float
2713+
Tolerance for precise triangle checks.
27062714
27072715
Returns
27082716
----------

trimesh/exchange/stl.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import numpy as np
22

33
from .. import util
4+
from ..typed import Dict, Stream
45

56

67
class HeaderError(Exception):
@@ -19,22 +20,20 @@ class HeaderError(Exception):
1920
_stl_dtype_header = np.dtype([("header", np.void, 80), ("face_count", "<u4")])
2021

2122

22-
def load_stl(file_obj, **kwargs):
23+
def load_stl(file_obj: Stream, **kwargs) -> Dict:
2324
"""
24-
Load an STL file from a file object.
25+
Load a binary or an ASCII STL file from a file object.
2526
2627
Parameters
2728
----------
28-
file_obj : open file-like object
29+
file_obj
2930
Containing STL data
3031
3132
Returns
3233
----------
33-
loaded : dict
34-
kwargs for a Trimesh constructor with keys:
35-
vertices: (n,3) float, vertices
36-
faces: (m,3) int, indexes of vertices
37-
face_normals: (m,3) float, normal vector of each face
34+
loaded
35+
Keyword arguments for a Trimesh constructor with
36+
data loaded into properly shaped numpy arrays.
3837
"""
3938
# save start of file obj
4039
file_pos = file_obj.tell()
@@ -53,7 +52,7 @@ def load_stl(file_obj, **kwargs):
5352
return load_stl_ascii(file_obj)
5453

5554

56-
def load_stl_binary(file_obj):
55+
def load_stl_binary(file_obj: Stream) -> Dict:
5756
"""
5857
Load a binary STL file from a file object.
5958
@@ -64,10 +63,9 @@ def load_stl_binary(file_obj):
6463
6564
Returns
6665
----------
67-
loaded: kwargs for a Trimesh constructor with keys:
68-
vertices: (n,3) float, vertices
69-
faces: (m,3) int, indexes of vertices
70-
face_normals: (m,3) float, normal vector of each face
66+
loaded
67+
Keyword arguments for a Trimesh constructor with data
68+
loaded into properly shaped numpy arrays.
7169
"""
7270
# the header is always 84 bytes long, we just reference the dtype.itemsize
7371
# to be explicit about where that magical number comes from
@@ -136,7 +134,7 @@ def load_stl_binary(file_obj):
136134
return result
137135

138136

139-
def load_stl_ascii(file_obj):
137+
def load_stl_ascii(file_obj: Stream) -> Dict:
140138
"""
141139
Load an ASCII STL file from a file object.
142140
@@ -147,11 +145,9 @@ def load_stl_ascii(file_obj):
147145
148146
Returns
149147
----------
150-
loaded : dict
151-
kwargs for a Trimesh constructor with keys:
152-
vertices: (n, 3) float, vertices
153-
faces: (m, 3) int, indexes of vertices
154-
face_normals: (m, 3) float, normal vector of each face
148+
loaded
149+
Keyword arguments for a Trimesh constructor with
150+
data loaded into properly shaped numpy arrays.
155151
"""
156152

157153
# read all text into one string

trimesh/grouping.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from . import util
1111
from .constants import log, tol
12-
from .typed import ArrayLike, Integer, NDArray, Optional
12+
from .typed import ArrayLike, Integer, NDArray, Number, Optional, Sequence, Tuple
1313

1414
try:
1515
from scipy.spatial import cKDTree
@@ -666,14 +666,16 @@ def group_vectors(vectors, angle=1e-4, include_negative=False):
666666
return new_vectors, groups
667667

668668

669-
def group_distance(values, distance):
669+
def group_distance(
670+
values: ArrayLike, distance: Number
671+
) -> Tuple[NDArray[np.float64], Sequence]:
670672
"""
671-
Find groups of points which have neighbours closer than radius,
672-
where no two points in a group are farther than distance apart.
673+
Find non-overlapping groups of points where no two points in a
674+
group are farther than 2*distance apart.
673675
674676
Parameters
675677
---------
676-
points : (n, d) float
678+
values : (n, d) float
677679
Points of dimension d
678680
distance : float
679681
Max distance between points in a cluster
@@ -700,6 +702,7 @@ def group_distance(values, distance):
700702
if consumed[index]:
701703
continue
702704
group = np.array(tree.query_ball_point(value, distance), dtype=np.int64)
705+
group = group[~consumed[group]]
703706
consumed[group] = True
704707
unique.append(np.median(values[group], axis=0))
705708
groups.append(group)

trimesh/path/polygons.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,7 @@ def projected(
735735
apad=None,
736736
tol_dot=1e-10,
737737
precise: bool = False,
738+
precise_eps: float = 1e-10,
738739
):
739740
"""
740741
Project a mesh onto a plane and then extract the polygon
@@ -753,8 +754,6 @@ def projected(
753754
----------
754755
mesh : trimesh.Trimesh
755756
Source geometry
756-
check : bool
757-
If True make sure is flat
758757
normal : (3,) float
759758
Normal to extract flat pattern along
760759
origin : None or (3,) float
@@ -773,10 +772,10 @@ def projected(
773772
and then de-padding result by to avoid zero-width gaps.
774773
tol_dot : float
775774
Tolerance for discarding on-edge triangles.
776-
max_regions : int
777-
Raise an exception if the mesh has more than this
778-
number of disconnected regions to fail quickly before
779-
unioning.
775+
precise : bool
776+
Use the precise projection computation using shapely.
777+
precise_eps : float
778+
Tolerance for precise triangle checks.
780779
781780
Returns
782781
----------
@@ -829,15 +828,28 @@ def projected(
829828
vertices_2D = transform_points(mesh.vertices, to_2D)[:, :2]
830829

831830
if precise:
832-
eps = 1e-10
831+
# precise mode just unions triangles as one shapely
832+
# polygon per triangle which historically has been very slow
833+
# but it is more defensible intellectually
833834
faces = mesh.faces[side]
834-
# just union all the polygons
835+
# round the 2D vertices with slightly more precision
836+
# than our final dilate-erode cleanup
837+
digits = int(np.abs(np.log10(precise_eps)) + 2)
838+
rounded = np.round(vertices_2D, digits)
839+
# get the triangles as closed 4-vertex polygons
840+
triangles = rounded[np.column_stack((faces, faces[:, :1]))]
841+
# do a check for exactly-degenerate triangles where any two
842+
# vertices are exactly identical which means the triangle has
843+
# zero area
844+
valid = ~(triangles[:, [0, 0, 2]] == triangles[:, [1, 2, 1]]).all(axis=2).any(
845+
axis=1
846+
)
847+
# union the valid triangles and then dilate-erode to clean up
848+
# any holes or defects smaller than precise_eps
835849
return (
836-
ops.unary_union(
837-
[Polygon(f) for f in vertices_2D[np.column_stack((faces, faces[:, :1]))]]
838-
)
839-
.buffer(eps)
840-
.buffer(-eps)
850+
ops.unary_union([Polygon(f) for f in triangles[valid]])
851+
.buffer(precise_eps)
852+
.buffer(-precise_eps)
841853
)
842854

843855
# a sequence of face indexes that are connected

trimesh/typed.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343

4444
# most loader routes take `file_obj` which can either be
4545
# a file-like object or a file path, or sometimes a dict
46-
4746
Stream = Union[IO, BytesIO, StringIO, BinaryIO, TextIO, BufferedRandom]
4847
Loadable = Union[str, Path, Stream, Dict, None]
4948

0 commit comments

Comments
 (0)