Skip to content

Commit 136f67c

Browse files
authored
Merge pull request #2232 from mikedh/release/pickle
Release: Pickleable
2 parents 7853a5e + 7fbbdc0 commit 136f67c

15 files changed

Lines changed: 229 additions & 20 deletions

File tree

examples/prepare.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""
2+
An example of a simple way of preparing and analyzing a model
3+
for properties of interest to 3D printing, implemented using
4+
dataclasses which can easily be used for API routes.
5+
6+
- Volume
7+
- Oriented Bounding Box
8+
- thin regions
9+
10+
The nominal operations flow we're using here is defined by
11+
easily serialized `dataclass` objects and goes:
12+
13+
RawFile -> # a raw loadable file
14+
PrintCandidate -> # usable properties about the model
15+
PrintJob -> # after quantities and options are picked
16+
PrintRelease # i.e. what you would send to the machine
17+
"""
18+
19+
import os
20+
from dataclasses import dataclass
21+
from typing import Optional
22+
23+
import trimesh
24+
25+
26+
@dataclass
27+
class RawFile:
28+
# the data for the loadable file
29+
file_data: bytes
30+
31+
# the original name of the file
32+
# the extension will be used to determine type
33+
file_name: str
34+
35+
# the units of the file if it is in a format
36+
# that does not include units (i.e. STL files)
37+
units: Optional[str] = None
38+
39+
40+
@dataclass
41+
class Error:
42+
# the user-facing message
43+
message: str
44+
45+
# a code that can be used to group problems
46+
code: str
47+
48+
# if faces were colored to indicate the problem
49+
color: str
50+
51+
52+
@dataclass
53+
class PrintCandidate:
54+
# the repaired model repaired into a GLB file
55+
glb: bytes
56+
57+
# any problems that were encountered
58+
errors: list[Error]
59+
60+
# the volume of the mesh
61+
volume: float
62+
63+
# the size of the axis-aligned bounding box
64+
extents: list[float]
65+
66+
67+
def load(raw: RawFile) -> trimesh.Trimesh:
68+
"""
69+
Load a raw file for 3D printing, collapsing meshes into a single body.
70+
"""
71+
# coerce scenes and formats with normals into a mesh
72+
mesh = trimesh.load(
73+
file_obj=trimesh.util.wrap_as_stream(raw.file_data),
74+
file_type=raw.file_name,
75+
process=True,
76+
merge_tex=True, # ignore UV coordinates
77+
merge_norm=True, # ignore vertex normals
78+
force="mesh", # concatenate into single mesh
79+
)
80+
mesh.process(merge_tex=True, merge_norm=True)
81+
82+
# if passed explicitly
83+
if mesh.units is None and raw.units is not None:
84+
mesh.units = raw.units
85+
86+
# convert mesh to meters and guess as a fallback
87+
mesh.convert_units("meters", guess=True)
88+
89+
# make sure it is using face colors
90+
mesh.visual = trimesh.visual.ColorVisuals(mesh=mesh)
91+
92+
return mesh
93+
94+
95+
def prepare(
96+
mesh: trimesh.Trimesh,
97+
minimum_thickness: float = 0.0005,
98+
) -> PrintCandidate:
99+
"""
100+
Take a raw 3D model and process it for 3D printing.
101+
"""
102+
103+
# collect issues to report
104+
problems = []
105+
106+
if not mesh.fill_holes():
107+
problems.append(
108+
Error(message="Mesh is not watertight!", code="watertight", color="#FF0000")
109+
)
110+
# will apply "red" to broken faces
111+
trimesh.repair.broken_faces(mesh=mesh, color=[255, 0, 0, 255])
112+
113+
thick_bad = (
114+
trimesh.proximity.thickness(
115+
mesh, mesh.triangles_center - mesh.face_normals * 0.001
116+
)
117+
< minimum_thickness
118+
)
119+
120+
if thick_bad.any():
121+
mesh.visual.face_colors[thick_bad] = [0, 0, 255, 255]
122+
problems.append(
123+
Error(
124+
message="Mesh has regions below minimum thickness!",
125+
code="watertight",
126+
color="#0000FF",
127+
)
128+
)
129+
130+
mesh.show()
131+
132+
return PrintCandidate(
133+
glb=mesh.export(file_type="glb"),
134+
errors=problems,
135+
volume=mesh.volume,
136+
extents=mesh.extents,
137+
)
138+
139+
140+
if __name__ == "__main__":
141+
# current working directory
142+
cwd = os.path.abspath(os.path.expanduser(os.path.dirname(__file__)))
143+
144+
# create a raw "request"
145+
file_name = "machinist.3DXML"
146+
# file_name = 'ADIS16480.STL'
147+
file_path = os.path.join(cwd, "..", "models", file_name)
148+
with open(file_path, "rb") as f:
149+
loaded = load(raw=RawFile(file_data=f.read(), file_name=file_name, units=None))
150+
151+
from pyinstrument import Profiler
152+
153+
with Profiler() as P:
154+
report = prepare(loaded)
155+
156+
P.print()

models/cubevc.glb

4.66 KB
Binary file not shown.

pyproject.toml

Lines changed: 6 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.7"
8-
version = "4.4.0"
8+
version = "4.4.1"
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."
@@ -137,7 +137,8 @@ select = [
137137
"W", # style warnings
138138
"YTT", # sys.version
139139
"ISC002",
140-
"NPY201"
140+
"NPY201",
141+
"TID251"
141142
]
142143

143144
ignore = [
@@ -153,6 +154,9 @@ ignore = [
153154
# don't allow implicit string concatenation
154155
flake8-implicit-str-concat = {"allow-multiline" = false}
155156

157+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
158+
"IPython.embed".msg = "you forgot to remove a debug embed ;)"
159+
156160
[tool.codespell]
157161
skip = "*.js*,./docs/built/*,./docs/generate/*,./models*,*.toml"
158162
ignore-words-list = "nd,coo,whats,bu,childs,mis,filetests"

tests/corpus.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,7 @@ def on_repo(repo, commit):
9898
a, b = geom.visual.material, ori.visual.material
9999
# try our fancy equal
100100
assert equal(a.baseColorFactor, b.baseColorFactor)
101-
try:
102-
assert equal(a.baseColorTexture, b.baseColorTexture)
103-
except BaseException:
104-
from IPython import embed
105-
106-
embed()
101+
assert equal(a.baseColorTexture, b.baseColorTexture)
107102

108103
except NotImplementedError as E:
109104
# this is what unsupported formats

tests/generic.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import json
1010
import copy
1111
import time
12+
import pickle
1213
import shutil
1314
import timeit
1415
import base64

tests/helpers/dxfhelp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def write_json(template, file_name="../templates/dxf.json"):
3131

3232
def replace_whitespace(text, SAFE_SPACE="|<^>|", insert=True, reformat=False):
3333
"""
34-
Replace non-strippable whitepace in a string with a safe space
34+
Replace non-strippable whitespace in a string with a safe space
3535
"""
3636
if insert:
3737
# replace whitespace with safe space chr

tests/test_gltf.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,23 @@ def test_vertex_attrib(self):
796796
# make sure the color vertex attributes survived the roundtrip
797797
assert g.np.allclose(r.visual.vertex_attributes["color"], colors)
798798

799+
def test_vertex_colors_import(self):
800+
# get a mesh with face colors
801+
m = g.get_mesh("cubevc.glb")
802+
assert len(m.geometry.items()) > 0
803+
804+
mesh = next(iter(m.geometry.items()))[1]
805+
assert mesh is not None
806+
807+
# Loaded mesh should have vertex colors
808+
assert hasattr(mesh.visual, "vertex_colors")
809+
810+
# Loaded mesh should have all vertex colors filled with magenta color
811+
magenta = g.np.array([255, 0, 255, 255])
812+
for color in mesh.visual.vertex_colors:
813+
is_magenta = g.np.array_equal(color, magenta)
814+
assert is_magenta, f"Imported vertex color is not of expected value: got {color}, expected {magenta}"
815+
799816
def test_export_postprocess(self):
800817
scene = g.trimesh.Scene()
801818
sphere = g.trimesh.primitives.Sphere()

tests/test_mesh.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,33 @@ def test_meshes(self):
104104
assert len(mesh.vertices) == len(mesh.vertex_defects)
105105
assert len(mesh.principal_inertia_components) == 3
106106

107+
# make a ray query which may lead to unpicklable caching
108+
dimension = (100, 3)
109+
ray_origins = g.random(dimension)
110+
ray_directions = g.np.tile([0, 0, 1], (dimension[0], 1))
111+
ray_origins[:, 2] = mesh.bounds[0][2] - mesh.scale
112+
113+
# call additional C objects
114+
assert mesh.kdtree is not None
115+
assert mesh.triangles_tree is not None
116+
117+
# force ray object to be created
118+
ray = mesh.ray.intersects_location(ray_origins, ray_directions)
119+
assert ray is not None
120+
107121
# collect list of cached properties that are writeable
108122
writeable = []
123+
124+
# make sure a roundtrip pickle works
125+
# if the cache has non-pickleable stuff this will break
126+
pickle = g.pickle.dumps(mesh)
127+
assert isinstance(pickle, bytes)
128+
assert len(pickle) > 0
129+
130+
r = g.pickle.loads(pickle)
131+
assert r.faces.shape == mesh.faces.shape
132+
assert g.np.isclose(r.volume, mesh.volume)
133+
109134
# we should have built up a bunch of stuff into
110135
# our cache, so make sure all numpy arrays cached
111136
# are read-only and not crazy

trimesh/exchange/export.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ def export_scene(scene, file_obj, file_type=None, resolver=None, **kwargs):
252252
else:
253253
raise ValueError("file_type not specified!")
254254

255-
# always remove whitepace and leading characters
255+
# always remove whitespace and leading characters
256256
file_type = file_type.strip().lower().lstrip(".")
257257

258258
# now handle our different scene export types

trimesh/exchange/gltf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,11 +1560,11 @@ def _read_buffers(
15601560
if len(colors) == len(kwargs["vertices"]):
15611561
if visuals is None:
15621562
# just pass to mesh as vertex color
1563-
kwargs["vertex_colors"] = colors
1563+
kwargs["vertex_colors"] = colors.copy()
15641564
else:
15651565
# we ALSO have texture so save as vertex
15661566
# attribute
1567-
visuals.vertex_attributes["color"] = colors
1567+
visuals.vertex_attributes["color"] = colors.copy()
15681568
except BaseException:
15691569
# survive failed colors
15701570
log.debug("failed to load colors", exc_info=True)

0 commit comments

Comments
 (0)