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

1import json 

2import os 

3 

4import numpy as np 

5 

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 

17 

18 

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 

22 

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 

31 

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 

41 

42 if util.is_pathlib(file_obj): 

43 # handle `pathlib` objects by converting to string 

44 file_obj = str(file_obj.absolute()) 

45 

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) 

59 

60 # make sure file type is lower case 

61 file_type = str(file_type).lower() 

62 

63 if file_type not in _mesh_exporters: 

64 raise ValueError("%s exporter not available!", file_type) 

65 

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()) 

79 

80 # OBJ files save assets everywhere 

81 if file_type == "obj": 

82 kwargs["resolver"] = resolver 

83 

84 # run the exporter 

85 export = _mesh_exporters[file_type](mesh, **kwargs) 

86 

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") 

92 

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) 

97 

98 return export 

99 

100 if hasattr(file_obj, "write"): 

101 result = util.write_encoded(file_obj, export) 

102 else: 

103 result = export 

104 

105 # if we opened anything close it here 

106 if was_opened: 

107 file_obj.close() 

108 

109 return result 

110 

111 

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") 

118 

119 

120def export_dict(mesh, encoding=None): 

121 """ 

122 Export a mesh to a dict 

123 

124 Parameters 

125 ------------ 

126 mesh : trimesh.Trimesh 

127 Mesh to be exported 

128 encoding : str or None 

129 Such as 'base64' 

130 

131 Returns 

132 ------------- 

133 export : dict 

134 Data stored in dict 

135 """ 

136 

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) 

144 

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} 

151 

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) 

162 

163 return export 

164 

165 

166def scene_to_dict(scene, use_base64=False, include_metadata=True): 

167 """ 

168 Export a Scene object as a dict. 

169 

170 Parameters 

171 ------------- 

172 scene : trimesh.Scene 

173 Scene object to be exported 

174 

175 Returns 

176 ------------- 

177 as_dict : dict 

178 Scene as a dict 

179 """ 

180 

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 } 

192 

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) 

200 

201 # encode arrays with base64 or not 

202 if use_base64: 

203 file_type = "dict64" 

204 else: 

205 file_type = "dict" 

206 

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 

222 

223 

224def export_scene(scene, file_obj, file_type=None, resolver=None, **kwargs): 

225 """ 

226 Export a snapshot of the current scene. 

227 

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 

235 

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!") 

243 

244 if util.is_pathlib(file_obj): 

245 # handle `pathlib` objects by converting to string 

246 file_obj = str(file_obj.absolute()) 

247 

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!") 

254 

255 # always remove whitespace and leading characters 

256 file_type = file_type.strip().lower().lstrip(".") 

257 

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 

276 

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}") 

286 

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 

305 

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) 

314 

315 # no writeable file object so return data 

316 return data 

317 

318 

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)