Coverage for trimesh/interfaces/blender.py: 67%

55 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +0000

1import os 

2import platform 

3 

4from .. import util 

5from ..constants import log 

6from ..resources import get_string 

7from ..typed import BooleanOperationType, Iterable 

8from .generic import MeshScript 

9 

10if platform.system() == "Windows": 

11 # try to find Blender install on Windows 

12 # split existing path by delimiter 

13 _search_path = [i for i in os.environ.get("PATH", "").split(";") if len(i) > 0] 

14 for pf in [r"C:\Program Files", r"C:\Program Files (x86)"]: 

15 pf = os.path.join(pf, "Blender Foundation") 

16 if os.path.exists(pf): 

17 for p in os.listdir(pf): 

18 if "Blender" in p: 

19 _search_path.append(os.path.join(pf, p)) 

20 _search_path = ";".join(set(_search_path)) 

21 log.debug("searching for blender in: %s", _search_path) 

22elif platform.system() == "Darwin": 

23 # try to find Blender on Mac OSX 

24 _search_path = [i for i in os.environ.get("PATH", "").split(":") if len(i) > 0] 

25 _search_path.extend( 

26 [ 

27 "/Applications/blender.app/Contents/MacOS", 

28 "/Applications/Blender.app/Contents/MacOS", 

29 "/Applications/Blender/blender.app/Contents/MacOS", 

30 ] 

31 ) 

32 _search_path = ":".join(set(_search_path)) 

33 log.debug("searching for blender in: %s", _search_path) 

34else: 

35 _search_path = os.environ.get("PATH", "") 

36 

37_blender_executable = util.which("blender", path=_search_path) 

38exists = _blender_executable is not None 

39 

40# a map that translates: 

41# `trimesh.BooleanOperationType` -> blender value 

42_blender_bool = { 

43 "union": "UNION", 

44 "difference": "DIFFERENCE", 

45 "intersection": "INTERSECT", 

46} 

47 

48 

49def boolean( 

50 meshes: Iterable, 

51 operation: BooleanOperationType = "difference", 

52 use_exact: bool = True, 

53 use_self: bool = False, 

54 debug: bool = False, 

55 check_volume: bool = True, 

56): 

57 """ 

58 Run a boolean operation with multiple meshes using Blender. 

59 

60 Parameters 

61 ----------- 

62 meshes 

63 List of mesh objects to be operated on 

64 operation 

65 Type of boolean operation ("difference", "union", "intersect"). 

66 use_exact 

67 Use the "exact" mode as opposed to the "fast" mode. 

68 use_self 

69 Whether to consider self-intersections. 

70 debug 

71 Provide additional output for troubleshooting. 

72 check_volume 

73 Raise an error if not all meshes are watertight 

74 positive volumes. Advanced users may want to ignore 

75 this check as it is expensive. 

76 

77 Returns 

78 ---------- 

79 result 

80 The result of the boolean operation on the provided meshes. 

81 """ 

82 if not exists: 

83 raise ValueError("No blender available!") 

84 if check_volume and not all(m.is_volume for m in meshes): 

85 raise ValueError("Not all meshes are volumes!") 

86 

87 # conversions from the trimesh `BooleanOperationType` to the blender option 

88 key = operation.lower().strip() 

89 if key not in _blender_bool: 

90 raise ValueError( 

91 f"`{operation}` is not a valid boolean: `{_blender_bool.keys()}`" 

92 ) 

93 

94 if use_exact: 

95 solver_options = "EXACT" 

96 else: 

97 solver_options = "FAST" 

98 

99 # get the template from our resources folder 

100 template = get_string("templates/blender_boolean.py.tmpl") 

101 # use string substitutions rather than `string.Template` as we aren't going 

102 # to be filling in all the values here, `MeshScript` is going to be 

103 # the source of `$MESH_PRE`, etc. 

104 script = ( 

105 template.replace("$OPERATION", _blender_bool[key]) 

106 .replace("$SOLVER_OPTIONS", solver_options) 

107 .replace("$USE_SELF", f"{use_self}") 

108 ) 

109 with MeshScript(meshes=meshes, script=script, debug=debug) as blend: 

110 result = blend.run(_blender_executable + " --background --python $SCRIPT") 

111 

112 result = util.make_sequence(result) 

113 for m in result: 

114 # blender returns actively incorrect face normals 

115 m.face_normals = None 

116 

117 return util.concatenate(result) 

118 

119 

120def unwrap( 

121 mesh, angle_limit: float = 66.0, island_margin: float = 0.0, debug: bool = False 

122): 

123 """ 

124 Run an unwrap operation using blender. 

125 """ 

126 if not exists: 

127 raise ValueError("No blender available!") 

128 

129 # get the template from our resources folder 

130 template = get_string("templates/blender_unwrap.py.template") 

131 script = template.replace("$ANGLE_LIMIT", f"{angle_limit:.6f}").replace( 

132 "$ISLAND_MARGIN", f"{island_margin:.6f}" 

133 ) 

134 

135 with MeshScript(meshes=[mesh], script=script, exchange="obj", debug=debug) as blend: 

136 result = blend.run(_blender_executable + " --background --python $SCRIPT") 

137 

138 for m in util.make_sequence(result): 

139 # blender returns actively incorrect face normals 

140 m.face_normals = None 

141 

142 return result