# ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### from blenderkit import paths, rerequests, image_utils import bpy from mathutils import Vector import json import os import sys import shutil import logging import traceback import inspect bk_logger = logging.getLogger('blenderkit') ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000 BELOW_NORMAL_PRIORITY_CLASS = 0x00004000 HIGH_PRIORITY_CLASS = 0x00000080 IDLE_PRIORITY_CLASS = 0x00000040 NORMAL_PRIORITY_CLASS = 0x00000020 REALTIME_PRIORITY_CLASS = 0x00000100 def experimental_enabled(): preferences = bpy.context.preferences.addons['blenderkit'].preferences return preferences.experimental_features def get_process_flags(): flags = BELOW_NORMAL_PRIORITY_CLASS if sys.platform != 'win32': # TODO test this on windows flags = 0 return flags def activate(ob): bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) bpy.context.view_layer.objects.active = ob def selection_get(): aob = bpy.context.view_layer.objects.active selobs = bpy.context.view_layer.objects.selected[:] return (aob, selobs) def selection_set(sel): bpy.ops.object.select_all(action='DESELECT') bpy.context.view_layer.objects.active = sel[0] for ob in sel[1]: ob.select_set(True) def get_active_model(): if bpy.context.view_layer.objects.active is not None: ob = bpy.context.view_layer.objects.active while ob.parent is not None: ob = ob.parent return ob return None def get_active_HDR(): scene = bpy.context.scene ui_props = scene.blenderkitUI image = ui_props.hdr_upload_image return image def get_selected_models(): ''' Detect all hierarchies that contain asset data from selection. Only parents that have actual ['asset data'] get returned Returns list of objects containing asset data. ''' obs = bpy.context.selected_objects[:] done = {} parents = [] for ob in obs: if ob not in done: while ob.parent is not None and ob not in done and ob.blenderkit.asset_base_id == '' and ob.instance_collection is None: done[ob] = True ob = ob.parent if ob not in parents and ob not in done: if ob.blenderkit.name != '' or ob.instance_collection is not None: parents.append(ob) done[ob] = True # if no blenderkit - like objects were found, use the original selection. if len(parents) == 0: parents = obs return parents def get_selected_replace_adepts(): ''' Detect all hierarchies that contain either asset data from selection, or selected objects themselves. Returns list of objects for replacement. ''' obs = bpy.context.selected_objects[:] done = {} parents = [] for selected_ob in obs: ob = selected_ob if ob not in done: while ob.parent is not None and ob not in done and ob.blenderkit.asset_base_id == '' and ob.instance_collection is None: done[ob] = True # print('step,',ob.name) ob = ob.parent # print('fin', ob.name) if ob not in parents and ob not in done: if ob.blenderkit.name != '' or ob.instance_collection is not None: parents.append(ob) done[ob] = True # print(parents) # if no blenderkit - like objects were found, use the original selection. if len(parents) == 0: parents = obs pprint('replace adepts') pprint(str(parents)) return parents def get_search_props(): scene = bpy.context.scene if scene is None: return; uiprops = scene.blenderkitUI props = None if uiprops.asset_type == 'MODEL': if not hasattr(scene, 'blenderkit_models'): return; props = scene.blenderkit_models if uiprops.asset_type == 'SCENE': if not hasattr(scene, 'blenderkit_scene'): return; props = scene.blenderkit_scene if uiprops.asset_type == 'HDR': if not hasattr(scene, 'blenderkit_HDR'): return; props = scene.blenderkit_HDR if uiprops.asset_type == 'MATERIAL': if not hasattr(scene, 'blenderkit_mat'): return; props = scene.blenderkit_mat if uiprops.asset_type == 'TEXTURE': if not hasattr(scene, 'blenderkit_tex'): return; # props = scene.blenderkit_tex if uiprops.asset_type == 'BRUSH': if not hasattr(scene, 'blenderkit_brush'): return; props = scene.blenderkit_brush return props def get_active_asset(): scene = bpy.context.scene ui_props = scene.blenderkitUI if ui_props.asset_type == 'MODEL': if bpy.context.view_layer.objects.active is not None: ob = get_active_model() return ob if ui_props.asset_type == 'SCENE': return bpy.context.scene if ui_props.asset_type == 'HDR': return get_active_HDR() elif ui_props.asset_type == 'MATERIAL': if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None: return bpy.context.active_object.active_material elif ui_props.asset_type == 'TEXTURE': return None elif ui_props.asset_type == 'BRUSH': b = get_active_brush() if b is not None: return b return None def get_upload_props(): scene = bpy.context.scene ui_props = scene.blenderkitUI if ui_props.asset_type == 'MODEL': if bpy.context.view_layer.objects.active is not None: ob = get_active_model() return ob.blenderkit if ui_props.asset_type == 'SCENE': s = bpy.context.scene return s.blenderkit if ui_props.asset_type == 'HDR': hdr = ui_props.hdr_upload_image#bpy.data.images.get(ui_props.hdr_upload_image) if not hdr: return None return hdr.blenderkit elif ui_props.asset_type == 'MATERIAL': if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None: return bpy.context.active_object.active_material.blenderkit elif ui_props.asset_type == 'TEXTURE': return None elif ui_props.asset_type == 'BRUSH': b = get_active_brush() if b is not None: return b.blenderkit return None def previmg_name(index, fullsize=False): if not fullsize: return '.bkit_preview_' + str(index).zfill(3) else: return '.bkit_preview_full_' + str(index).zfill(3) def get_active_brush(): context = bpy.context brush = None if context.sculpt_object: brush = context.tool_settings.sculpt.brush elif context.image_paint_object: # could be just else, but for future possible more types... brush = context.tool_settings.image_paint.brush return brush def load_prefs(): user_preferences = bpy.context.preferences.addons['blenderkit'].preferences # if user_preferences.api_key == '': fpath = paths.BLENDERKIT_SETTINGS_FILENAME if os.path.exists(fpath): try: with open(fpath, 'r', encoding = 'utf-8') as s: prefs = json.load(s) user_preferences.api_key = prefs.get('API_key', '') user_preferences.global_dir = prefs.get('global_dir', paths.default_global_dict()) user_preferences.api_key_refresh = prefs.get('API_key_refresh', '') except Exception as e: print('failed to read addon preferences.') print(e) os.remove(fpath) def save_prefs(self, context): # first check context, so we don't do this on registration or blender startup if not bpy.app.background: # (hasattr kills blender) user_preferences = bpy.context.preferences.addons['blenderkit'].preferences # we test the api key for length, so not a random accidentally typed sequence gets saved. lk = len(user_preferences.api_key) if 0 < lk < 25: # reset the api key in case the user writes some nonsense, e.g. a search string instead of the Key user_preferences.api_key = '' props = get_search_props() props.report = 'Login failed. Please paste a correct API Key.' prefs = { 'API_key': user_preferences.api_key, 'API_key_refresh': user_preferences.api_key_refresh, 'global_dir': user_preferences.global_dir, } try: fpath = paths.BLENDERKIT_SETTINGS_FILENAME if not os.path.exists(paths._presets): os.makedirs(paths._presets) with open(fpath, 'w', encoding = 'utf-8') as s: json.dump(prefs, s, ensure_ascii=False, indent=4) except Exception as e: print(e) def uploadable_asset_poll(): '''returns true if active asset type can be uploaded''' ui_props = bpy.context.scene.blenderkitUI if ui_props.asset_type == 'MODEL': return bpy.context.view_layer.objects.active is not None if ui_props.asset_type == 'MATERIAL': return bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None if ui_props.asset_type == 'HDR': return ui_props.hdr_upload_image is not None return True def get_hidden_texture(img, force_reload=False): # i = get_hidden_image(tpath, bdata_name, force_reload=force_reload) # bdata_name = f".{bdata_name}" t = bpy.data.textures.get(img.name) if t is None: t = bpy.data.textures.new(img.name, 'IMAGE') if t.image != img: t.image = img return t def get_hidden_image(tpath, bdata_name, force_reload=False, colorspace = 'sRGB'): if bdata_name[0] == '.': hidden_name = bdata_name else: hidden_name = '.%s' % bdata_name img = bpy.data.images.get(hidden_name) if tpath.startswith('//'): tpath = bpy.path.abspath(tpath) if img == None or (img.filepath != tpath): if tpath.startswith('//'): tpath = bpy.path.abspath(tpath) if not os.path.exists(tpath) or os.path.isdir(tpath): tpath = paths.get_addon_thumbnail_path('thumbnail_notready.jpg') if img is None: img = bpy.data.images.load(tpath) img.name = hidden_name else: if img.filepath != tpath: if img.packed_file is not None: img.unpack(method='USE_ORIGINAL') img.filepath = tpath img.reload() image_utils.set_colorspace(img,colorspace) elif force_reload: if img.packed_file is not None: img.unpack(method='USE_ORIGINAL') img.reload() image_utils.set_colorspace(img,colorspace) return img def get_thumbnail(name): p = paths.get_addon_thumbnail_path(name) name = '.%s' % name img = bpy.data.images.get(name) if img == None: img = bpy.data.images.load(p) image_utils.set_colorspace(img,'sRGB') img.name = name img.name = name return img def files_size_to_text(size): fsmb = size // (1024 * 1024) fskb = size % 1024 if fsmb == 0: return f'{fskb}KB' else: return f'{fsmb}MB {fskb}KB' def get_brush_props(context): brush = get_active_brush() if brush is not None: return brush.blenderkit return None def p(text, text1='', text2='', text3='', text4='', text5='', level = 'DEBUG'): '''debug printing depending on blender's debug value''' if 1:#bpy.app.debug_value != 0: # print('-----BKit debug-----\n') # traceback.print_stack() texts = [text1,text2,text3,text4,text5] text = str(text) for t in texts: if t!= '': text += ' ' + str(t) bk_logger.debug(text) # print('---------------------\n') def copy_asset(fp1, fp2): '''synchronizes the asset between folders, including it's texture subdirectories''' if 1: bk_logger.debug('copy asset') bk_logger.debug(fp1 +' '+ fp2) if not os.path.exists(fp2): shutil.copyfile(fp1, fp2) bk_logger.debug('copied') source_dir = os.path.dirname(fp1) target_dir = os.path.dirname(fp2) for subdir in os.scandir(source_dir): if not subdir.is_dir(): continue target_subdir = os.path.join(target_dir, subdir.name) if os.path.exists(target_subdir): continue bk_logger.debug(str(subdir) +' '+ str(target_subdir)) shutil.copytree(subdir, target_subdir) bk_logger.debug('copied') # except Exception as e: # print('BlenderKit failed to copy asset') # print(fp1, fp2) # print(e) def pprint(data, data1=None, data2=None, data3=None, data4=None): '''pretty print jsons''' p(json.dumps(data, indent=4, sort_keys=True)) def get_hierarchy(ob): '''get all objects in a tree''' obs = [] doobs = [ob] # pprint('get hierarchy') pprint(ob.name) while len(doobs) > 0: o = doobs.pop() doobs.extend(o.children) obs.append(o) return obs def select_hierarchy(ob, state=True): obs = get_hierarchy(ob) for ob in obs: ob.select_set(state) return obs def delete_hierarchy(ob): obs = get_hierarchy(ob) bpy.ops.object.delete({"selected_objects": obs}) def get_bounds_snappable(obs, use_modifiers=False): # progress('getting bounds of object(s)') parent = obs[0] while parent.parent is not None: parent = parent.parent maxx = maxy = maxz = -10000000 minx = miny = minz = 10000000 s = bpy.context.scene obcount = 0 # calculates the mesh obs. Good for non-mesh objects matrix_parent = parent.matrix_world for ob in obs: # bb=ob.bound_box mw = ob.matrix_world subp = ob.parent # while parent.parent is not None: # mw = if ob.type == 'MESH' or ob.type == 'CURVE': # If to_mesh() works we can use it on curves and any other ob type almost. # disabled to_mesh for 2.8 by now, not wanting to use dependency graph yet. depsgraph = bpy.context.evaluated_depsgraph_get() object_eval = ob.evaluated_get(depsgraph) if ob.type == 'CURVE': mesh = object_eval.to_mesh() else: mesh = object_eval.data # to_mesh(context.depsgraph, apply_modifiers=self.applyModifiers, calc_undeformed=False) obcount += 1 if mesh is not None: for c in mesh.vertices: coord = c.co parent_coord = matrix_parent.inverted() @ mw @ Vector( (coord[0], coord[1], coord[2])) # copy this when it works below. minx = min(minx, parent_coord.x) miny = min(miny, parent_coord.y) minz = min(minz, parent_coord.z) maxx = max(maxx, parent_coord.x) maxy = max(maxy, parent_coord.y) maxz = max(maxz, parent_coord.z) # bpy.data.meshes.remove(mesh) if ob.type == 'CURVE': object_eval.to_mesh_clear() if obcount == 0: minx, miny, minz, maxx, maxy, maxz = 0, 0, 0, 0, 0, 0 minx *= parent.scale.x maxx *= parent.scale.x miny *= parent.scale.y maxy *= parent.scale.y minz *= parent.scale.z maxz *= parent.scale.z return minx, miny, minz, maxx, maxy, maxz def get_bounds_worldspace(obs, use_modifiers=False): # progress('getting bounds of object(s)') s = bpy.context.scene maxx = maxy = maxz = -10000000 minx = miny = minz = 10000000 obcount = 0 # calculates the mesh obs. Good for non-mesh objects for ob in obs: # bb=ob.bound_box mw = ob.matrix_world if ob.type == 'MESH' or ob.type == 'CURVE': depsgraph = bpy.context.evaluated_depsgraph_get() ob_eval = ob.evaluated_get(depsgraph) mesh = ob_eval.to_mesh() obcount += 1 if mesh is not None: for c in mesh.vertices: coord = c.co world_coord = mw @ Vector((coord[0], coord[1], coord[2])) minx = min(minx, world_coord.x) miny = min(miny, world_coord.y) minz = min(minz, world_coord.z) maxx = max(maxx, world_coord.x) maxy = max(maxy, world_coord.y) maxz = max(maxz, world_coord.z) ob_eval.to_mesh_clear() if obcount == 0: minx, miny, minz, maxx, maxy, maxz = 0, 0, 0, 0, 0, 0 return minx, miny, minz, maxx, maxy, maxz def is_linked_asset(ob): return ob.get('asset_data') and ob.instance_collection != None def get_dimensions(obs): minx, miny, minz, maxx, maxy, maxz = get_bounds_snappable(obs) bbmin = Vector((minx, miny, minz)) bbmax = Vector((maxx, maxy, maxz)) dim = Vector((maxx - minx, maxy - miny, maxz - minz)) return dim, bbmin, bbmax def requests_post_thread(url, json, headers): r = rerequests.post(url, json=json, verify=True, headers=headers) def get_headers(api_key): headers = { "accept": "application/json", } if api_key != '': headers["Authorization"] = "Bearer %s" % api_key return headers def scale_2d(v, s, p): '''scale a 2d vector with a pivot''' return (p[0] + s[0] * (v[0] - p[0]), p[1] + s[1] * (v[1] - p[1])) def scale_uvs(ob, scale=1.0, pivot=Vector((.5, .5))): mesh = ob.data if len(mesh.uv_layers) > 0: uv = mesh.uv_layers[mesh.uv_layers.active_index] # Scale a UV map iterating over its coordinates to a given scale and with a pivot point for uvindex in range(len(uv.data)): uv.data[uvindex].uv = scale_2d(uv.data[uvindex].uv, scale, pivot) # map uv cubic and switch of auto tex space and set it to 1,1,1 def automap(target_object=None, target_slot=None, tex_size=1, bg_exception=False, just_scale=False): s = bpy.context.scene mat_props = s.blenderkit_mat if mat_props.automap: tob = bpy.data.objects[target_object] # only automap mesh models if tob.type == 'MESH' and len(tob.data.polygons) > 0: # check polycount for a rare case where no polys are in editmesh actob = bpy.context.active_object bpy.context.view_layer.objects.active = tob # auto tex space if tob.data.use_auto_texspace: tob.data.use_auto_texspace = False if not just_scale: tob.data.texspace_size = (1, 1, 1) if 'automap' not in tob.data.uv_layers: bpy.ops.mesh.uv_texture_add() uvl = tob.data.uv_layers[-1] uvl.name = 'automap' # TODO limit this to active material # tob.data.uv_textures['automap'].active = True scale = tob.scale.copy() if target_slot is not None: tob.active_material_index = target_slot bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='DESELECT') # this exception is just for a 2.8 background thunmbnailer crash, can be removed when material slot select works... if bg_exception: bpy.ops.mesh.select_all(action='SELECT') else: bpy.ops.object.material_slot_select() scale = (scale.x + scale.y + scale.z) / 3.0 if not just_scale: bpy.ops.uv.cube_project( cube_size=scale * 2.0 / (tex_size), correct_aspect=False) # it's * 2.0 because blender can't tell size of a unit cube :) bpy.ops.object.editmode_toggle() tob.data.uv_layers.active = tob.data.uv_layers['automap'] tob.data.uv_layers["automap"].active_render = True # this by now works only for thumbnail preview, but should be extended to work on arbitrary objects. # by now, it takes the basic uv map = 1 meter. also, it now doeasn't respect more materials on one object, # it just scales whole UV. if just_scale: scale_uvs(tob, scale=Vector((1 / tex_size, 1 / tex_size))) bpy.context.view_layer.objects.active = actob def name_update(): scene = bpy.context.scene ui_props = scene.blenderkitUI props = get_upload_props() if props.name_old != props.name: props.name_changed = True props.name_old = props.name nname = props.name.strip() nname = nname.replace('_', ' ') if nname.isupper(): nname = nname.lower() nname = nname[0].upper() + nname[1:] props.name = nname # here we need to fix the name for blender data = ' or " give problems in path evaluation down the road. fname = props.name fname = fname.replace('\'', '') fname = fname.replace('\"', '') asset = get_active_asset() if ui_props.asset_type != 'HDR': # Here we actually rename assets datablocks, but don't do that with HDR's and possibly with others asset.name = fname def get_param(asset_data, parameter_name): if not asset_data.get('parameters'): # this can appear in older version files. return None for p in asset_data['parameters']: if p.get('parameterType') == parameter_name: return p['value'] return None def params_to_dict(params): params_dict = {} for p in params: params_dict[p['parameterType']] = p['value'] return params_dict def dict_to_params(inputs, parameters=None): if parameters == None: parameters = [] for k in inputs.keys(): if type(inputs[k]) == list: strlist = "" for idx, s in enumerate(inputs[k]): strlist += s if idx < len(inputs[k]) - 1: strlist += ',' value = "%s" % strlist elif type(inputs[k]) != bool: value = inputs[k] else: value = str(inputs[k]) parameters.append( { "parameterType": k, "value": value }) return parameters def update_tags(self, context): props = self commasep = props.tags.split(',') ntags = [] for tag in commasep: if len(tag) > 19: short_tags = tag.split(' ') for short_tag in short_tags: if len(short_tag) > 19: short_tag = short_tag[:18] ntags.append(short_tag) else: ntags.append(tag) if len(ntags) == 1: ntags = ntags[0].split(' ') ns = '' for t in ntags: if t != '': ns += t + ',' ns = ns[:-1] if props.tags != ns: props.tags = ns def user_logged_in(): a = bpy.context.window_manager.get('bkit profile') if a is not None: return True return False def profile_is_validator(): a = bpy.context.window_manager.get('bkit profile') if a is not None and a['user'].get('exmenu'): return True return False def guard_from_crash(): '''Blender tends to crash when trying to run some functions with the addon going through unregistration process.''' if bpy.context.preferences.addons.get('blenderkit') is None: return False; if bpy.context.preferences.addons['blenderkit'].preferences is None: return False; return True def get_largest_area(area_type='VIEW_3D'): maxsurf = 0 maxa = None maxw = None region = None for w in bpy.data.window_managers[0].windows: for a in w.screen.areas: if a.type == area_type: asurf = a.width * a.height if asurf > maxsurf: maxa = a maxw = w maxsurf = asurf for r in a.regions: if r.type == 'WINDOW': region = r global active_area_pointer, active_window_pointer, active_region_pointer active_window_pointer = maxw.as_pointer() active_area_pointer = maxa.as_pointer() active_region_pointer = region.as_pointer() return maxw, maxa, region def get_fake_context(context, area_type='VIEW_3D'): C_dict = {} # context.copy() #context.copy was a source of problems - incompatibility with addons that also define context C_dict.update(region='WINDOW') # try: # context = context.copy() # # print('bk context copied successfully') # except Exception as e: # print(e) # print('BlenderKit: context.copy() failed. Can be a colliding addon.') context = {} if context.get('area') is None or context.get('area').type != area_type: w, a, r = get_largest_area(area_type=area_type) if w: # sometimes there is no area of the requested type. Let's face it, some people use Blender without 3d view. override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} C_dict.update(override) # print(w,a,r) return C_dict def label_multiline(layout, text='', icon='NONE', width=-1): ''' draw a ui label, but try to split it in multiple lines.''' if text.strip() == '': return lines = text.split('\n') if width > 0: threshold = int(width / 5.5) else: threshold = 35 maxlines = 8 li = 0 for l in lines: while len(l) > threshold: i = l.rfind(' ', 0, threshold) if i < 1: i = threshold l1 = l[:i] layout.label(text=l1, icon=icon) icon = 'NONE' l = l[i:].lstrip() li += 1 if li > maxlines: break; if li > maxlines: break; layout.label(text=l, icon=icon) icon = 'NONE' def trace(): traceback.print_stack()