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

1import os 

2import platform 

3import subprocess 

4from string import Template 

5from subprocess import CalledProcessError, check_output 

6from tempfile import NamedTemporaryFile 

7 

8from .. import exchange 

9from ..util import log 

10 

11 

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 

19 

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) 

39 

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) 

43 

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 

48 

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

53 

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 

60 

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 

72 

73 if self.debug: 

74 log.info("executing: {}".format(" ".join(command_run))) 

75 

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

83 

84 if self.debug: 

85 log.info(output.decode()) 

86 

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) 

89 

90 return mesh_results 

91 

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)