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
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 01:40 +0000
1from typing import Union
3from pathlib import Path
4from tempfile import TemporaryDirectory
6import bpy
7import bmesh
9import logging
10LOGGER = logging.getLogger(__name__)
12Filepath = Union[str, Path]
15# SCENE-FUNCTIONS---------------------------------------------------------------
16def set_scene(scene):
17 '''
18 Set's Blender's scene context ot given scene
20 scene - Blender scene object.
21 '''
22 bpy.context.window.scene = scene
25def delete_scene(scene):
26 '''
27 Deletes given scene from Blender.
29 scene - Blender scene.
30 '''
31 set_scene(scene)
32 bpy.ops.scene.delete()
33 LOGGER.debug('Scene deleted.')
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)
45 old_scene = bpy.data.scenes[0]
46 set_scene(old_scene)
47 delete_all_objects()
49 bpy.ops.scene.new(type='EMPTY')
50 scene = bpy.data.scenes[-1]
51 set_scene(scene)
52 delete_all_objects()
54 set_scene(old_scene)
55 delete_all_objects()
56 delete_scene(old_scene)
58 bpy.data.scenes[0].name = 'Scene'
59 LOGGER.debug('All scenes deleted.')
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.')
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.')
79# OBJECT-FUNCTIONS-----------------------------------------------------------
80def select_object(object_):
81 '''
82 Selects given Blender object.
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_
91def deselect_object(object_):
92 '''
93 Deselect given Blender object.
95 Args:
96 object_ (bpy object): Blender object to be deselecoted.
97 '''
98 object_.select_set(False)
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)
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
118def filter_select_objects(pattern):
119 '''
120 Clears selected objects and then selects Blender objects according to given
121 pattern.
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)
130def delete_object(object_):
131 '''
132 Deletes given object_.
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()
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()
151def filter_objects(object_type):
152 '''
153 Filters Blender objects by object type.
155 Args:
156 object_type (str): Type of Blender object.
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 ))
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.
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()
187def deselect_faces(object_, indices):
188 '''
189 Deselect faces on given Blender mesh object according to according to given
190 face indices.
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()
207def select_all_faces(object_):
208 '''
209 Select all faces of the given Blender mesh object.
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')
221def deselect_all_faces(object_):
222 '''
223 Deselect all faces of the given Blender mesh object.
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')
235def triangulate_faces(object_):
236 '''
237 Triangulate currenly selected faces of given Blender mesh object.
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()
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)
257def mesh_to_pydata(mesh):
258 '''
259 Converts a given Blender mesh in to a tuple of vertices, edges and faces.
261 Args:
262 mesh (bpy mesh): Blender mesh object.
264 Returns:
265 tuple: (vertices, edges, faces).
266 '''
267 verts = list(map(lambda x: list(x.co), mesh.data.vertices.values()))
269 edges = mesh.data.edge_keys
270 edges = sorted([sorted(list(x)) for x in edges])
272 faces = list(map(lambda x: list(x.vertices), mesh.data.polygons.values()))
274 return (verts, edges, faces)
277# IO-FUNCTIONS------------------------------------------------------------------
278def read_mesh(filepath):
279 # type: (Filepath) -> None
280 '''
281 Read mesh from given filepath.
282 The following formats are supported:
284 * abc
285 * dae
286 * fbx
287 * glb
288 * obj
289 * stl
290 * usd
291 * usdc
292 * usdz
294 Args:
295 filepath (str): Path to mesh file.
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}'
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}.')
323 LOGGER.debug(f'Imported: {fp}')
326def write_mesh(filepath):
327 # type: (Filepath) -> None
328 '''
329 Write scene to mesh file.
330 The following formats are supported:
332 * abc
333 * dae
334 * fbx
335 * glb
336 * obj
337 * stl
338 * usd
339 * usdc
340 * usdz
342 Args:
343 filepath (str): Target filepath.
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}.')
368 LOGGER.debug(f'Exported: {fp}')
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:
377 * abc
378 * dae
379 * fbx
380 * glb
381 * obj
382 * stl
383 * usd
384 * usdc
385 * usdz
387 Args:
388 content (bytes): File content.
389 source_format (str): Source file format.
390 target_format (str): Target file format.
392 Returns:
393 bytes: Target file as bytes.
394 '''
395 delete_all_scenes()
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)
403 target = Path(root, f'target.{target_format}')
404 write_mesh(target)
405 with open(target, 'rb') as f:
406 output = f.read()
408 return output