Skip to content

Commit 6bb01c5

Browse files
authored
Release: Docstrings + Docker UV (#2455)
- update docstring for `linear_color_map` - release #2453 - switch docker images to use `uv` to install Python, and add an alias for `pip`->`uv pip` so the images continue to behave similarly. - remove `Trimesh.smoothed`, which has been long-deprecated and scheduled for removal no earlier than March 2024. `Trimesh.smooth_shaded` has been recommended for the entire deprecation period via a warning. - fix #2456 - this fixes a crash on a PLY with both edge data (i.e. `trimesh.Path3D`) and face data (`Trimesh`). We now load both into the kwargs, but the `exchange` entry point picks only the faces. It should probably be returning a `Scene` which has both the mesh and path. - release #2457 - also refactor local functions `lin2srgb`/`srgb2lin` -> `trimesh.visual.color.linear_to_srgb` and add a round-trip test - in the docs build update all locked versions
2 parents e6458a6 + 59e5f69 commit 6bb01c5

File tree

18 files changed

+300
-97
lines changed

18 files changed

+300
-97
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ jobs:
6969
- name: Build
7070
run: pyproject-build --outdir dist .
7171
- name: Publish
72-
uses: pypa/gh-action-pypi-publish@v1.12.4
72+
uses: pypa/gh-action-pypi-publish@v1.13.0
7373

7474
containers:
7575
name: Build Docker Images

Dockerfile

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1+
2+
# Stage 1: Build uv
3+
FROM ghcr.io/astral-sh/uv:0.8.16 AS uv
4+
15
# use a vanilla Debian base image
26
FROM debian:trixie-slim AS base
37
LABEL maintainer="[email protected]"
48

5-
# Create a non-root user with `uid=499`.
9+
# Copy uv binary from the builder stage
10+
COPY --from=uv uv /usr/local/bin/uv
11+
12+
# Create a non-root user with `uid=499` and switch to it.
613
RUN useradd -m -u 499 -s /bin/bash user && \
714
apt-get update && \
8-
apt-get install --no-install-recommends -qq -y python3.13-venv && \
15+
apt-get install --no-install-recommends -qq -y ca-certificates && \
916
apt-get clean -y && rm -rf /var/lib/apt/lists/*
10-
1117
USER user
1218

1319
WORKDIR /home/user
@@ -17,11 +23,14 @@ WORKDIR /home/user
1723
# but if you use Debian methods like `update-alternatives`
1824
# it won't provide a `pip` which works easily and it isn't
1925
# easy to know how system packages interact with pip packages
20-
RUN python3.13 -m venv venv
26+
RUN uv venv --python=python3.13 venv
2127

22-
# So scripts installed from pip are in $PATH
28+
# Add `pip` script install location to $PATH
2329
ENV PATH="/home/user/venv/bin:$PATH"
2430
ENV VIRTUAL_ENV="/home/user/venv"
31+
# bash script that redirects `pip install X`->`uv pip install X`
32+
RUN echo "uv pip \$*" > /home/user/venv/bin/pip && \
33+
chmod +x /home/user/venv/bin/pip
2534

2635
# Install helper script to PATH.
2736
COPY --chmod=755 docker/trimesh-setup /home/user/venv/bin
@@ -30,13 +39,6 @@ COPY --chmod=755 docker/trimesh-setup /home/user/venv/bin
3039
## install things that need building
3140
FROM base AS build
3241

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-
4042
# copy in essential files
4143
COPY --chown=499 trimesh/ /home/user/trimesh
4244
COPY --chown=499 pyproject.toml /home/user/
@@ -77,10 +79,6 @@ RUN pip install .[all] && \
7779
# check for lint problems
7880
RUN ruff check trimesh
7981

80-
# run a limited array of static type checks
81-
# TODO : get this to pass on base
82-
RUN pyright trimesh/base.py || true
83-
8482
# run pytest wrapped with xvfb for simple viewer tests
8583
# print more columns so the short summary is usable
8684
RUN COLUMNS=240 xvfb-run pytest \

docs/examples.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def generate_index(source: str, target: str) -> str:
8181
continue
8282

8383
with open(convert) as f:
84-
doc, post = postprocess(f.read(), title=title)
84+
_doc, post = postprocess(f.read(), title=title)
8585
with open(convert, "w") as f:
8686
f.write(post)
8787

docs/requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ recommonmark==0.7.1
33
jupyter==1.1.1
44

55
# get sphinx version range from furo install
6-
furo==2024.8.6
6+
furo==2025.7.19
77
myst-parser==4.0.1
8-
pyopenssl==25.0.0
8+
pyopenssl==25.3.0
99
autodocsumm==0.2.14
1010
jinja2==3.1.6
11-
matplotlib==3.10.1
11+
matplotlib==3.10.6
1212
nbconvert==7.16.6
1313

pyproject.toml

Lines changed: 2 additions & 2 deletions
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.8.1"
8+
version = "4.8.2"
99
authors = [{name = "Michael Dawson-Haggerty", email = "[email protected]"}]
1010
license = {file = "LICENSE.md"}
1111
description = "Import, export, process, analyze and view triangular meshes."
@@ -112,7 +112,6 @@ test = [
112112
# a good idea in our "cannonical docker images"
113113
test_more = [
114114
"coveralls",
115-
"pyright",
116115
"ezdxf",
117116
"xatlas",
118117
"pytest-beartype; python_version>='3.10'",
@@ -171,6 +170,7 @@ flake8-implicit-str-concat = {"allow-multiline" = false}
171170
[tool.ruff.lint.flake8-tidy-imports.banned-api]
172171
"IPython.embed".msg = "you forgot to remove a debug embed ;)"
173172
"numpy.empty".msg = "uninitialized arrays are haunted try numpy.zeros"
173+
"numpy.empty_like".msg = "uninitialized arrays are haunted try numpy.zeros"
174174

175175
[tool.codespell]
176176
skip = "*.js*,./docs/built/*,./docs/generate/*,./models*,*.toml"

tests/test_color.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,29 @@
44
import generic as g
55

66

7+
def test_linear_srgb():
8+
from trimesh.visual.color import linear_to_srgb, srgb_to_linear, to_rgba
9+
10+
# deterministically create some colors
11+
original = g.np.arange(255, dtype=g.np.uint8).reshape((-1, 3))
12+
13+
# convert them to linear form
14+
linear = srgb_to_linear(original)
15+
16+
assert linear.shape == original.shape
17+
assert g.np.ptp(linear) > 0.0
18+
assert linear.dtype == g.np.float64
19+
20+
# convert them back to srgb float colors
21+
srgb = linear_to_srgb(linear)
22+
23+
# will convert float -> uint8 and clip off the alpha
24+
roundtrip = to_rgba(srgb)[:, :3]
25+
26+
# we should have roundtripped exactly
27+
assert g.np.allclose(roundtrip, original)
28+
29+
730
def test_visual():
831
mesh = g.get_mesh("featuretype.STL")
932

@@ -422,5 +445,6 @@ def test_copy():
422445

423446

424447
if __name__ == "__main__":
425-
test_to_rgba_float()
426-
test_interpolate()
448+
# test_to_rgba_float()
449+
# test_interpolate()
450+
test_linear_srgb()

tests/test_extrude.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def test_extrude_mid_plane():
8585
polygon=Point([0, 0]).buffer(1.0), height=10.0, transform=tf, mid_plane=True
8686
)
8787

88-
# the mesh center of mass should be independant
88+
# the mesh center of mass should be independent
8989
# of rotation and exactly at the translation
9090
center = m.to_mesh().center_mass
9191
translate = tf[:3, 3]

tests/test_inertia.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ def test_points_inertia_sample():
480480
# check to see that randomly sampled volume of a box
481481
# moved somewhere in space is "about equal" with the
482482
# approximate inertia of the points compared to the
483-
# exact intertia tensor of the box primitive
483+
# exact inertia tensor of the box primitive
484484

485485
# start with a random but deterministic rotation matrix
486486
matrix = g.np.array(

tests/test_obj.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ def test_material_name():
547547
m.export(file_obj=path)
548548
roundtrip = g.trimesh.load_mesh(file_obj=path)
549549

550-
# material name should have survivied
550+
# material name should have survived
551551
assert roundtrip.visual.material.name == "a-super-duper-material"
552552

553553
g.check_fuze(roundtrip)

tests/test_pbr_concatenate.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"""
2+
Test for PBR material concatenation bug fix.
3+
4+
Tests that concatenating meshes with PBR materials preserves
5+
metallicFactor and roughnessFactor values and creates correct
6+
UV mappings for textures.
7+
"""
8+
9+
try:
10+
from . import generic as g
11+
except BaseException:
12+
import generic as g
13+
14+
15+
class PBRConcatenateTest(g.unittest.TestCase):
16+
def test_pbr_factors_preserved(self):
17+
"""Test that metallicFactor and roughnessFactor are preserved when
18+
concatenating meshes with PBR materials.
19+
20+
This tests the fix for the bug where these factors became None after
21+
concatenation, causing meshes to appear shiny/metallic.
22+
"""
23+
# Create two boxes with PBR materials
24+
box1 = g.trimesh.creation.box(extents=[1, 1, 1])
25+
mat1 = g.trimesh.visual.material.PBRMaterial(
26+
baseColorFactor=[255, 51, 51, 255], # Red
27+
metallicFactor=0.0,
28+
roughnessFactor=1.0,
29+
)
30+
box1.visual = g.trimesh.visual.TextureVisuals(material=mat1)
31+
32+
box2 = g.trimesh.creation.box(extents=[1, 1, 1])
33+
mat2 = g.trimesh.visual.material.PBRMaterial(
34+
baseColorFactor=[51, 255, 51, 255], # Green
35+
metallicFactor=0.0,
36+
roughnessFactor=1.0,
37+
)
38+
box2.visual = g.trimesh.visual.TextureVisuals(material=mat2)
39+
box2.apply_translation([1.5, 0, 0])
40+
41+
# Concatenate the meshes
42+
merged = g.trimesh.util.concatenate([box1, box2])
43+
44+
# Factor are `None` because textures are created for metallicFactor and
45+
# roughnessFactor
46+
assert merged.visual.material.metallicFactor is None
47+
assert merged.visual.material.roughnessFactor is None
48+
49+
# Check that textures were created
50+
assert merged.visual.material.baseColorTexture is not None
51+
assert merged.visual.material.metallicRoughnessTexture is not None
52+
53+
def test_pbr_uv_mapping(self):
54+
"""Test that UV coordinates correctly map to material values in the
55+
packed textures after concatenation.
56+
"""
57+
# Create two boxes with different colors but same PBR values
58+
box1 = g.trimesh.creation.box(extents=[0.5, 0.5, 0.5])
59+
mat1 = g.trimesh.visual.material.PBRMaterial(
60+
baseColorFactor=[255, 0, 0, 255],
61+
metallicFactor=0.0,
62+
roughnessFactor=1.0,
63+
)
64+
box1.visual = g.trimesh.visual.TextureVisuals(material=mat1)
65+
66+
box2 = g.trimesh.creation.box(extents=[0.5, 0.5, 0.5])
67+
mat2 = g.trimesh.visual.material.PBRMaterial(
68+
baseColorFactor=[0, 255, 0, 255],
69+
metallicFactor=0.0,
70+
roughnessFactor=1.0,
71+
)
72+
box2.visual = g.trimesh.visual.TextureVisuals(material=mat2)
73+
box2.apply_translation([1, 0, 0])
74+
75+
# Concatenate
76+
merged = g.trimesh.util.concatenate([box1, box2])
77+
78+
# Get the metallicRoughness texture
79+
tex = g.np.array(merged.visual.material.metallicRoughnessTexture)
80+
81+
# Check that all UV coordinates map to the correct material values
82+
# In glTF: Blue channel = metallic, Green channel = roughness
83+
for u, v in merged.visual.uv:
84+
# Convert UV to pixel coordinates
85+
px = round(u * (tex.shape[1] - 1))
86+
py = round((1 - v) * (tex.shape[0] - 1))
87+
88+
if 0 <= px < tex.shape[1] and 0 <= py < tex.shape[0]:
89+
roughness = tex[py, px, 1] # Green channel
90+
metallic = tex[py, px, 2] # Blue channel
91+
92+
# All vertices should map to roughness=255 (1.0) and metallic=0 (0.0)
93+
assert roughness == 255, (
94+
f"UV ({u:.3f}, {v:.3f}) maps to wrong roughness: {roughness}"
95+
)
96+
assert metallic == 0, (
97+
f"UV ({u:.3f}, {v:.3f}) maps to wrong metallic: {metallic}"
98+
)
99+
100+
def test_pbr_identical_materials(self):
101+
"""Test that concatenating meshes with identical PBR materials
102+
preserves the scalar values without creating textures."""
103+
# Create two boxes with identical materials
104+
box1 = g.trimesh.creation.box(extents=[0.5, 0.5, 0.5])
105+
mat1 = g.trimesh.visual.material.PBRMaterial(
106+
baseColorFactor=[128, 128, 128, 255],
107+
metallicFactor=0.5,
108+
roughnessFactor=0.7,
109+
)
110+
box1.visual = g.trimesh.visual.TextureVisuals(material=mat1)
111+
112+
box2 = g.trimesh.creation.box(extents=[0.5, 0.5, 0.5])
113+
mat2 = g.trimesh.visual.material.PBRMaterial(
114+
baseColorFactor=[128, 128, 128, 255], # Same color
115+
metallicFactor=0.5, # Same metallic
116+
roughnessFactor=0.7, # Same roughness
117+
)
118+
box2.visual = g.trimesh.visual.TextureVisuals(material=mat2)
119+
box2.apply_translation([1, 0, 0])
120+
121+
# Concatenate
122+
merged = g.trimesh.util.concatenate([box1, box2])
123+
124+
# When materials are identical, scalar values should be preserved
125+
assert merged.visual.material.metallicFactor == 0.5
126+
assert merged.visual.material.roughnessFactor == 0.7
127+
128+
# No metallicRoughness texture should be created for identical materials
129+
assert merged.visual.material.metallicRoughnessTexture is None
130+
131+
132+
if __name__ == "__main__":
133+
g.trimesh.util.attach_to_log()
134+
g.unittest.main()

0 commit comments

Comments
 (0)