Skip to content

Commit fd71c49

Browse files
authored
Release: Box Normals (#2371)
- Face normals were being recomputed unnecessarily for `Box` primitives. - release #2367 - update docker images to Python 3.13 as Debian trixie removed 3.12.
2 parents 9667bc6 + ccae0ed commit fd71c49

File tree

9 files changed

+103
-17
lines changed

9 files changed

+103
-17
lines changed

Dockerfile

+11-8
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ LABEL maintainer="[email protected]"
55
# Create a non-root user with `uid=499`.
66
RUN useradd -m -u 499 -s /bin/bash user && \
77
apt-get update && \
8-
apt-get install --no-install-recommends -qq -y python3.12-venv && \
9-
apt-get clean -y
8+
apt-get install --no-install-recommends -qq -y python3.13-venv && \
9+
apt-get clean -y && rm -rf /var/lib/apt/lists/*
1010

1111
USER user
1212

@@ -17,7 +17,7 @@ WORKDIR /home/user
1717
# but if you use Debian methods like `update-alternatives`
1818
# it won't provide a `pip` which works easily and it isn't
1919
# easy to know how system packages interact with pip packages
20-
RUN python3.12 -m venv venv
20+
RUN python3.13 -m venv venv
2121

2222
# So scripts installed from pip are in $PATH
2323
ENV PATH="/home/user/venv/bin:$PATH"
@@ -30,17 +30,20 @@ COPY --chmod=755 docker/trimesh-setup /home/user/venv/bin
3030
## install things that need building
3131
FROM base AS build
3232

33+
USER root
34+
# `xatlas` currently needs to compile on 3.13 from the sdist
35+
RUN apt-get update && \
36+
apt-get install --no-install-recommends -y python3.13-dev build-essential g++ && \
37+
apt-get clean -y && rm -rf /var/lib/apt/lists/*
38+
USER user
39+
3340
# copy in essential files
3441
COPY --chown=499 trimesh/ /home/user/trimesh
3542
COPY --chown=499 pyproject.toml /home/user/
3643

3744
# install trimesh into the venv
3845
RUN pip install /home/user[easy]
3946

40-
# install FCL from a hopefully temporary fork
41-
# as the original `python-fcl` currently has broken wheels on PyPi
42-
RUN pip install fclx
43-
4447
####################################
4548
### Build output image most things should run on
4649
FROM base AS output
@@ -62,7 +65,7 @@ COPY --chown=499 pyproject.toml .
6265
COPY --chown=499 ./.git ./.git/
6366

6467
USER root
65-
RUN trimesh-setup --install=test,gmsh,gltf_validator,llvmpipe,binvox,blender
68+
RUN trimesh-setup --install=test,gmsh,gltf_validator,llvmpipe,binvox,blender,build
6669
USER user
6770

6871
RUN blender --version

docker/trimesh-setup

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ config_json = """
2525
"apt": {
2626
"build": [
2727
"build-essential",
28+
"python3.13-dev",
2829
"g++",
2930
"make",
3031
"git"

pyproject.toml

+2-2
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.6.4"
8+
version = "4.6.5"
99
authors = [{name = "Michael Dawson-Haggerty", email = "[email protected]"}]
1010
license = {file = "LICENSE.md"}
1111
description = "Import, export, process, analyze and view triangular meshes."
@@ -94,7 +94,7 @@ recommend = [
9494
"psutil",
9595
"scikit-image",
9696
"fast-simplification",
97-
# "python-fcl", # do collision checks # TODO : broken on numpy 2
97+
"python-fcl", # do collision checks
9898
"openctm; platform_machine=='x86_64'", # load `CTM` compressed models
9999
"cascadio", # load `STEP` files
100100
]

tests/generic.py

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import subprocess
2727
import collections
2828
import numpy as np
29+
import pytest
2930

3031
import trimesh
3132

tests/test_paths.py

+59-1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,62 @@ def test_path():
131131
assert len(loaded_path.vertex_attributes["_test"]) == 3
132132

133133

134+
def test_path_to_gltf_with_line():
135+
path = g.trimesh.path.Path3D(
136+
entities=[
137+
g.trimesh.path.entities.Line(points=[0, 1, 2]),
138+
g.trimesh.path.entities.Line(points=[3, 4, 5]),
139+
],
140+
vertices=g.np.array(
141+
[
142+
[0, 0, 0],
143+
[0, 1, 0],
144+
[1, 1, 0],
145+
[0, 0, 1],
146+
[0, 1, 1],
147+
[1, 1, 2],
148+
]
149+
),
150+
)
151+
path.vertex_attributes = {"_test": g.np.array([0, 0, 0, 1, 1, 1], dtype=g.np.float32)}
152+
153+
tree, _ = g.trimesh.exchange.gltf._create_gltf_structure(g.trimesh.Scene([path]))
154+
# should have included the attribute by name
155+
assert set(tree["meshes"][0]["primitives"][0]["attributes"].keys()) == {
156+
"POSITION",
157+
"_test",
158+
}
159+
160+
assert len(tree["accessors"]) == 2
161+
acc = tree["accessors"]
162+
assert acc[0]["count"] == acc[1]["count"]
163+
164+
165+
def test_path_to_gltf_with_arc():
166+
path = g.trimesh.path.Path3D(
167+
entities=[
168+
g.trimesh.path.entities.Line(points=[0, 1, 2]),
169+
g.trimesh.path.entities.Arc(points=[3, 4, 5]),
170+
],
171+
vertices=g.np.array(
172+
[
173+
[0, 0, 0],
174+
[0, 1, 0],
175+
[1, 1, 0],
176+
[0, 0, 1],
177+
[0, 1, 1],
178+
[1, 1, 2],
179+
]
180+
),
181+
)
182+
path.vertex_attributes = {"_test": g.np.array([0, 0, 0, 1, 1, 1], dtype=g.np.float32)}
183+
184+
tree, _ = g.trimesh.exchange.gltf._create_gltf_structure(g.trimesh.Scene([path]))
185+
186+
# should have skipped the attribute rather than exporting a broken one
187+
assert set(tree["meshes"][0]["primitives"][0]["attributes"].keys()) == {"POSITION"}
188+
189+
134190
def test_poly():
135191
p = g.get_mesh("2D/LM2.dxf")
136192
assert p.is_closed
@@ -357,4 +413,6 @@ def test_svg_arc_snap():
357413

358414
if __name__ == "__main__":
359415
g.trimesh.util.attach_to_log()
360-
g.unittest.main()
416+
# g.unittest.main()
417+
418+
test_path_to_gltf_with_arc()

tests/test_scenegraph.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88

99
def random_chr():
10-
return chr(ord("a") + int(round(g.random() * 25)))
10+
return chr(ord("a") + round(g.random() * 25))
1111

1212

1313
def test_forest():

trimesh/base.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@
7979
Path2D = ExceptionWrapper(E)
8080
Path3D = ExceptionWrapper(E)
8181

82+
# save immutable identity matrices for checks
83+
_IDENTITY3 = np.eye(3, dtype=np.float64)
84+
_IDENTITY3.flags.writeable = False
85+
_IDENTITY4 = np.eye(4, dtype=np.float64)
86+
_IDENTITY4.flags.writeable = False
87+
8288

8389
class Trimesh(Geometry3D):
8490
def __init__(
@@ -2446,15 +2452,15 @@ def apply_transform(self, matrix: ArrayLike) -> "Trimesh":
24462452

24472453
# exit early if we've been passed an identity matrix
24482454
# np.allclose is surprisingly slow so do this test
2449-
elif util.allclose(matrix, np.eye(4), 1e-8):
2455+
elif util.allclose(matrix, _IDENTITY4, 1e-8):
24502456
return self
24512457

24522458
# new vertex positions
24532459
new_vertices = transformations.transform_points(self.vertices, matrix=matrix)
24542460

24552461
# check to see if the matrix has rotation
24562462
# rather than just translation
2457-
has_rotation = not util.allclose(matrix[:3, :3], np.eye(3), atol=1e-6)
2463+
has_rotation = not util.allclose(matrix[:3, :3], _IDENTITY3, atol=1e-6)
24582464

24592465
# transform overridden center of mass
24602466
if "center_mass" in self._data:

trimesh/exchange/gltf.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -1180,14 +1180,23 @@ def _append_path(path, name, tree, buffer_items):
11801180
else:
11811181
data = attrib
11821182

1183-
data = util.stack_lines(data).reshape((-1,))
1183+
if not all(util.is_instance_named(e, "Line") for e in path.entities):
1184+
log.warning(
1185+
f"Vertex attributes are only supported for Line entities, skipping `{key}`"
1186+
)
1187+
continue
1188+
1189+
data_discretized = np.array(
1190+
[util.stack_lines(e.discrete(data)) for e in path.entities]
1191+
)
1192+
stacked_data = data_discretized.reshape((-1,))
11841193

11851194
# store custom vertex attributes
11861195
current["primitives"][0]["attributes"][key] = _data_append(
11871196
acc=tree["accessors"],
11881197
buff=buffer_items,
1189-
blob=_build_accessor(data),
1190-
data=data,
1198+
blob=_build_accessor(stacked_data),
1199+
data=stacked_data,
11911200
)
11921201

11931202
tree["meshes"].append(current)

trimesh/primitives.py

+8
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,19 @@ def vertices(self, values):
7777

7878
@property
7979
def face_normals(self):
80+
# if the mesh hasn't been created yet do that
81+
# before checking to see if the mesh creation
82+
# already populated the face normals
83+
if "vertices" not in self._cache:
84+
self._create_mesh()
85+
8086
# we need to avoid the logic in the superclass that
8187
# is specific to the data model prioritizing faces
8288
stored = self._cache["face_normals"]
8389
if util.is_shape(stored, (-1, 3)):
8490
return stored
91+
92+
# if the creation did not populate normals we have to do it
8593
# just calculate if not stored
8694
unit, valid = triangles.normals(self.triangles)
8795
normals = np.zeros((len(valid), 3))

0 commit comments

Comments
 (0)