Newer
Older
# ##### 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 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
'''
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:
# if no blenderkit - like objects were found, use the original selection.
if len(parents) == 0:
parents = obs
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
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()
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
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:
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):
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)
# 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)
t = bpy.data.textures.new(img.name, 'IMAGE')
if t.image != img:
t.image = img
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)
# print('---------------------\n')
def copy_asset(fp1, fp2):
'''synchronizes the asset between folders, including it's texture subdirectories'''
if 1:
bk_logger.debug('copy asset')
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))
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):
p(json.dumps(data, indent=4, sort_keys=True))
'''get all objects in a tree'''
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)
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()
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)
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)))
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
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;
def get_largest_area(area_type='VIEW_3D'):
maxsurf = 0
maxa = None
maxw = None
region = None
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)
Vilém Duha
committed
if w:
# sometimes there is no area of the requested type. Let's face it, some people use Blender without 3d view.
Vilém Duha
committed
override = {'window': w, 'screen': w.screen, 'area': a, 'region': r}
C_dict.update(override)
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
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)