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