Coverage for trimesh/interfaces/generic.py: 86%
59 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 os
2import platform
3import subprocess
4from string import Template
5from subprocess import CalledProcessError, check_output
6from tempfile import NamedTemporaryFile
8from .. import exchange
9from ..util import log
12class MeshScript:
13 def __init__(self, meshes, script, exchange="stl", debug=False, **kwargs):
14 self.debug = debug
15 self.kwargs = kwargs
16 self.meshes = meshes
17 self.script = script
18 self.exchange = exchange
20 def __enter__(self):
21 # windows has problems with multiple programs using open files so we close
22 # them at the end of the enter call, and delete them ourselves at exit
23 # Blender sorts its objects alphabetically
24 # so prefix the mesh number on the file name
25 digit_count = len(str(len(self.meshes)))
26 self.mesh_pre = [
27 NamedTemporaryFile(
28 suffix=f".{self.exchange}",
29 prefix=f"{str(i).zfill(digit_count)}_",
30 mode="wb",
31 delete=False,
32 )
33 for i in range(len(self.meshes))
34 ]
35 self.mesh_post = NamedTemporaryFile(
36 suffix=f".{self.exchange}", mode="rb", delete=False
37 )
38 self.script_out = NamedTemporaryFile(mode="wb", delete=False)
40 # export the meshes to a temporary STL container
41 for mesh, file_obj in zip(self.meshes, self.mesh_pre):
42 mesh.export(file_obj=file_obj.name)
44 self.replacement = {"MESH_" + str(i): m.name for i, m in enumerate(self.mesh_pre)}
45 self.replacement["MESH_PRE"] = str([i.name for i in self.mesh_pre])
46 self.replacement["MESH_POST"] = self.mesh_post.name
47 self.replacement["SCRIPT"] = self.script_out.name
49 script_text = Template(self.script).substitute(self.replacement)
50 if platform.system() == "Windows":
51 script_text = script_text.replace("\\", "\\\\")
52 self.script_out.write(script_text.encode("utf-8"))
54 # close all temporary files
55 self.script_out.close()
56 self.mesh_post.close()
57 for file_obj in self.mesh_pre:
58 file_obj.close()
59 return self
61 def run(self, command):
62 # substitute per-token so a replacement containing whitespace
63 # stays a single argv entry
64 command_run = [
65 Template(part).substitute(self.replacement) for part in command.split()
66 ]
67 # run the binary
68 startupinfo = None
69 if platform.system() == "Windows":
70 startupinfo = subprocess.STARTUPINFO()
71 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
73 if self.debug:
74 log.info("executing: {}".format(" ".join(command_run)))
76 try:
77 output = check_output(
78 command_run, stderr=subprocess.STDOUT, startupinfo=startupinfo
79 )
80 except CalledProcessError as E:
81 # raise with the output from the process
82 raise RuntimeError(E.output.decode())
84 if self.debug:
85 log.info(output.decode())
87 # bring the binaries result back as a set of Trimesh kwargs
88 mesh_results = exchange.load.load_mesh(self.mesh_post.name, **self.kwargs)
90 return mesh_results
92 def __exit__(self, *args, **kwargs):
93 if self.debug:
94 log.info(f"MeshScript.debug: not deleting {self.script_out.name}")
95 return
96 # delete all the temporary files by name
97 # they are closed but their names are still available
98 os.remove(self.script_out.name)
99 for file_obj in self.mesh_pre:
100 os.remove(file_obj.name)
101 os.remove(self.mesh_post.name)