Skip to content

Commit c8b2216

Browse files
authored
Release: Simple endian tests (#2512)
Even if we can't fully support other endian platforms, imports and exports should explicitly set endian-ness of outputs. - Adds a `make test-arch` which runs a subset of test on qemu/docker to catch simple problems on little-endian and 32bit platforms - Fixes #2511 - Fixes #2510 - Adds fix from #2506 which implemented `Line.closed` setter to auto-close polylines when passed. - Audit use of `np.einsum`/`np.dot` from good discussion in #2514. My conclusion was that `numpy.dot` is probably not the culprit and too core to avoid. As part of looking into this I benchmarked our other uses of `np.einsum` against simple numpy operations, it was on average ~2x slower. I also tried the `np.einsum(... optimize=True) as discussed in this [interesting post](https://dev.to/kylepena/investigating-the-performance-of-npeinsum-22ho) but for the trimesh use cases of very simple expressions this was substantially slower in every case I measured.
2 parents 5688bae + 7e01f5f commit c8b2216

File tree

16 files changed

+112
-35
lines changed

16 files changed

+112
-35
lines changed

.github/workflows/test.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ jobs:
5050
- name: Run Pytest In Docker
5151
run: make tests
5252

53+
arch:
54+
name: Architecture Tests (s390x + i386)
55+
runs-on: ubuntu-latest
56+
steps:
57+
- uses: actions/checkout@v4
58+
- uses: docker/setup-qemu-action@v3
59+
- name: Run Tests On s390x and i386
60+
run: make test-arch
61+
5362
corpus:
5463
runs-on: ubuntu-latest
5564
name: Check Corpus Loading

Makefile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,27 @@ docs: ## Build trimesh's sphinx docs
8080
bash: build ## Start a bash terminal in the image.
8181
docker run -it $(TAG_LATEST) /bin/bash
8282

83+
.PHONY: test-arch
84+
test-arch: ## Run tests on big-endian s390x and 32-bit i386 via QEMU emulation.
85+
docker run --rm --platform linux/s390x \
86+
-v $(CURDIR):/trimesh \
87+
-w /trimesh \
88+
debian:trixie-slim \
89+
bash -c "apt-get update -qq && apt-get install -y -qq \
90+
python3-numpy python3-pytest python3-pil python3-shapely python3-scipy python3-lxml \
91+
python3-networkx python3-jsonschema python3-httpx python3-collada python3-rtree \
92+
&& python3 -m pytest \
93+
tests/test_ply.py tests/test_gltf.py tests/test_voxel.py \
94+
tests/test_stl.py tests/test_dae.py tests/test_obj.py -v"
95+
docker run --rm --platform linux/386 \
96+
-v $(CURDIR):/trimesh \
97+
-w /trimesh \
98+
debian:trixie-slim \
99+
bash -c "apt-get update -qq && apt-get install -y -qq \
100+
python3-numpy python3-pytest python3-scipy python3-rtree \
101+
&& python3 -m pytest \
102+
tests/test_voxel.py -v"
103+
83104
.PHONY: publish-docker
84105
publish-docker: build ## Publish Docker images.
85106
docker push $(TAG_LATEST)

docs/requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
pypandoc==1.16.2
1+
pypandoc==1.17
22
recommonmark==0.7.1
33
jupyter==1.1.1
44

55
# get sphinx version range from furo install
66
furo==2025.12.19
77
myst-parser==5.0.0
8-
pyopenssl==25.3.0
8+
pyopenssl==26.0.0
99
autodocsumm==0.2.14
1010
jinja2==3.1.6
1111
matplotlib==3.10.8

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.11.3"
8+
version = "4.11.4"
99
authors = [{name = "Michael Dawson-Haggerty", email = "mikedh@kerfed.com"}]
1010
license = {file = "LICENSE.md"}
1111
description = "Import, export, process, analyze and view triangular meshes."

tests/test_entity.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
def test_line_closed():
2+
import numpy as np
3+
4+
from trimesh.path.entities import Line
5+
6+
e = Line(points=[0, 1, 2], closed=False)
7+
assert np.allclose(e.points, [0, 1, 2])
8+
9+
e = Line(points=[0, 1, 2], closed=True)
10+
assert np.allclose(e.points, [0, 1, 2, 0])
11+
12+
e = Line(points=[0, 1, 2, 0], closed=True)
13+
assert np.allclose(e.points, [0, 1, 2, 0])
14+
15+
# should it really drop the last point... that seems weird
16+
e = Line(points=[0, 1, 2, 0], closed=False)
17+
assert np.allclose(e.points, [0, 1, 2, 0])
18+
19+
20+
if __name__ == "__main__":
21+
import trimesh
22+
23+
trimesh.util.attach_to_log()
24+
test_line_closed()

trimesh/curvature.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import numpy as np
99

1010
from . import util
11+
from .util import diagonal_dot
1112

1213
try:
1314
from scipy.sparse import coo_matrix
@@ -153,9 +154,9 @@ def line_ball_intersection(start_points, end_points, center, radius):
153154
L = end_points - start_points
154155
oc = start_points - center # o-c
155156
r = radius
156-
ldotl = np.einsum("ij, ij->i", L, L) # l.l
157-
ldotoc = np.einsum("ij, ij->i", L, oc) # l.(o-c)
158-
ocdotoc = np.einsum("ij, ij->i", oc, oc) # (o-c).(o-c)
157+
ldotl = diagonal_dot(L, L)
158+
ldotoc = diagonal_dot(L, oc)
159+
ocdotoc = diagonal_dot(oc, oc)
159160
discrims = ldotoc**2 - ldotl * (ocdotoc - r**2)
160161

161162
# If discriminant is non-positive, then we have zero length

trimesh/exchange/gltf/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -963,9 +963,10 @@ def _append_mesh(
963963
# GLTF has no floating point type larger than 32 bits so clip
964964
# any float64 or larger to float32
965965
if attrib.dtype.kind == "f" and attrib.dtype.itemsize > 4:
966-
data = attrib.astype(np.float32)
966+
data = attrib.astype(float32)
967967
else:
968-
data = attrib
968+
# force little-endian to match GLTF binary format
969+
data = attrib.astype(attrib.dtype.newbyteorder("<"), copy=False)
969970

970971
if len(data.shape) == 1:
971972
data = data[:, np.newaxis]
@@ -1206,9 +1207,10 @@ def _append_path(path, name, tree, buffer_items):
12061207
# GLTF has no floating point type larger than 32 bits so clip
12071208
# any float64 or larger to float32
12081209
if attrib.dtype.kind == "f" and attrib.dtype.itemsize > 4:
1209-
data = attrib.astype(np.float32)
1210+
data = attrib.astype(float32)
12101211
else:
1211-
data = attrib
1212+
# force little-endian to match GLTF binary format
1213+
data = attrib.astype(attrib.dtype.newbyteorder("<"), copy=False)
12121214

12131215
if not all(util.is_instance_named(e, "Line") for e in path.entities):
12141216
log.warning(

trimesh/exchange/ply.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,12 @@ def _add_attributes_to_dtype(dtype, attributes):
150150
dtype : list of numpy datatypes
151151
"""
152152
for name, data in attributes.items():
153-
if data.ndim == 1:
154-
dtype.append((name, data.dtype))
153+
# force little-endian to match PLY binary format
154+
field_dtype = data.dtype.newbyteorder("<")
155+
if data.ndim > 1:
156+
dtype.extend([(f"{name}_count", "<u1"), (name, field_dtype, data.shape[1])])
155157
else:
156-
attribute_dtype = data.dtype if len(data.dtype) == 0 else data.dtype[0]
157-
dtype.append((f"{name}_count", "<u1"))
158-
dtype.append((name, attribute_dtype, data.shape[1]))
158+
dtype.append((name, field_dtype))
159159
return dtype
160160

161161

trimesh/intersections.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ def slice_faces_plane(
554554
denom[denom == 0.0] = 1e-12 # prevent division by zero
555555
dist = np.divide(num, denom)
556556
# intersection points for each segment
557-
int_points = np.einsum("ij,ijk->ijk", dist, d) + o
557+
int_points = dist[:, :, None] * d + o
558558

559559
# Initialize the array of new vertices with the current vertices
560560
new_vertices = vertices

trimesh/path/entities.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
from copy import deepcopy
10+
from logging import getLogger
1011

1112
import numpy as np
1213

@@ -15,6 +16,8 @@
1516
from .arc import arc_center, discretize_arc
1617
from .curve import discretize_bezier, discretize_bspline
1718

19+
log = getLogger(__name__)
20+
1821

1922
class Entity(ABC):
2023
def __init__(
@@ -105,8 +108,7 @@ def closed(self):
105108
closed : bool
106109
Is the entity closed or not?
107110
"""
108-
closed = len(self.points) > 2 and self.points[0] == self.points[-1]
109-
return closed
111+
return len(self.points) > 2 and self.points[0] == self.points[-1]
110112

111113
@property
112114
def nodes(self):
@@ -529,6 +531,21 @@ def discrete(self, vertices, scale=1.0):
529531
"""
530532
return self._orient(vertices[self.points])
531533

534+
@property
535+
def closed(self):
536+
return len(self.points) > 2 and self.points[0] == self.points[-1]
537+
538+
@closed.setter
539+
def closed(self, value: bool):
540+
current = self.points[0] == self.points[-1]
541+
if value and not current:
542+
# case where we've been asked to close the line
543+
# this seems pretty obvious that we should just append the first pointOB
544+
self.points = np.concatenate((self.points, [self.points[0]]))
545+
elif not value and current:
546+
# case where we've been asked to *disconnect* a closed path
547+
log.debug("ignoring `Line.closed = False`")
548+
532549
@property
533550
def is_valid(self):
534551
"""

0 commit comments

Comments
 (0)