Coverage for trimesh/exchange/export.py: 92%
142 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1import json
2import os
4import numpy as np
6from .. import resolvers, util
7from ..constants import log
8from .dae import _collada_exporters
9from .gltf import export_glb, export_gltf
10from .obj import export_obj
11from .off import _off_exporters
12from .ply import _ply_exporters
13from .stl import export_stl, export_stl_ascii
14from .threemf import _3mf_exporters
15from .urdf import export_urdf # NOQA
16from .xyz import _xyz_exporters
19def export_mesh(mesh, file_obj, file_type=None, resolver=None, **kwargs):
20 """
21 Export a Trimesh object to a file- like object, or to a filename
23 Parameters
24 -----------
25 file_obj : str, file-like
26 Where should mesh be exported to
27 file_type : str or None
28 Represents file type (eg: 'stl')
29 resolver : None or trimesh.resolvers.Resolver
30 Resolver to write referenced assets to
32 Returns
33 ----------
34 exported : bytes or str
35 Result of exporter
36 """
37 # if we opened a file object in this function
38 # we will want to close it when we're done
39 was_opened = False
40 file_name = None
42 if util.is_pathlib(file_obj):
43 # handle `pathlib` objects by converting to string
44 file_obj = str(file_obj.absolute())
46 if isinstance(file_obj, str):
47 if file_type is None:
48 # get file type from file name
49 file_type = (str(file_obj).split(".")[-1]).lower()
50 if file_type in _mesh_exporters:
51 was_opened = True
52 file_name = file_obj
53 # get full path of file before opening
54 file_path = os.path.abspath(os.path.expanduser(file_obj))
55 file_obj = open(file_path, "wb")
56 if resolver is None:
57 # create a resolver which can write files to the path
58 resolver = resolvers.FilePathResolver(file_path)
60 # make sure file type is lower case
61 file_type = str(file_type).lower()
63 if file_type not in _mesh_exporters:
64 raise ValueError("%s exporter not available!", file_type)
66 if isinstance(mesh, (list, tuple, set, np.ndarray)):
67 faces = 0
68 for m in mesh:
69 faces += len(m.faces)
70 log.debug(
71 "Exporting %d meshes with a total of %d faces as %s",
72 len(mesh),
73 faces,
74 file_type.upper(),
75 )
76 elif hasattr(mesh, "faces"):
77 # if the mesh has faces log the number
78 log.debug("Exporting %d faces as %s", len(mesh.faces), file_type.upper())
80 # OBJ files save assets everywhere
81 if file_type == "obj":
82 kwargs["resolver"] = resolver
84 # run the exporter
85 export = _mesh_exporters[file_type](mesh, **kwargs)
87 # if the export is multiple files (i.e. GLTF)
88 if isinstance(export, dict):
89 # if we have a filename rename the default GLTF
90 if file_name is not None and "model.gltf" in export:
91 export[os.path.basename(file_name)] = export.pop("model.gltf")
93 # write the files if a resolver has been passed
94 if resolver is not None:
95 for name, data in export.items():
96 resolver.write(name=name, data=data)
98 return export
100 if hasattr(file_obj, "write"):
101 result = util.write_encoded(file_obj, export)
102 else:
103 result = export
105 # if we opened anything close it here
106 if was_opened:
107 file_obj.close()
109 return result
112def export_dict64(mesh):
113 """
114 Export a mesh as a dictionary, with data encoded
115 to base64.
116 """
117 return export_dict(mesh, encoding="base64")
120def export_dict(mesh, encoding=None):
121 """
122 Export a mesh to a dict
124 Parameters
125 ------------
126 mesh : trimesh.Trimesh
127 Mesh to be exported
128 encoding : str or None
129 Such as 'base64'
131 Returns
132 -------------
133 export : dict
134 Data stored in dict
135 """
137 def encode(item, dtype=None):
138 if encoding is None:
139 return item.tolist()
140 else:
141 if dtype is None:
142 dtype = item.dtype
143 return util.array_to_encoded(item, dtype=dtype, encoding=encoding)
145 # metadata keys we explicitly want to preserve
146 # sometimes there are giant datastructures we don't
147 # care about in metadata which causes exports to be
148 # extremely slow, so skip all but known good keys
149 meta_keys = ["units", "file_name", "file_path"]
150 metadata = {k: v for k, v in mesh.metadata.items() if k in meta_keys}
152 export = {
153 "metadata": metadata,
154 "faces": encode(mesh.faces),
155 "face_normals": encode(mesh.face_normals),
156 "vertices": encode(mesh.vertices),
157 }
158 if mesh.visual.kind == "face":
159 export["face_colors"] = encode(mesh.visual.face_colors)
160 elif mesh.visual.kind == "vertex":
161 export["vertex_colors"] = encode(mesh.visual.vertex_colors)
163 return export
166def scene_to_dict(scene, use_base64=False, include_metadata=True):
167 """
168 Export a Scene object as a dict.
170 Parameters
171 -------------
172 scene : trimesh.Scene
173 Scene object to be exported
175 Returns
176 -------------
177 as_dict : dict
178 Scene as a dict
179 """
181 # save some basic data about the scene
182 export = {
183 "graph": scene.graph.to_edgelist(),
184 "geometry": {},
185 "scene_cache": {
186 "bounds": scene.bounds.tolist(),
187 "extents": scene.extents.tolist(),
188 "centroid": scene.centroid.tolist(),
189 "scale": scene.scale,
190 },
191 }
193 if include_metadata:
194 try:
195 # jsonify will convert numpy arrays to lists recursively
196 # a little silly round-tripping to json but it is pretty fast
197 export["metadata"] = json.loads(util.jsonify(scene.metadata))
198 except BaseException:
199 log.warning("failed to serialize metadata", exc_info=True)
201 # encode arrays with base64 or not
202 if use_base64:
203 file_type = "dict64"
204 else:
205 file_type = "dict"
207 # if the mesh has an export method use it
208 # otherwise put the mesh itself into the export object
209 for geometry_name, geometry in scene.geometry.items():
210 if hasattr(geometry, "export"):
211 # export the data
212 exported = {
213 "data": geometry.export(file_type=file_type),
214 "file_type": file_type,
215 }
216 export["geometry"][geometry_name] = exported
217 else:
218 # case where mesh object doesn't have exporter
219 # might be that someone replaced the mesh with a URL
220 export["geometry"][geometry_name] = geometry
221 return export
224def export_scene(scene, file_obj, file_type=None, resolver=None, **kwargs):
225 """
226 Export a snapshot of the current scene.
228 Parameters
229 ----------
230 file_obj : str, file-like, or None
231 File object to export to
232 file_type : str or None
233 What encoding to use for meshes
234 IE: dict, dict64, stl
236 Returns
237 ----------
238 export : bytes
239 Only returned if file_obj is None
240 """
241 if len(scene.geometry) == 0:
242 raise ValueError("Can't export empty scenes!")
244 if util.is_pathlib(file_obj):
245 # handle `pathlib` objects by converting to string
246 file_obj = str(file_obj.absolute())
248 # if we weren't passed a file type extract from file_obj
249 if file_type is None:
250 if isinstance(file_obj, str):
251 file_type = str(file_obj).split(".")[-1]
252 else:
253 raise ValueError("file_type not specified!")
255 # always remove whitespace and leading characters
256 file_type = file_type.strip().lower().lstrip(".")
258 # now handle our different scene export types
259 if file_type == "gltf":
260 data = export_gltf(scene, **kwargs)
261 elif file_type == "glb":
262 data = export_glb(scene, **kwargs)
263 elif file_type == "dict":
264 data = scene_to_dict(scene, *kwargs)
265 elif file_type == "obj":
266 # if we are exporting by name automatically create a
267 # resolver which lets the exporter write assets like
268 # the materials and textures next to the exported mesh
269 if resolver is None and isinstance(file_obj, str):
270 resolver = resolvers.FilePathResolver(file_obj)
271 data = export_obj(scene, resolver=resolver, **kwargs)
272 elif file_type == "dict64":
273 data = scene_to_dict(scene, use_base64=True)
274 elif file_type == "svg":
275 from trimesh.path.exchange import svg_io
277 data = svg_io.export_svg(scene, **kwargs)
278 elif file_type == "ply":
279 data = _mesh_exporters["ply"](scene.to_mesh(), **kwargs)
280 elif file_type == "stl":
281 data = export_stl(scene.to_mesh(), **kwargs)
282 elif file_type == "3mf":
283 data = _mesh_exporters["3mf"](scene, **kwargs)
284 else:
285 raise ValueError(f"unsupported export format: {file_type}")
287 # now write the data or return bytes of result
288 if isinstance(data, dict):
289 # GLTF files return a dict-of-bytes as they
290 # represent multiple files so create a filepath
291 # resolver and write the files if someone passed
292 # a path we can write to.
293 if resolver is None and isinstance(file_obj, str):
294 resolver = resolvers.FilePathResolver(file_obj)
295 # the requested "gltf"
296 bare_path = os.path.split(file_obj)[-1]
297 for name, blob in data.items():
298 if name == "model.gltf":
299 # write the root data to specified file
300 resolver.write(bare_path, blob)
301 else:
302 # write the supporting files
303 resolver.write(name, blob)
304 return data
306 if hasattr(file_obj, "write"):
307 # if it's just a regular file object
308 return util.write_encoded(file_obj, data)
309 elif isinstance(file_obj, str):
310 # assume strings are file paths
311 file_path = os.path.abspath(os.path.expanduser(file_obj))
312 with open(file_path, "wb") as f:
313 util.write_encoded(f, data)
315 # no writeable file object so return data
316 return data
319_mesh_exporters = {
320 "stl": export_stl,
321 "dict": export_dict,
322 "glb": export_glb,
323 "obj": export_obj,
324 "gltf": export_gltf,
325 "dict64": export_dict64,
326 "stl_ascii": export_stl_ascii,
327}
328_mesh_exporters.update(_ply_exporters)
329_mesh_exporters.update(_off_exporters)
330_mesh_exporters.update(_collada_exporters)
331_mesh_exporters.update(_xyz_exporters)
332_mesh_exporters.update(_3mf_exporters)