1# ##### BEGIN GPL LICENSE BLOCK ##### 2# 3# This program is free software; you can redistribute it and/or 4# modify it under the terms of the GNU General Public License 5# as published by the Free Software Foundation; either version 2 6# of the License, or (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software Foundation, 15# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# ##### END GPL LICENSE BLOCK ##### 18 19 20if "bpy" in locals(): 21 from importlib import reload 22 23 paths = reload(paths) 24 rerequests = reload(rerequests) 25 26else: 27 from blenderkit import paths, rerequests 28 29import bpy 30from mathutils import Vector 31import json 32import os 33import sys 34 35ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000 36BELOW_NORMAL_PRIORITY_CLASS = 0x00004000 37HIGH_PRIORITY_CLASS = 0x00000080 38IDLE_PRIORITY_CLASS = 0x00000040 39NORMAL_PRIORITY_CLASS = 0x00000020 40REALTIME_PRIORITY_CLASS = 0x00000100 41 42 43def get_process_flags(): 44 flags = BELOW_NORMAL_PRIORITY_CLASS 45 if sys.platform != 'win32': # TODO test this on windows 46 flags = 0 47 return flags 48 49 50def activate(ob): 51 bpy.ops.object.select_all(action='DESELECT') 52 ob.select_set(True) 53 bpy.context.view_layer.objects.active = ob 54 55 56def selection_get(): 57 aob = bpy.context.view_layer.objects.active 58 selobs = bpy.context.view_layer.objects.selected[:] 59 return (aob, selobs) 60 61 62def selection_set(sel): 63 bpy.ops.object.select_all(action='DESELECT') 64 bpy.context.view_layer.objects.active = sel[0] 65 for ob in sel[1]: 66 ob.select_set(True) 67 68 69def get_active_model(): 70 if bpy.context.view_layer.objects.active is not None: 71 ob = bpy.context.view_layer.objects.active 72 while ob.parent is not None: 73 ob = ob.parent 74 return ob 75 return None 76 77 78def get_selected_models(): 79 ''' 80 Detect all hierarchies that contain asset data from selection. Only parents that have actual ['asset data'] get returned 81 Returns 82 list of objects containing asset data. 83 84 ''' 85 obs = bpy.context.selected_objects[:] 86 done = {} 87 parents = [] 88 for ob in obs: 89 if ob not in done: 90 while ob.parent is not None and ob not in done and ob.blenderkit.asset_base_id == '' and ob.instance_collection is None: 91 done[ob] = True 92 ob = ob.parent 93 94 if ob not in parents and ob not in done: 95 if ob.blenderkit.name != '' or ob.instance_collection is not None: 96 parents.append(ob) 97 done[ob] = True 98 99 # if no blenderkit - like objects were found, use the original selection. 100 if len(parents) == 0: 101 parents = obs 102 return parents 103 104 105def get_selected_replace_adepts(): 106 ''' 107 Detect all hierarchies that contain either asset data from selection, or selected objects themselves. 108 Returns 109 list of objects for replacement. 110 111 ''' 112 obs = bpy.context.selected_objects[:] 113 done = {} 114 parents = [] 115 for selected_ob in obs: 116 ob = selected_ob 117 if ob not in done: 118 while ob.parent is not None and ob not in done and ob.blenderkit.asset_base_id == '' and ob.instance_collection is None: 119 done[ob] = True 120 # print('step,',ob.name) 121 ob = ob.parent 122 123 # print('fin', ob.name) 124 if ob not in parents and ob not in done: 125 if ob.blenderkit.name != '' or ob.instance_collection is not None: 126 parents.append(ob) 127 128 done[ob] = True 129 # print(parents) 130 # if no blenderkit - like objects were found, use the original selection. 131 if len(parents) == 0: 132 parents = obs 133 return parents 134 135 136def get_search_props(): 137 scene = bpy.context.scene 138 if scene is None: 139 return; 140 uiprops = scene.blenderkitUI 141 props = None 142 if uiprops.asset_type == 'MODEL': 143 if not hasattr(scene, 'blenderkit_models'): 144 return; 145 props = scene.blenderkit_models 146 if uiprops.asset_type == 'SCENE': 147 if not hasattr(scene, 'blenderkit_scene'): 148 return; 149 props = scene.blenderkit_scene 150 if uiprops.asset_type == 'MATERIAL': 151 if not hasattr(scene, 'blenderkit_mat'): 152 return; 153 props = scene.blenderkit_mat 154 155 if uiprops.asset_type == 'TEXTURE': 156 if not hasattr(scene, 'blenderkit_tex'): 157 return; 158 # props = scene.blenderkit_tex 159 160 if uiprops.asset_type == 'BRUSH': 161 if not hasattr(scene, 'blenderkit_brush'): 162 return; 163 props = scene.blenderkit_brush 164 return props 165 166 167def get_active_asset(): 168 scene = bpy.context.scene 169 ui_props = scene.blenderkitUI 170 if ui_props.asset_type == 'MODEL': 171 if bpy.context.view_layer.objects.active is not None: 172 ob = get_active_model() 173 return ob 174 if ui_props.asset_type == 'SCENE': 175 return bpy.context.scene 176 177 elif ui_props.asset_type == 'MATERIAL': 178 if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None: 179 return bpy.context.active_object.active_material 180 elif ui_props.asset_type == 'TEXTURE': 181 return None 182 elif ui_props.asset_type == 'BRUSH': 183 b = get_active_brush() 184 if b is not None: 185 return b 186 return None 187 188 189def get_upload_props(): 190 scene = bpy.context.scene 191 ui_props = scene.blenderkitUI 192 if ui_props.asset_type == 'MODEL': 193 if bpy.context.view_layer.objects.active is not None: 194 ob = get_active_model() 195 return ob.blenderkit 196 if ui_props.asset_type == 'SCENE': 197 s = bpy.context.scene 198 return s.blenderkit 199 elif ui_props.asset_type == 'MATERIAL': 200 if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None: 201 return bpy.context.active_object.active_material.blenderkit 202 elif ui_props.asset_type == 'TEXTURE': 203 return None 204 elif ui_props.asset_type == 'BRUSH': 205 b = get_active_brush() 206 if b is not None: 207 return b.blenderkit 208 return None 209 210 211def previmg_name(index, fullsize=False): 212 if not fullsize: 213 return '.bkit_preview_' + str(index).zfill(3) 214 else: 215 return '.bkit_preview_full_' + str(index).zfill(3) 216 217 218def get_active_brush(): 219 context = bpy.context 220 brush = None 221 if context.sculpt_object: 222 brush = context.tool_settings.sculpt.brush 223 elif context.image_paint_object: # could be just else, but for future possible more types... 224 brush = context.tool_settings.image_paint.brush 225 return brush 226 227 228def load_prefs(): 229 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences 230 # if user_preferences.api_key == '': 231 fpath = paths.BLENDERKIT_SETTINGS_FILENAME 232 if os.path.exists(fpath): 233 with open(fpath, 'r') as s: 234 prefs = json.load(s) 235 user_preferences.api_key = prefs.get('API_key', '') 236 user_preferences.global_dir = prefs.get('global_dir', paths.default_global_dict()) 237 user_preferences.api_key_refresh = prefs.get('API_key_refresh', '') 238 239 240def save_prefs(self, context): 241 # first check context, so we don't do this on registration or blender startup 242 if not bpy.app.background: # (hasattr kills blender) 243 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences 244 # we test the api key for length, so not a random accidentally typed sequence gets saved. 245 lk = len(user_preferences.api_key) 246 if 0 < lk < 25: 247 # reset the api key in case the user writes some nonsense, e.g. a search string instead of the Key 248 user_preferences.api_key = '' 249 props = get_search_props() 250 props.report = 'Login failed. Please paste a correct API Key.' 251 252 prefs = { 253 'API_key': user_preferences.api_key, 254 'API_key_refresh': user_preferences.api_key_refresh, 255 'global_dir': user_preferences.global_dir, 256 } 257 try: 258 fpath = paths.BLENDERKIT_SETTINGS_FILENAME 259 if not os.path.exists(paths._presets): 260 os.makedirs(paths._presets) 261 f = open(fpath, 'w') 262 with open(fpath, 'w') as s: 263 json.dump(prefs, s) 264 except Exception as e: 265 print(e) 266 267 268def get_hidden_texture(tpath, bdata_name, force_reload=False): 269 i = get_hidden_image(tpath, bdata_name, force_reload=force_reload) 270 bdata_name = f".{bdata_name}" 271 t = bpy.data.textures.get(bdata_name) 272 if t is None: 273 t = bpy.data.textures.new('.test', 'IMAGE') 274 if t.image != i: 275 t.image = i 276 return t 277 278 279def get_hidden_image(tpath, bdata_name, force_reload=False): 280 hidden_name = '.%s' % bdata_name 281 img = bpy.data.images.get(hidden_name) 282 283 if tpath.startswith('//'): 284 tpath = bpy.path.abspath(tpath) 285 286 gap = '\n\n\n' 287 en = '\n' 288 if img == None or (img.filepath != tpath): 289 if tpath.startswith('//'): 290 tpath = bpy.path.abspath(tpath) 291 if not os.path.exists(tpath) or os.path.isdir(tpath): 292 tpath = paths.get_addon_thumbnail_path('thumbnail_notready.jpg') 293 294 if img is None: 295 img = bpy.data.images.load(tpath) 296 img.name = hidden_name 297 else: 298 if img.filepath != tpath: 299 if img.packed_file is not None: 300 img.unpack(method='USE_ORIGINAL') 301 302 img.filepath = tpath 303 img.reload() 304 img.colorspace_settings.name = 'sRGB' 305 elif force_reload: 306 if img.packed_file is not None: 307 img.unpack(method='USE_ORIGINAL') 308 img.reload() 309 img.colorspace_settings.name = 'sRGB' 310 return img 311 312 313def get_thumbnail(name): 314 p = paths.get_addon_thumbnail_path(name) 315 name = '.%s' % name 316 img = bpy.data.images.get(name) 317 if img == None: 318 img = bpy.data.images.load(p) 319 img.colorspace_settings.name = 'sRGB' 320 img.name = name 321 img.name = name 322 323 return img 324 325 326def get_brush_props(context): 327 brush = get_active_brush() 328 if brush is not None: 329 return brush.blenderkit 330 return None 331 332 333def p(text, text1='', text2='', text3='', text4='', text5=''): 334 '''debug printing depending on blender's debug value''' 335 if bpy.app.debug_value != 0: 336 print(text, text1, text2, text3, text4, text5) 337 338 339def pprint(data): 340 '''pretty print jsons''' 341 p(json.dumps(data, indent=4, sort_keys=True)) 342 343 344def get_hierarchy(ob): 345 '''get all objects in a tree''' 346 obs = [] 347 doobs = [ob] 348 while len(doobs) > 0: 349 o = doobs.pop() 350 doobs.extend(o.children) 351 obs.append(o) 352 return obs 353 354 355def select_hierarchy(ob, state=True): 356 obs = get_hierarchy(ob) 357 for ob in obs: 358 ob.select_set(state) 359 return obs 360 361 362def delete_hierarchy(ob): 363 obs = get_hierarchy(ob) 364 bpy.ops.object.delete({"selected_objects": obs}) 365 366 367def get_bounds_snappable(obs, use_modifiers=False): 368 # progress('getting bounds of object(s)') 369 parent = obs[0] 370 while parent.parent is not None: 371 parent = parent.parent 372 maxx = maxy = maxz = -10000000 373 minx = miny = minz = 10000000 374 375 s = bpy.context.scene 376 377 obcount = 0 # calculates the mesh obs. Good for non-mesh objects 378 matrix_parent = parent.matrix_world 379 for ob in obs: 380 # bb=ob.bound_box 381 mw = ob.matrix_world 382 subp = ob.parent 383 # while parent.parent is not None: 384 # mw = 385 386 if ob.type == 'MESH' or ob.type == 'CURVE': 387 # If to_mesh() works we can use it on curves and any other ob type almost. 388 # disabled to_mesh for 2.8 by now, not wanting to use dependency graph yet. 389 depsgraph = bpy.context.evaluated_depsgraph_get() 390 391 object_eval = ob.evaluated_get(depsgraph) 392 if ob.type == 'CURVE': 393 mesh = object_eval.to_mesh() 394 else: 395 mesh = object_eval.data 396 397 # to_mesh(context.depsgraph, apply_modifiers=self.applyModifiers, calc_undeformed=False) 398 obcount += 1 399 if mesh is not None: 400 for c in mesh.vertices: 401 coord = c.co 402 parent_coord = matrix_parent.inverted() @ mw @ Vector( 403 (coord[0], coord[1], coord[2])) # copy this when it works below. 404 minx = min(minx, parent_coord.x) 405 miny = min(miny, parent_coord.y) 406 minz = min(minz, parent_coord.z) 407 maxx = max(maxx, parent_coord.x) 408 maxy = max(maxy, parent_coord.y) 409 maxz = max(maxz, parent_coord.z) 410 # bpy.data.meshes.remove(mesh) 411 if ob.type == 'CURVE': 412 object_eval.to_mesh_clear() 413 414 if obcount == 0: 415 minx, miny, minz, maxx, maxy, maxz = 0, 0, 0, 0, 0, 0 416 417 minx *= parent.scale.x 418 maxx *= parent.scale.x 419 miny *= parent.scale.y 420 maxy *= parent.scale.y 421 minz *= parent.scale.z 422 maxz *= parent.scale.z 423 424 return minx, miny, minz, maxx, maxy, maxz 425 426 427def get_bounds_worldspace(obs, use_modifiers=False): 428 # progress('getting bounds of object(s)') 429 s = bpy.context.scene 430 maxx = maxy = maxz = -10000000 431 minx = miny = minz = 10000000 432 obcount = 0 # calculates the mesh obs. Good for non-mesh objects 433 for ob in obs: 434 # bb=ob.bound_box 435 mw = ob.matrix_world 436 if ob.type == 'MESH' or ob.type == 'CURVE': 437 depsgraph = bpy.context.evaluated_depsgraph_get() 438 ob_eval = ob.evaluated_get(depsgraph) 439 mesh = ob_eval.to_mesh() 440 obcount += 1 441 if mesh is not None: 442 for c in mesh.vertices: 443 coord = c.co 444 world_coord = mw @ Vector((coord[0], coord[1], coord[2])) 445 minx = min(minx, world_coord.x) 446 miny = min(miny, world_coord.y) 447 minz = min(minz, world_coord.z) 448 maxx = max(maxx, world_coord.x) 449 maxy = max(maxy, world_coord.y) 450 maxz = max(maxz, world_coord.z) 451 ob_eval.to_mesh_clear() 452 453 if obcount == 0: 454 minx, miny, minz, maxx, maxy, maxz = 0, 0, 0, 0, 0, 0 455 return minx, miny, minz, maxx, maxy, maxz 456 457 458def is_linked_asset(ob): 459 return ob.get('asset_data') and ob.instance_collection != None 460 461 462def get_dimensions(obs): 463 minx, miny, minz, maxx, maxy, maxz = get_bounds_snappable(obs) 464 bbmin = Vector((minx, miny, minz)) 465 bbmax = Vector((maxx, maxy, maxz)) 466 dim = Vector((maxx - minx, maxy - miny, maxz - minz)) 467 return dim, bbmin, bbmax 468 469 470def requests_post_thread(url, json, headers): 471 r = rerequests.post(url, json=json, verify=True, headers=headers) 472 473 474def get_headers(api_key): 475 headers = { 476 "accept": "application/json", 477 } 478 if api_key != '': 479 headers["Authorization"] = "Bearer %s" % api_key 480 return headers 481 482 483def scale_2d(v, s, p): 484 '''scale a 2d vector with a pivot''' 485 return (p[0] + s[0] * (v[0] - p[0]), p[1] + s[1] * (v[1] - p[1])) 486 487 488def scale_uvs(ob, scale=1.0, pivot=Vector((.5, .5))): 489 mesh = ob.data 490 if len(mesh.uv_layers) > 0: 491 uv = mesh.uv_layers[mesh.uv_layers.active_index] 492 493 # Scale a UV map iterating over its coordinates to a given scale and with a pivot point 494 for uvindex in range(len(uv.data)): 495 uv.data[uvindex].uv = scale_2d(uv.data[uvindex].uv, scale, pivot) 496 497 498# map uv cubic and switch of auto tex space and set it to 1,1,1 499def automap(target_object=None, target_slot=None, tex_size=1, bg_exception=False, just_scale=False): 500 from blenderkit import bg_blender as bg 501 s = bpy.context.scene 502 mat_props = s.blenderkit_mat 503 if mat_props.automap: 504 tob = bpy.data.objects[target_object] 505 # only automap mesh models 506 if tob.type == 'MESH': 507 actob = bpy.context.active_object 508 bpy.context.view_layer.objects.active = tob 509 510 # auto tex space 511 if tob.data.use_auto_texspace: 512 tob.data.use_auto_texspace = False 513 514 if not just_scale: 515 tob.data.texspace_size = (1, 1, 1) 516 517 if 'automap' not in tob.data.uv_layers: 518 bpy.ops.mesh.uv_texture_add() 519 uvl = tob.data.uv_layers[-1] 520 uvl.name = 'automap' 521 522 # TODO limit this to active material 523 # tob.data.uv_textures['automap'].active = True 524 525 scale = tob.scale.copy() 526 527 if target_slot is not None: 528 tob.active_material_index = target_slot 529 bpy.ops.object.mode_set(mode='EDIT') 530 bpy.ops.mesh.select_all(action='DESELECT') 531 532 # this exception is just for a 2.8 background thunmbnailer crash, can be removed when material slot select works... 533 if bg_exception: 534 bpy.ops.mesh.select_all(action='SELECT') 535 else: 536 bpy.ops.object.material_slot_select() 537 538 scale = (scale.x + scale.y + scale.z) / 3.0 539 if not just_scale: 540 bpy.ops.uv.cube_project( 541 cube_size=scale * 2.0 / (tex_size), 542 correct_aspect=False) # it's * 2.0 because blender can't tell size of a unit cube :) 543 544 bpy.ops.object.editmode_toggle() 545 tob.data.uv_layers.active = tob.data.uv_layers['automap'] 546 tob.data.uv_layers["automap"].active_render = True 547 # this by now works only for thumbnail preview, but should be extended to work on arbitrary objects. 548 # by now, it takes the basic uv map = 1 meter. also, it now doeasn't respect more materials on one object, 549 # it just scales whole UV. 550 if just_scale: 551 scale_uvs(tob, scale=Vector((1 / tex_size, 1 / tex_size))) 552 bpy.context.view_layer.objects.active = actob 553 554 555def name_update(): 556 props = get_upload_props() 557 if props.name_old != props.name: 558 props.name_changed = True 559 props.name_old = props.name 560 nname = props.name.strip() 561 nname = nname.replace('_', ' ') 562 563 if nname.isupper(): 564 nname = nname.lower() 565 nname = nname[0].upper() + nname[1:] 566 props.name = nname 567 # here we need to fix the name for blender data = ' or " give problems in path evaluation down the road. 568 fname = props.name 569 fname = fname.replace('\'', '') 570 fname = fname.replace('\"', '') 571 asset = get_active_asset() 572 asset.name = fname 573 574 575def params_to_dict(params): 576 params_dict = {} 577 for p in params: 578 params_dict[p['parameterType']] = p['value'] 579 return params_dict 580 581 582def dict_to_params(inputs, parameters=None): 583 if parameters == None: 584 parameters = [] 585 for k in inputs.keys(): 586 if type(inputs[k]) == list: 587 strlist = "" 588 for idx, s in enumerate(inputs[k]): 589 strlist += s 590 if idx < len(inputs[k]) - 1: 591 strlist += ',' 592 593 value = "%s" % strlist 594 elif type(inputs[k]) != bool: 595 value = inputs[k] 596 else: 597 value = str(inputs[k]) 598 parameters.append( 599 { 600 "parameterType": k, 601 "value": value 602 }) 603 return parameters 604 605 606def user_logged_in(): 607 a = bpy.context.window_manager.get('bkit profile') 608 if a is not None: 609 return True 610 return False 611 612 613def profile_is_validator(): 614 a = bpy.context.window_manager.get('bkit profile') 615 if a is not None and a['user'].get('exmenu'): 616 return True 617 return False 618 619 620def guard_from_crash(): 621 '''Blender tends to crash when trying to run some functions with the addon going through unregistration process.''' 622 if bpy.context.preferences.addons.get('blenderkit') is None: 623 return False; 624 if bpy.context.preferences.addons['blenderkit'].preferences is None: 625 return False; 626 return True 627 628 629def get_largest_area(area_type='VIEW_3D'): 630 maxsurf = 0 631 maxa = None 632 maxw = None 633 region = None 634 for w in bpy.data.window_managers[0].windows: 635 for a in w.screen.areas: 636 if a.type == area_type: 637 asurf = a.width * a.height 638 if asurf > maxsurf: 639 maxa = a 640 maxw = w 641 maxsurf = asurf 642 643 for r in a.regions: 644 if r.type == 'WINDOW': 645 region = r 646 global active_area, active_window, active_region 647 active_window = maxw 648 active_area = maxa 649 active_region = region 650 return maxw, maxa, region 651 652 653def get_fake_context(context, area_type='VIEW_3D'): 654 C_dict = {} # context.copy() #context.copy was a source of problems - incompatibility with addons that also define context 655 C_dict.update(region='WINDOW') 656 657 try: 658 context = context.copy() 659 except Exception as e: 660 print(e) 661 print('BlenderKit: context.copy() failed. probably a colliding addon.') 662 context = {} 663 664 if context.get('area') is None or context.get('area').type != area_type: 665 w, a, r = get_largest_area(area_type=area_type) 666 if w: 667 #sometimes there is no area of the requested type. Let's face it, some people use Blender without 3d view. 668 override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} 669 C_dict.update(override) 670 # print(w,a,r) 671 return C_dict 672 673 674def label_multiline(layout, text='', icon='NONE', width=-1): 675 ''' draw a ui label, but try to split it in multiple lines.''' 676 if text.strip() == '': 677 return 678 lines = text.split('\n') 679 if width > 0: 680 threshold = int(width / 5.5) 681 else: 682 threshold = 35 683 maxlines = 8 684 li = 0 685 for l in lines: 686 while len(l) > threshold: 687 i = l.rfind(' ', 0, threshold) 688 if i < 1: 689 i = threshold 690 l1 = l[:i] 691 layout.label(text=l1, icon=icon) 692 icon = 'NONE' 693 l = l[i:].lstrip() 694 li += 1 695 if li > maxlines: 696 break; 697 if li > maxlines: 698 break; 699 layout.label(text=l, icon=icon) 700 icon = 'NONE' 701