Coverage for /home/ubuntu/baas/python/baas/blender/blender_tools.py: 100%

163 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 01:40 +0000

1from typing import Union 

2 

3from pathlib import Path 

4from tempfile import TemporaryDirectory 

5 

6import bpy 

7import bmesh 

8 

9import logging 

10LOGGER = logging.getLogger(__name__) 

11 

12Filepath = Union[str, Path] 

13 

14 

15# SCENE-FUNCTIONS--------------------------------------------------------------- 

16def set_scene(scene): 

17 ''' 

18 Set's Blender's scene context ot given scene 

19 

20 scene - Blender scene object. 

21 ''' 

22 bpy.context.window.scene = scene 

23 

24 

25def delete_scene(scene): 

26 ''' 

27 Deletes given scene from Blender. 

28 

29 scene - Blender scene. 

30 ''' 

31 set_scene(scene) 

32 bpy.ops.scene.delete() 

33 LOGGER.debug('Scene deleted.') 

34 

35 

36def delete_all_scenes(): 

37 ''' 

38 Destroys all Blender scenes and creates a new empty one. 

39 ''' 

40 for scene in bpy.data.scenes[:-1]: 

41 set_scene(scene) 

42 delete_all_objects() 

43 delete_scene(scene) 

44 

45 old_scene = bpy.data.scenes[0] 

46 set_scene(old_scene) 

47 delete_all_objects() 

48 

49 bpy.ops.scene.new(type='EMPTY') 

50 scene = bpy.data.scenes[-1] 

51 set_scene(scene) 

52 delete_all_objects() 

53 

54 set_scene(old_scene) 

55 delete_all_objects() 

56 delete_scene(old_scene) 

57 

58 bpy.data.scenes[0].name = 'Scene' 

59 LOGGER.debug('All scenes deleted.') 

60 

61 

62# MODE-FUNCTIONS---------------------------------------------------------------- 

63def activate_edit_mode(): 

64 ''' 

65 Set Blender interaction mode to edit. 

66 ''' 

67 bpy.ops.object.mode_set(mode='EDIT') 

68 LOGGER.debug('Edit mode active.') 

69 

70 

71def activate_object_mode(): 

72 ''' 

73 Set Blender interaction mode to object. 

74 ''' 

75 bpy.ops.object.mode_set(mode='OBJECT') 

76 LOGGER.debug('Object mode active.') 

77 

78 

79# OBJECT-FUNCTIONS----------------------------------------------------------- 

80def select_object(object_): 

81 ''' 

82 Selects given Blender object. 

83 

84 Args: 

85 object_ (bpy object): Blender object to be selected. 

86 ''' 

87 object_.select_set(True) 

88 bpy.context.view_layer.objects.active = object_ 

89 

90 

91def deselect_object(object_): 

92 ''' 

93 Deselect given Blender object. 

94 

95 Args: 

96 object_ (bpy object): Blender object to be deselecoted. 

97 ''' 

98 object_.select_set(False) 

99 

100 

101def select_all_objects(): 

102 ''' 

103 Selects all objects within current Blender scene. 

104 ''' 

105 for obj in bpy.context.scene.objects: 

106 select_object(obj) 

107 

108 

109def deselect_all_objects(): 

110 ''' 

111 Deselect all objects within current Blender scene. 

112 ''' 

113 for obj in bpy.context.scene.objects: 

114 deselect_object(obj) 

115 bpy.context.view_layer.objects.active = None 

116 

117 

118def filter_select_objects(pattern): 

119 ''' 

120 Clears selected objects and then selects Blender objects according to given 

121 pattern. 

122 

123 Args: 

124 pattern (str): Glob pattern of object name. 

125 ''' 

126 deselect_all_objects() 

127 bpy.ops.object.select_pattern(pattern=pattern, case_sensitive=True) 

128 

129 

130def delete_object(object_): 

131 ''' 

132 Deletes given object_. 

133 

134 Args: 

135 object_ (bpy.types.Object): Blender object to be deleted. 

136 ''' 

137 deselect_all_objects() 

138 select_object(object_) 

139 bpy.ops.object.delete() 

140 

141 

142def delete_all_objects(): 

143 ''' 

144 Deletes all objects within current scene. 

145 ''' 

146 deselect_all_objects() 

147 select_all_objects() 

148 bpy.ops.object.delete() 

149 

150 

151def filter_objects(object_type): 

152 ''' 

153 Filters Blender objects by object type. 

154 

155 Args: 

156 object_type (str): Type of Blender object. 

157 

158 Returns: 

159 list: List of all Blender objects of given type. 

160 ''' 

161 return list(filter( 

162 lambda x: x.type == object_type.upper(), bpy.context.scene.objects 

163 )) 

164 

165 

166# FACE-FUNCTIONS--------------------------------------------------------------- 

167def select_faces(object_, indices): 

168 ''' 

169 Select faces on given Blender mesh object according to according to given 

170 face indices. 

171 

172 Args: 

173 object_ (bpy object): Blender object with faces to be selected. 

174 indices (list): List of integers. 

175 ''' 

176 deselect_all_objects() 

177 select_object(object_) 

178 activate_edit_mode() 

179 bpy.ops.mesh.select_mode(type='FACE') 

180 mesh = bmesh.from_edit_mesh(object_.data) 

181 mesh.faces.ensure_lookup_table() 

182 for i in indices: 

183 mesh.faces[i].select = True 

184 object_.data.update() 

185 

186 

187def deselect_faces(object_, indices): 

188 ''' 

189 Deselect faces on given Blender mesh object according to according to given 

190 face indices. 

191 

192 Args: 

193 object_ (bpy object): Blender object with faces to be deselected. 

194 indices (list): List of integers. 

195 ''' 

196 deselect_all_objects() 

197 select_object(object_) 

198 activate_edit_mode() 

199 bpy.ops.mesh.select_mode(type='FACE') 

200 mesh = bmesh.from_edit_mesh(object_.data) 

201 mesh.faces.ensure_lookup_table() 

202 for i in indices: 

203 mesh.faces[i].select = False 

204 object_.data.update() 

205 

206 

207def select_all_faces(object_): 

208 ''' 

209 Select all faces of the given Blender mesh object. 

210 

211 Args: 

212 object_ (bpy object): Blender object with faces to be selected. 

213 ''' 

214 deselect_all_objects() 

215 select_object(object_) 

216 activate_edit_mode() 

217 bpy.ops.mesh.select_mode(type='FACE') 

218 bpy.ops.mesh.select_all(action='SELECT') 

219 

220 

221def deselect_all_faces(object_): 

222 ''' 

223 Deselect all faces of the given Blender mesh object. 

224 

225 Args: 

226 object_ (bpy object): Blender object with faces to be selected. 

227 ''' 

228 deselect_all_objects() 

229 select_object(object_) 

230 activate_edit_mode() 

231 bpy.ops.mesh.select_mode(type='FACE') 

232 bpy.ops.mesh.select_all(action='DESELECT') 

233 

234 

235def triangulate_faces(object_): 

236 ''' 

237 Triangulate currenly selected faces of given Blender mesh object. 

238 

239 Args: 

240 object_ (bpy object): Blender object with faces preselected for 

241 triangulation. 

242 ''' 

243 activate_edit_mode() 

244 bpy.ops.mesh.quads_convert_to_tris() 

245 activate_object_mode() 

246 

247 

248def triangulate_all_objects(): 

249 ''' 

250 Triangulates all faces of all objects with current Blender scene. 

251 ''' 

252 for obj in filter_objects('mesh'): 

253 select_object(obj) 

254 triangulate_faces(obj) 

255 

256 

257def mesh_to_pydata(mesh): 

258 ''' 

259 Converts a given Blender mesh in to a tuple of vertices, edges and faces. 

260 

261 Args: 

262 mesh (bpy mesh): Blender mesh object. 

263 

264 Returns: 

265 tuple: (vertices, edges, faces). 

266 ''' 

267 verts = list(map(lambda x: list(x.co), mesh.data.vertices.values())) 

268 

269 edges = mesh.data.edge_keys 

270 edges = sorted([sorted(list(x)) for x in edges]) 

271 

272 faces = list(map(lambda x: list(x.vertices), mesh.data.polygons.values())) 

273 

274 return (verts, edges, faces) 

275 

276 

277# IO-FUNCTIONS------------------------------------------------------------------ 

278def read_mesh(filepath): 

279 # type: (Filepath) -> None 

280 ''' 

281 Read mesh from given filepath. 

282 The following formats are supported: 

283 

284 * abc 

285 * dae 

286 * fbx 

287 * glb 

288 * obj 

289 * stl 

290 * usd 

291 * usdc 

292 * usdz 

293 

294 Args: 

295 filepath (str): Path to mesh file. 

296 

297 Raises: 

298 AssertionError: If file is not found. 

299 ValueError: If file extension is unknown. 

300 ''' 

301 fp = Path(filepath).as_posix() 

302 assert Path(fp).is_file(), f'File not found: {fp}' 

303 

304 ext = Path(fp).suffix[1:] 

305 match ext: 

306 case 'abc': 

307 bpy.ops.wm.alembic_import(filepath=fp, filter_glob='*.abc') 

308 case 'dae': 

309 bpy.ops.wm.collada_import(filepath=fp, filter_glob='*.dae') 

310 case 'fbx': 

311 bpy.ops.import_scene.fbx(filepath=fp, filter_glob='*.fbx') 

312 case 'glb': 

313 bpy.ops.import_scene.gltf(filepath=fp, filter_glob='*.glb') 

314 case 'obj': 

315 bpy.ops.wm.obj_import(filepath=fp, filter_glob='*.obj') 

316 case 'stl': 

317 bpy.ops.wm.stl_import(filepath=fp, filter_glob='*.stl') 

318 case 'usd' | 'usdc' | 'usdz': 

319 bpy.ops.wm.usd_import(filepath=fp, filter_glob='*.usd') 

320 case _: 

321 raise ValueError(f'Unknown file extension: {ext}.') 

322 

323 LOGGER.debug(f'Imported: {fp}') 

324 

325 

326def write_mesh(filepath): 

327 # type: (Filepath) -> None 

328 ''' 

329 Write scene to mesh file. 

330 The following formats are supported: 

331 

332 * abc 

333 * dae 

334 * fbx 

335 * glb 

336 * obj 

337 * stl 

338 * usd 

339 * usdc 

340 * usdz 

341 

342 Args: 

343 filepath (str): Target filepath. 

344 

345 Raises: 

346 ValueError: If file extension is unknown. 

347 ''' 

348 fp = Path(filepath).as_posix() 

349 ext = Path(fp).suffix[1:] 

350 match ext: 

351 case 'abc': 

352 bpy.ops.wm.alembic_export(filepath=fp) 

353 case 'dae': 

354 bpy.ops.wm.collada_export(filepath=fp) 

355 case 'fbx': 

356 bpy.ops.export_scene.fbx(filepath=fp) 

357 case 'glb': 

358 bpy.ops.export_scene.gltf(filepath=fp, export_materials='PLACEHOLDER') 

359 case 'obj': 

360 bpy.ops.wm.obj_export(filepath=fp, export_materials=False) 

361 case 'stl': 

362 bpy.ops.wm.stl_export(filepath=fp) 

363 case 'usd' | 'usdc' | 'usdz': 

364 bpy.ops.wm.usd_export(filepath=fp, export_materials=False) 

365 case _: 

366 raise ValueError(f'Unknown file extension: {ext}.') 

367 

368 LOGGER.debug(f'Exported: {fp}') 

369 

370 

371def convert_mesh(content, source_format, target_format): 

372 # type: (bytes, str, str) -> bytes 

373 ''' 

374 Converts a given mesh from a source format to a target format. 

375 The following formats are supported: 

376 

377 * abc 

378 * dae 

379 * fbx 

380 * glb 

381 * obj 

382 * stl 

383 * usd 

384 * usdc 

385 * usdz 

386 

387 Args: 

388 content (bytes): File content. 

389 source_format (str): Source file format. 

390 target_format (str): Target file format. 

391 

392 Returns: 

393 bytes: Target file as bytes. 

394 ''' 

395 delete_all_scenes() 

396 

397 with TemporaryDirectory(prefix='baas_') as root: 

398 source = Path(root, f'source.{source_format}') 

399 with open(source, 'wb') as f: 

400 f.write(content) 

401 read_mesh(source) 

402 

403 target = Path(root, f'target.{target_format}') 

404 write_mesh(target) 

405 with open(target, 'rb') as f: 

406 output = f.read() 

407 

408 return output