Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVilem Duha <vilem.duha@gmail.com>2019-03-29 19:52:47 +0300
committerVilem Duha <vilem.duha@gmail.com>2019-03-29 19:52:47 +0300
commit50ea2790f91788f247225be88b8e7a97c7bfb937 (patch)
treedf7e480fe061f594823e882dca0a0a7118f4a718
parent69c01cad71191de344dbfd66c45640d0ec281bc3 (diff)
BlenderKit initial commit.
BlenderKit add-on is the official addon of the BlenderKit service for Blender 3d. (www.blenderkit.com) It enables users to upload, search, download, and rate different assets for blender. It works together with BlenderKit server.
-rw-r--r--blenderkit/README.md3
-rw-r--r--blenderkit/__init__.py1410
-rw-r--r--blenderkit/append_link.py193
-rw-r--r--blenderkit/asset_inspector.py376
-rw-r--r--blenderkit/autothumb.py329
-rw-r--r--blenderkit/autothumb_material_bg.py121
-rw-r--r--blenderkit/autothumb_model_bg.py158
-rw-r--r--blenderkit/bg_blender.py237
-rw-r--r--blenderkit/blendfiles/cleaned.blendbin0 -> 41012 bytes
-rw-r--r--blenderkit/blendfiles/material_thumbnailer_cycles.blendbin0 -> 558984 bytes
-rw-r--r--blenderkit/blendfiles/thumbnailer.blendbin0 -> 211100 bytes
-rw-r--r--blenderkit/categories.py89
-rw-r--r--blenderkit/data/categories.json3145
-rw-r--r--blenderkit/download.py832
-rw-r--r--blenderkit/overrides.py261
-rw-r--r--blenderkit/paths.py217
-rw-r--r--blenderkit/ratings.py207
-rw-r--r--blenderkit/registration.py101
-rw-r--r--blenderkit/search.py998
-rw-r--r--blenderkit/thumbnails/arrow_left.pngbin0 -> 2119 bytes
-rw-r--r--blenderkit/thumbnails/arrow_right.pngbin0 -> 2084 bytes
-rw-r--r--blenderkit/thumbnails/bar_slider.pngbin0 -> 861 bytes
-rw-r--r--blenderkit/thumbnails/locked.pngbin0 -> 2184 bytes
-rw-r--r--blenderkit/thumbnails/locked_large.pngbin0 -> 3170 bytes
-rw-r--r--blenderkit/thumbnails/rating_ui.pngbin0 -> 100234 bytes
-rw-r--r--blenderkit/thumbnails/star_white.pngbin0 -> 1552 bytes
-rw-r--r--blenderkit/thumbnails/thumbnail_notready.jpgbin0 -> 13810 bytes
-rw-r--r--blenderkit/thumbnails/vs_deleted.pngbin0 -> 2151 bytes
-rw-r--r--blenderkit/thumbnails/vs_on_hold.pngbin0 -> 1583 bytes
-rw-r--r--blenderkit/thumbnails/vs_ready.pngbin0 -> 2137 bytes
-rw-r--r--blenderkit/thumbnails/vs_uploaded.pngbin0 -> 1801 bytes
-rw-r--r--blenderkit/ui.py1424
-rw-r--r--blenderkit/ui_bgl.py127
-rw-r--r--blenderkit/ui_panels.py769
-rw-r--r--blenderkit/upload.py765
-rw-r--r--blenderkit/upload_bg.py199
-rw-r--r--blenderkit/utils.py415
-rw-r--r--blenderkit/version_checker.py73
38 files changed, 12449 insertions, 0 deletions
diff --git a/blenderkit/README.md b/blenderkit/README.md
new file mode 100644
index 00000000..e2e77067
--- /dev/null
+++ b/blenderkit/README.md
@@ -0,0 +1,3 @@
+BlenderKit add-on is the official addon of the BlenderKit service for Blender 3d.
+It enables users to upload, search, download, and rate different assets for blender.
+It works together with BlenderKit server. \ No newline at end of file
diff --git a/blenderkit/__init__.py b/blenderkit/__init__.py
new file mode 100644
index 00000000..017da14d
--- /dev/null
+++ b/blenderkit/__init__.py
@@ -0,0 +1,1410 @@
+# ##### 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 #####
+
+bl_info = {
+ "name": "Blender Kit",
+ "author": "Vilem Duha",
+ "version": (1, 0, 22),
+ "blender": (2, 80, 0),
+ "location": "View3D > Properties > BlenderKit",
+ "description": "Online BlenderKit library (materials, models, brushes and more)",
+ "warning": "",
+ "wiki_url": "https://youtu.be/1hVgcQhIAo8"
+ "Scripts/Add_Mesh/BlenderKit",
+ "category": "Add Mesh",
+}
+
+if "bpy" in locals():
+ import importlib
+
+ importlib.reload(asset_inspector)
+ importlib.reload(search)
+ importlib.reload(download)
+ importlib.reload(ratings)
+ importlib.reload(autothumb)
+ importlib.reload(ui)
+ importlib.reload(bg_blender)
+ importlib.reload(paths)
+ importlib.reload(utils)
+else:
+ from blenderkit import asset_inspector, search, download, upload, ratings, autothumb, ui, bg_blender, paths, utils, \
+ overrides, ui_panels, categories
+
+import os
+import math
+import time
+# import logging
+import bpy
+
+from bpy.app.handlers import persistent
+import bpy.utils.previews
+import mathutils
+from mathutils import Vector
+from bpy.props import (
+ IntProperty,
+ FloatProperty,
+ FloatVectorProperty,
+ StringProperty,
+ EnumProperty,
+ BoolProperty,
+ PointerProperty,
+)
+from bpy.types import (
+ Operator,
+ Panel,
+ AddonPreferences,
+ PropertyGroup,
+)
+
+
+# logging.basicConfig(filename = 'blenderkit.log', level = logging.INFO,
+# format = ' %(asctime)s:%(filename)s:%(funcName)s:%(lineno)d:%(message)s')
+
+
+@persistent
+def scene_load(context):
+ search.load_previews()
+ ui_props = bpy.context.scene.blenderkitUI
+ ui_props.assetbar_on = False
+ ui_props.turn_off = False
+
+
+licenses = (
+ ('royalty_free', 'Royalty Free', 'royalty free commercial license'),
+ ('cc_zero', 'Creative Commons Zero', 'Creative Commons Zero'),
+)
+conditions = (
+ ('UNSPECIFIED', 'Unspecified', "Don't use this in search"),
+ ('NEW', 'New', 'Shiny new item'),
+ ('USED', 'Used', 'Casually used item'),
+ ('OLD', 'Old', 'Old item'),
+ ('DESOLATE', 'Desolate', 'Desolate item - dusty & rusty'),
+)
+model_styles = (
+ ('REALISTIC', 'Realistic', "photo realistic model"),
+ ('PAINTERLY', 'Painterly', 'hand painted with visible strokes, mostly for games'),
+ ('LOWPOLY', 'Lowpoly', "Lowpoly art -don't mix up with polycount!"),
+ ('ANIME', 'Anime', 'Anime style'),
+ ('2D VECTOR', '2d Vector', '2d vector'),
+ ('3D GRAPHICS', '3d Graphics', '3d graphics'),
+ ('OTHER', 'Other', 'Other style'),
+)
+search_model_styles = (
+ ('REALISTIC', 'Realistic', "photo realistic model"),
+ ('PAINTERLY', 'Painterly', 'hand painted with visible strokes, mostly for games'),
+ ('LOWPOLY', 'Lowpoly', "Lowpoly art -don't mix up with polycount!"),
+ ('ANIME', 'Anime', 'Anime style'),
+ ('2D VECTOR', '2d Vector', '2d vector'),
+ ('3D GRAPHICS', '3d Graphics', '3d graphics'),
+ ('OTHER', 'Other', 'Other Style'),
+ ('ANY', 'Any', 'Any Style'),
+)
+material_styles = (
+ ('REALISTIC', 'Realistic', "photo realistic model"),
+ ('NPR', 'Non photorealistic', 'hand painted with visible strokes, mostly for games'),
+ ('OTHER', 'Other', 'Other style'),
+)
+search_material_styles = (
+ ('REALISTIC', 'Realistic', "photo realistic model"),
+ ('NPR', 'Non photorealistic', 'hand painted with visible strokes, mostly for games'),
+ ('ANY', 'Any', 'Any'),
+)
+engines = (
+ ('CYCLES', 'Cycles', 'blender cycles pathtracer'),
+ ('EEVEE', 'Eevee', 'blender eevee renderer'),
+ ('OCTANE', 'Octane', 'octane render enginge'),
+ ('ARNOLD', 'Arnold', 'arnold render engine'),
+ ('V-RAY', 'V-Ray', 'V-Ray renderer'),
+ ('UNREAL', 'Unreal', 'Unreal engine'),
+ ('UNITY', 'Unity', 'Unity engine'),
+ ('GODOT', 'Godot', 'Godot engine'),
+ ('3D-PRINT', '3d printer', 'object can be 3d printed'),
+ ('OTHER', 'Other', 'any other engine'),
+ ('NONE', 'None', 'no more engine block.'),
+)
+pbr_types = (
+ ('METALLIC', 'Metallic-Roughness', 'Metallic/Roughness PBR material type'),
+ ('SPECULAR', 'Specular Glossy', ''),
+)
+
+mesh_poly_types = (
+ ('QUAD', 'quad', ''),
+ ('QUAD_DOMINANT', 'quad_dominant', ''),
+ ('TRI_DOMINANT', 'tri_dominant', ''),
+ ('TRI', 'tri', ''),
+ ('NGON', 'ngon_dominant', ''),
+ ('OTHER', 'other', ''),
+)
+
+thumbnail_angles = (
+ ('DEFAULT', 'default', ''),
+ ('FRONT', 'front', ''),
+ ('SIDE', 'side', ''),
+ ('TOP', 'top', ''),
+)
+
+thumbnail_snap = (
+ ('GROUND', 'ground', ''),
+ ('WALL', 'wall', ''),
+ ('CEILING', 'ceiling', ''),
+ ('FLOAT', 'floating', ''),
+)
+
+
+def get_upload_asset_type(self):
+ typemapper = {
+ BlenderKitModelUploadProps: 'model',
+ BlenderKitSceneUploadProps: 'scene',
+ BlenderKitMaterialUploadProps: 'material',
+ BlenderKitBrushUploadProps: 'brush'
+ }
+ asset_type = typemapper[type(self)]
+ return asset_type
+
+
+def get_subcategory_enums(self, context):
+ wm = bpy.context.window_manager
+ asset_type = get_upload_asset_type(self)
+ items = []
+ if self.category != '':
+ asset_categories = categories.get_category(wm['bkit_categories'], cat_path=(asset_type, self.category,))
+ for c in asset_categories['children']:
+ items.append((c['slug'], c['name'], c['description']))
+
+ return items
+
+
+def get_category_enums(self, context):
+ wm = bpy.context.window_manager
+ asset_type = get_upload_asset_type(self)
+ asset_categories = categories.get_category(wm['bkit_categories'], cat_path=(asset_type,))
+ items = []
+ for c in asset_categories['children']:
+ items.append((c['slug'], c['name'], c['description']))
+ return items
+
+
+def switch_search_results(self, context):
+ s = bpy.context.scene
+ props = s.blenderkitUI
+ if props.asset_type == 'MODEL':
+ s['search results'] = s.get('bkit model search')
+ elif props.asset_type == 'SCENE':
+ s['search results'] = s.get('bkit scene search')
+ elif props.asset_type == 'MATERIAL':
+ s['search results'] = s.get('bkit material search')
+ elif props.asset_type == 'TEXTURE':
+ s['search results'] = s.get('bkit texture search')
+ elif props.asset_type == 'BRUSH':
+ s['search results'] = s.get('bkit brush search')
+ search.load_previews()
+
+
+class BlenderKitUIProps(PropertyGroup):
+ down_up: EnumProperty(
+ name="Download vs Upload",
+ items=(
+ ('SEARCH', 'Search', 'Searching is active', 'VIEWZOOM', 0),
+ ('UPLOAD', 'Upload', 'Uploads are active', 'COPYDOWN', 1),
+ # ('RATING', 'Rating', 'Rating is active', 'SOLO_ON', 2)
+ ),
+ description="BLenderKit",
+ default="SEARCH",
+ )
+ asset_type: EnumProperty(
+ name="Active Asset Type",
+ items=(
+ ('MODEL', 'Model', 'Browse models', 'OBJECT_DATAMODE', 0),
+ ('SCENE', 'SCENE', 'Browse scenes', 'SCENE_DATA', 1),
+ ('MATERIAL', 'Material', 'Browse models', 'MATERIAL', 2),
+ # ('TEXTURE', 'Texture', 'Browse textures', 'TEXTURE', 3),
+ ('BRUSH', 'Brush', 'Browse brushes', 'BRUSH_DATA', 3)
+ ),
+ description="Activate asset in UI",
+ default="MATERIAL",
+ update=switch_search_results
+ )
+ # these aren't actually used ( by now, seems to better use globals in UI module:
+ draw_tooltip: BoolProperty(name="Draw Tooltip", default=False)
+ addon_update: BoolProperty(name="Should Update Addon", default=False)
+ tooltip: StringProperty(
+ name="Tooltip",
+ description="asset preview info",
+ default="")
+
+ ui_scale = 1
+
+ thumb_size_def = 96
+ margin_def = 0
+
+ thumb_size: IntProperty(name="Thumbnail Size", default=thumb_size_def, min=-1, max=256)
+
+ margin: IntProperty(name="Margin", default=margin_def, min=-1, max=256)
+ highlight_margin: IntProperty(name="Higlight Margin", default=int(margin_def / 2), min=-10, max=256)
+
+ bar_height: IntProperty(name="Bar Height", default=thumb_size_def + 2 * margin_def, min=-1, max=2048)
+ bar_x_offset: IntProperty(name="Bar X Offset", default=20, min=0, max=5000)
+ bar_y_offset: IntProperty(name="Bar Y Offset", default=80, min=0, max=5000)
+
+ bar_x: IntProperty(name="Bar X", default=100, min=0, max=5000)
+ bar_y: IntProperty(name="Bar Y", default=100, min=50, max=5000)
+ bar_end: IntProperty(name="Bar End", default=100, min=0, max=5000)
+ bar_width: IntProperty(name="Bar Width", default=100, min=0, max=5000)
+
+ wcount: IntProperty(name="Width Count", default=10, min=0, max=5000)
+ hcount: IntProperty(name="Rows", default=5, min=0, max=5000)
+
+ reports_y: IntProperty(name="Reports Y", default=5, min=0, max=5000)
+ reports_x: IntProperty(name="Reports X", default=5, min=0, max=5000)
+
+ assetbar_on: BoolProperty(name="Assetbar On", default=False)
+ turn_off: BoolProperty(name="Turn Off", default=False)
+
+ mouse_x: IntProperty(name="Mouse X", default=0)
+ mouse_y: IntProperty(name="Mouse Y", default=0)
+
+ active_index: IntProperty(name="Active Index", default=-3)
+ scrolloffset: IntProperty(name="Scroll Offset", default=0)
+ drawoffset: IntProperty(name="Draw Offset", default=0)
+
+ dragging: BoolProperty(name="Dragging", default=False)
+ drag_init: BoolProperty(name="Drag Initialisation", default=False)
+ drag_length: IntProperty(name="Drag length", default=0)
+ draw_drag_image: BoolProperty(name="Draw Drag Image", default=False)
+ draw_snapped_bounds: BoolProperty(name="Draw Snapped Bounds", default=False)
+
+ snapped_location: FloatVectorProperty(name="Snapped Location", default=(0, 0, 0))
+ snapped_bbox_min: FloatVectorProperty(name="Snapped Bbox Min", default=(0, 0, 0))
+ snapped_bbox_max: FloatVectorProperty(name="Snapped Bbox Max", default=(0, 0, 0))
+ snapped_normal: FloatVectorProperty(name="Snapped Normal", default=(0, 0, 0))
+
+ snapped_rotation: FloatVectorProperty(name="Snapped Rotation", default=(0, 0, 0), subtype='QUATERNION')
+
+ has_hit: BoolProperty(name="has_hit", default=False)
+ thumbnail_image = StringProperty(
+ name="Thumbnail Image",
+ description="",
+ default=paths.get_addon_thumbnail_path('thumbnail_notready.jpg'))
+
+ #### rating UI props
+ rating_ui_scale = ui_scale
+
+ rating_button_on: BoolProperty(name="Rating Button On", default=True)
+ rating_menu_on: BoolProperty(name="Rating Menu On", default=False)
+ rating_on: BoolProperty(name="Rating on", default=True)
+
+ rating_button_width: IntProperty(name="Rating Button Width", default=50 * ui_scale)
+ rating_button_height: IntProperty(name="Rating Button Height", default=50 * ui_scale)
+
+ rating_x: IntProperty(name="Rating UI X", default=10)
+ rating_y: IntProperty(name="Rating UI Y", default=10)
+
+ rating_ui_width: IntProperty(name="Rating UI Width", default=rating_ui_scale * 600)
+ rating_ui_height: IntProperty(name="Rating UI Heightt", default=rating_ui_scale * 256)
+
+ quality_stars_x: IntProperty(name="Rating UI Stars X", default=rating_ui_scale * 90)
+ quality_stars_y: IntProperty(name="Rating UI Stars Y", default=rating_ui_scale * 190)
+
+ star_size: IntProperty(name="Star Size", default=rating_ui_scale * 50)
+
+ workhours_bar_slider_size: IntProperty(name="Workhours Bar Slider Size", default=rating_ui_scale * 30)
+
+ workhours_bar_x: IntProperty(name="Workhours Bar X", default=rating_ui_scale * (100 - 15))
+ workhours_bar_y: IntProperty(name="Workhours Bar Y", default=rating_ui_scale * (45 - 15))
+
+ workhours_bar_x_max: IntProperty(name="Workhours Bar X Max", default=rating_ui_scale * (480 - 15))
+
+ dragging_rating: BoolProperty(name="Dragging Rating", default=False)
+ dragging_rating_quality: BoolProperty(name="Dragging Rating Quality", default=False)
+ dragging_rating_work_hours: BoolProperty(name="Dragging Rating Work Hours", default=False)
+ last_rating_time: FloatProperty(name="Last Rating Time", default=0.0)
+
+
+class BlenderKitCommonSearchProps(object):
+ # STATES
+ is_searching: BoolProperty(name="Searching", description="search is currently running (internal)", default=False)
+ is_downloading: BoolProperty(name="Downloading", description="download is currently running (internal)",
+ default=False)
+ search_done: BoolProperty(name="Search Completed", description="at least one search did run (internal)",
+ default=False)
+ search_error: BoolProperty(name="Search Error", description="last search had an error", default=False)
+ report: StringProperty(
+ name="Report",
+ description="errors and messages",
+ default="")
+
+
+def name_update(self, context):
+ ''' checks for name change, because it decides if whole asset has to be re-uploaded. Name is stored in the blend file
+ and that's the reason.'''
+ props = utils.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
+
+
+def update_tags(self, context):
+ props = utils.get_upload_props()
+
+ 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
+
+
+class BlenderKitCommonUploadProps(object):
+ id: StringProperty(
+ name="Asset Version Id",
+ description="Unique name of the asset version(hidden)",
+ default="")
+ asset_base_id: StringProperty(
+ name="Asset Base Id",
+ description="Unique name of the asset (hidden)",
+ default="")
+ name: StringProperty(
+ name="Name",
+ description="Main name of the asset",
+ default="",
+ update=name_update
+ )
+ # this is to store name for purpose of checking if name has changed.
+ name_old: StringProperty(
+ name="Old Name",
+ description="Old name of the asset",
+ default="",
+ )
+
+ description: StringProperty(
+ name="Description",
+ description="Description of the asset.",
+ default="")
+ tags: StringProperty(
+ name="Tags",
+ description="List of tags, separated by commas (optional)",
+ default="",
+ update=update_tags
+ )
+
+ name_changed: BoolProperty(name="Name Changed",
+ description="Name has changed, the asset has to be re-uploaded with all data",
+ default=False)
+
+ pbr: BoolProperty(name="Pure PBR Compatible",
+ description="Is compatible with PBR standard. This means only image textures are used with no"
+ " procedurals and no color correction, only pbr shader is used.",
+ default=False)
+
+ pbr_type: EnumProperty(
+ name="PBR Type",
+ items=pbr_types,
+ description="PBR type",
+ default="METALLIC",
+ )
+ license: EnumProperty(
+ items=licenses,
+ default='royalty_free',
+ description='License. Please read our help for choosing the right licenses',
+ )
+
+ is_free: BoolProperty(name="Free for Everyone",
+ description="You consent you want to release this asset as free for everyone.",
+ default=False)
+
+ uploading: BoolProperty(name="Uploading",
+ description="True when background process is running",
+ default=False,
+ update=autothumb.update_upload_material_preview)
+ upload_state: StringProperty(
+ name="State Of Upload",
+ description="bg process reports for upload",
+ default='')
+
+ has_thumbnail: BoolProperty(name="Has Thumbnail", description="True when thumbnail was checked and loaded",
+ default=False)
+
+ thumbnail_generating_state: StringProperty(
+ name="Thumbnail Generating State",
+ description="bg process reports for thumbnail generation",
+ default='Please add thumbnail(jpg, at least 512x512)')
+
+ report: StringProperty(
+ name="Missing Upload Properties",
+ description="used to write down what's missing",
+ default='')
+
+ category: EnumProperty(
+ name="Category",
+ description="main category to put into",
+ items=get_category_enums
+ )
+ subcategory: EnumProperty(
+ name="Subcategory",
+ description="main category to put into",
+ items=get_subcategory_enums
+ )
+
+
+class BlenderKitRatingProps(PropertyGroup):
+ rating_quality: IntProperty(name="Quality", description="quality of the material", default=0, min=-1, max=10)
+ rating_work_hours: FloatProperty(name="Work Hours", description="How many hours did this work take?", default=0.01,
+ min=0.0, max=1000
+ )
+ rating_complexity: IntProperty(name="Complexity",
+ description="Complexity is a number estimating how much work was spent on the asset.aaa",
+ default=0, min=0, max=10)
+ rating_virtual_price: FloatProperty(name="Virtual Price",
+ description="How much would you pay for this object if buing it?",
+ default=0, min=0, max=10000)
+ rating_problems: StringProperty(
+ name="Problems",
+ description="Problems found/ why did you take points down - this will be available for the author."
+ " As short as possible",
+ default="",
+ )
+ rating_compliments: StringProperty(
+ name="Compliments",
+ description="Comliments - let the author know you like his work! "
+ " As short as possible",
+ default="",
+ )
+
+
+class BlenderKitMaterialSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
+ search_keywords: StringProperty(
+ name="Search",
+ description="Search for these keywords",
+ default="",
+ update=search.search_update
+ )
+ search_style: EnumProperty(
+ name="Style",
+ items=search_material_styles,
+ description="Style of material",
+ default="ANY",
+ )
+ search_style_other: StringProperty(
+ name="Style Other",
+ description="Style not in the list",
+ default="",
+ )
+ search_engine: EnumProperty(
+ name='Engine',
+ items=engines,
+ default='NONE',
+ description='Output engine',
+ )
+ search_engine_other: StringProperty(
+ name="Engine",
+ description="engine not specified by addon",
+ default="",
+ )
+ automap: BoolProperty(name="Auto-Map",
+ description="reset object texture space and also add automatically a cube mapped UV "
+ "to the object. \n this allows most materials to apply instantly to any mesh",
+ default=True)
+
+
+class BlenderKitMaterialUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
+ style: EnumProperty(
+ name="Style",
+ items=material_styles,
+ description="Style of material",
+ default="REALISTIC",
+ )
+ style_other: StringProperty(
+ name="Style Other",
+ description="Style not in the list",
+ default="",
+ )
+ engine: EnumProperty(
+ name='Engine',
+ items=engines,
+ default='CYCLES',
+ description='Output engine',
+ )
+ engine_other: StringProperty(
+ name="Engine Other",
+ description="engine not specified by addon",
+ default="",
+ )
+
+ shaders: StringProperty(
+ name="Shaders Used",
+ description="shaders used in asset, autofilled",
+ default="",
+ )
+
+ uv: BoolProperty(name="Needs UV", description="needs an UV set", default=False)
+ # printable_3d : BoolProperty( name = "3d printable", description = "can be 3d printed", default = False)
+ animated: BoolProperty(name="Animated", description="is animated", default=False)
+ texture_resolution_min: IntProperty(name="Texture Resolution Min", description="texture resolution minimum",
+ default=0)
+ texture_resolution_max: IntProperty(name="Texture Resolution Max", description="texture resolution maximum",
+ default=0)
+
+ texture_size_meters: FloatProperty(name="Texture Size in Meters", description="face count, autofilled",
+ default=1.0, min=0)
+
+ thumbnail_scale: FloatProperty(name="Thumbnail Object Size",
+ description="size of material preview object in meters "
+ "- change for materials that look better at sizes different than 1m",
+ default=1, min=0.00001, max=10)
+ thumbnail_background: BoolProperty(name="Thumbnail Background",
+ description="For refractive materials, you might need a background. "
+ "Don't use if thumbnail looks good without it!",
+ default=False)
+ thumbnail_background_lightness: FloatProperty(name="Thumbnail Background Lightness",
+ description="set to make your material stand out", default=.9,
+ min=0.00001, max=1)
+ thumbnail_samples: IntProperty(name="Cycles Samples",
+ description="cycles samples setting", default=150,
+ min=5, max=5000)
+ thumbnail_denoising: BoolProperty(name="Use Denoising",
+ description="Use denoising", default=True)
+ adaptive_subdivision: BoolProperty(name="Adaptive Subdivide",
+ description="Use adaptive displacement subdivision", default=False)
+
+ thumbnail_generator_type: EnumProperty(
+ name="Thumbnail Style",
+ items=(
+ ('BALL', 'Ball', ""),
+ ('CUBE', 'Cube', 'cube'),
+ ('FLUID', 'Fluid', 'Fluid'),
+ ('CLOTH', 'Cloth', 'Cloth'),
+ ('HAIR', 'Hair', 'Hair ')
+ ),
+ description="Style of asset",
+ default="BALL",
+ )
+
+ thumbnail: StringProperty(
+ name="Thumbnail",
+ description="Path to the thumbnail - 512x512 .jpg image",
+ subtype='FILE_PATH',
+ default="",
+ update=autothumb.update_upload_material_preview)
+
+ is_generating_thumbnail: BoolProperty(name="Generating Thumbnail",
+ description="True when background process is running", default=False,
+ update=autothumb.update_upload_material_preview)
+
+
+class BlenderKitTextureUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
+ style: EnumProperty(
+ name="Style",
+ items=material_styles,
+ description="Style of texture",
+ default="REALISTIC",
+ )
+ style_other: StringProperty(
+ name="Style Other",
+ description="Style not in the list",
+ default="",
+ )
+
+ pbr: BoolProperty(name="PBR Compatible", description="Is compatible with PBR standard", default=False)
+
+ # printable_3d : BoolProperty( name = "3d printable", description = "can be 3d printed", default = False)
+ animated: BoolProperty(name="Animated", description="is animated", default=False)
+ resolution: IntProperty(name="Texture Resolution", description="texture resolution", default=0)
+
+
+class BlenderKitBrushSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
+ search_keywords: StringProperty(
+ name="Search",
+ description="Search for these keywords",
+ default="",
+ update=search.search_update
+ )
+
+
+class BlenderKitBrushUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
+ mode: EnumProperty(
+ name="Mode",
+ items=(
+ ('IMAGE', 'Texture paint', "Texture brush"),
+ ('SCULPT', 'Sculpt', 'Sculpt brush'),
+ ('VERTEX', 'Vertex paint', 'Vertex paint brush'),
+ ('WEIGHT', 'Weight paint', 'Weight paint brush'),
+ ),
+ description="Mode where the brush works",
+ default="SCULPT",
+ )
+
+
+# upload properties
+class BlenderKitModelUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
+ style: EnumProperty(
+ name="Style",
+ items=model_styles,
+ description="Style of asset",
+ default="REALISTIC",
+ )
+ style_other: StringProperty(
+ name="Style Other",
+ description="Style not in the list",
+ default="",
+ )
+ engine: EnumProperty(
+ name='Engine',
+ items=engines,
+ default='CYCLES',
+ description='Output engine',
+ )
+
+ production_level: EnumProperty(
+ name='Production Level',
+ items=(
+ ('FINISHED', 'Finished', 'Render or animation ready asset'),
+ ('TEMPLATE', 'Template', 'Asset intended to help in creation of something else'),
+ ),
+ default='FINISHED',
+ description='Production state of the asset, \n also template should be actually finished, \n'
+ 'just the nature of it can be a template, like a thumbnailer scene, \n '
+ 'finished mesh topology as start for modelling or similar.',
+ )
+
+ engine_other: StringProperty(
+ name="Engine",
+ description="engine not specified by addon",
+ default="",
+ )
+
+ engine1: EnumProperty(
+ name='2nd Engine',
+ items=engines,
+ default='NONE',
+ description='Output engine',
+ )
+ engine2: EnumProperty(
+ name='3rd Engine',
+ items=engines,
+ default='NONE',
+ description='Output engine',
+ )
+ engine3: EnumProperty(
+ name='4th Engine',
+ items=engines,
+ default='NONE',
+ description='Output engine',
+ )
+
+ manufacturer: StringProperty(
+ name="Manufacturer",
+ description="Manufacturer, company making a design peace or product.",
+ default="",
+ )
+
+ designer: StringProperty(
+ name="Designer",
+ description="Author of the original design piece depicted.",
+ default="",
+ )
+
+ design_collection: StringProperty(
+ name="Design Collection",
+ description="Fill if this piece is part of a design collection.",
+ default="",
+ )
+
+ design_variant: StringProperty(
+ name="Variant",
+ description="Colour or material variant of the product.",
+ default="",
+ )
+
+ thumbnail: StringProperty(
+ name="Thumbnail",
+ description="Path to the thumbnail - 512x512 .jpg image",
+ subtype='FILE_PATH',
+ default="",
+ update=autothumb.update_upload_model_preview)
+
+ thumbnail_background_lightness: FloatProperty(name="Thumbnail Background Lightness",
+ description="set to make your material stand out", default=.9,
+ min=0.01, max=10)
+
+ thumbnail_angle: EnumProperty(
+ name='Thumbnail Angle',
+ items=thumbnail_angles,
+ default='DEFAULT',
+ description='thumbnailer angle',
+ )
+
+ thumbnail_snap_to: EnumProperty(
+ name='Model Snaps To:',
+ items=thumbnail_snap,
+ default='GROUND',
+ description='typical placing of the interior. Leave on ground for most objects that respect gravity :)',
+ )
+
+ thumbnail_samples: IntProperty(name="Cycles Samples",
+ description="cycles samples setting", default=200,
+ min=5, max=5000)
+ thumbnail_denoising: BoolProperty(name="Use Denoising",
+ description="Use denoising", default=True)
+
+ use_design_year: BoolProperty(name="Use Design Year",
+ description="When this thing came into world for the first time\n"
+ " e.g. for dinosaur, you set -240 million years ;) ",
+ default=False)
+ design_year: IntProperty(name="Design Year", description="when was this item designed", default=1960)
+ # use_age : BoolProperty( name = "use item age", description = "use item age", default = False)
+ condition: EnumProperty(
+ items=conditions,
+ default='UNSPECIFIED',
+ description='age of the object',
+ )
+
+ adult: BoolProperty(name="Adult Content", description="adult content", default=False)
+
+ work_hours: FloatProperty(name="Work Hours", description="How long did it take you to finish the asset?",
+ default=0.0, min=0.0, max=8760)
+
+ modifiers: StringProperty(
+ name="Modifiers Used",
+ description="if you need specific modifiers, autofilled",
+ default="",
+ )
+
+ materials: StringProperty(
+ name="Material Names",
+ description="names of materials in the file, autofilled",
+ default="",
+ )
+ shaders: StringProperty(
+ name="Shaders Used",
+ description="shaders used in asset, autofilled",
+ default="",
+ )
+
+ dimensions: FloatVectorProperty(
+ name="Dimensions",
+ description="dimensions of the whole asset hierarchy",
+ default=(0, 0, 0),
+ )
+ bbox_min: FloatVectorProperty(
+ name="Bbox Min",
+ description="dimensions of the whole asset hierarchy",
+ default=(-.25, -.25, 0),
+ )
+ bbox_max: FloatVectorProperty(
+ name="Bbox Max",
+ description="dimensions of the whole asset hierarchy",
+ default=(.25, .25, .5),
+ )
+
+ texture_resolution_min: IntProperty(name="Texture Resolution Min",
+ description="texture resolution min, autofilled", default=0)
+ texture_resolution_max: IntProperty(name="Texture Resolution Max",
+ description="texture resolution max, autofilled", default=0)
+
+ pbr: BoolProperty(name="PBR Compatible", description="Is compatible with PBR standard", default=False)
+
+ uv: BoolProperty(name="Has UV", description="has an UV set", default=False)
+ # printable_3d : BoolProperty( name = "3d printable", description = "can be 3d printed", default = False)
+ animated: BoolProperty(name="Animated", description="is animated", default=False)
+ face_count: IntProperty(name="Face count", description="face count, autofilled", default=0)
+ face_count_render: IntProperty(name="Render Face Count", description="render face count, autofilled", default=0)
+
+ object_count: IntProperty(name="Number of Objects", description="how many objects are in the asset, autofilled",
+ default=0)
+ mesh_poly_type: EnumProperty(
+ name='Dominant Poly Type',
+ items=mesh_poly_types,
+ default='OTHER',
+ description='',
+ )
+
+ manifold: BoolProperty(name="Manifold", description="asset is manifold, autofilled", default=False)
+
+ rig: BoolProperty(name="Rig", description="asset is rigged, autofilled", default=False)
+ simulation: BoolProperty(name="Simulation", description="asset uses simulation, autofilled", default=False)
+ '''
+ filepath : StringProperty(
+ name="Filepath",
+ description="file path",
+ default="",
+ )
+ '''
+
+ # THUMBNAIL STATES
+ is_generating_thumbnail: BoolProperty(name="Generating Thumbnail",
+ description="True when background process is running", default=False,
+ update=autothumb.update_upload_model_preview)
+
+ has_autotags: BoolProperty(name="Has Autotagging Done", description="True when autotagging done", default=False)
+
+
+class BlenderKitSceneUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
+ style: EnumProperty(
+ name="Style",
+ items=model_styles,
+ description="Style of asset",
+ default="REALISTIC",
+ )
+ style_other: StringProperty(
+ name="Style Other",
+ description="Style not in the list",
+ default="",
+ )
+ engine: EnumProperty(
+ name='Engine',
+ items=engines,
+ default='CYCLES',
+ description='Output engine',
+ )
+
+ production_level: EnumProperty(
+ name='Production Level',
+ items=(
+ ('FINISHED', 'Finished', 'Render or animation ready asset'),
+ ('TEMPLATE', 'Template', 'Asset intended to help in creation of something else'),
+ ),
+ default='FINISHED',
+ description='Production state of the asset, \n also template should be actually finished, \n'
+ 'just the nature of it can be a template, like a thumbnailer scene, \n '
+ 'finished mesh topology as start for modelling or similar.',
+ )
+
+ engine_other: StringProperty(
+ name="Engine",
+ description="engine not specified by addon",
+ default="",
+ )
+
+ engine1: EnumProperty(
+ name='2nd Engine',
+ items=engines,
+ default='NONE',
+ description='Output engine',
+ )
+ engine2: EnumProperty(
+ name='3rd Engine',
+ items=engines,
+ default='NONE',
+ description='Output engine',
+ )
+ engine3: EnumProperty(
+ name='4th Engine',
+ items=engines,
+ default='NONE',
+ description='Output engine',
+ )
+
+ thumbnail: StringProperty(
+ name="Thumbnail",
+ description="Path to the thumbnail - 512x512 .jpg image",
+ subtype='FILE_PATH',
+ default="",
+ update=autothumb.update_upload_scene_preview)
+
+ use_design_year: BoolProperty(name="Use Design Year",
+ description="When this thing came into world for the first time\n"
+ " e.g. for dinosaur, you set -240 million years ;) ",
+ default=False)
+ design_year: IntProperty(name="Design Year", description="when was this item designed", default=1960)
+ # use_age : BoolProperty( name = "use item age", description = "use item age", default = False)
+ condition: EnumProperty(
+ items=conditions,
+ default='UNSPECIFIED',
+ description='age of the object',
+ )
+
+ adult: BoolProperty(name="Adult Content", description="adult content", default=False)
+
+ work_hours: FloatProperty(name="Work Hours", description="How long did it take you to finish the asset?",
+ default=0.0, min=0.0, max=8760)
+
+ modifiers: StringProperty(
+ name="Modifiers Used",
+ description="if you need specific modifiers, autofilled",
+ default="",
+ )
+
+ materials: StringProperty(
+ name="Material Names",
+ description="names of materials in the file, autofilled",
+ default="",
+ )
+ shaders: StringProperty(
+ name="Shaders Used",
+ description="shaders used in asset, autofilled",
+ default="",
+ )
+
+ dimensions: FloatVectorProperty(
+ name="Dimensions",
+ description="dimensions of the whole asset hierarchy",
+ default=(0, 0, 0),
+ )
+ bbox_min: FloatVectorProperty(
+ name="Dimensions",
+ description="dimensions of the whole asset hierarchy",
+ default=(-.25, -.25, 0),
+ )
+ bbox_max: FloatVectorProperty(
+ name="Dimensions",
+ description="dimensions of the whole asset hierarchy",
+ default=(.25, .25, .5),
+ )
+
+ texture_resolution_min: IntProperty(name="Texture Eesolution Min",
+ description="texture resolution min, autofilled", default=0)
+ texture_resolution_max: IntProperty(name="Texture Eesolution Max",
+ description="texture resolution max, autofilled", default=0)
+
+ pbr: BoolProperty(name="PBR Compatible", description="Is compatible with PBR standard", default=False)
+
+ uv: BoolProperty(name="Has UV", description="has an UV set", default=False)
+ # printable_3d : BoolProperty( name = "3d printable", description = "can be 3d printed", default = False)
+ animated: BoolProperty(name="Animated", description="is animated", default=False)
+ face_count: IntProperty(name="Face Count", description="face count, autofilled", default=0)
+ face_count_render: IntProperty(name="Render Face Count", description="render face count, autofilled", default=0)
+
+ object_count: IntProperty(name="Number of Objects", description="how many objects are in the asset, autofilled",
+ default=0)
+ mesh_poly_type: EnumProperty(
+ name='Dominant Poly Type',
+ items=mesh_poly_types,
+ default='OTHER',
+ description='',
+ )
+
+ rig: BoolProperty(name="Rig", description="asset is rigged, autofilled", default=False)
+ simulation: BoolProperty(name="Simulation", description="asset uses simulation, autofilled", default=False)
+
+ # THUMBNAIL STATES
+ is_generating_thumbnail: BoolProperty(name="Generating Thumbnail",
+ description="True when background process is running", default=False,
+ update=autothumb.update_upload_model_preview)
+
+ has_autotags: BoolProperty(name="Has Autotagging Done", description="True when autotagging done", default=False)
+
+
+class BlenderKitModelSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
+ search_keywords: StringProperty(
+ name="Search",
+ description="Search for these keywords",
+ default="",
+ update=search.search_update
+ )
+ search_style: EnumProperty(
+ name="Style",
+ items=search_model_styles,
+ description="keywords defining style (realistic, painted, polygonal, other)",
+ default="ANY",
+ update=search.search_update
+ )
+ search_style_other: StringProperty(
+ name="Style",
+ description="Search style - other",
+ default="",
+ update=search.search_update
+ )
+ search_engine: EnumProperty(
+ items=engines,
+ default='CYCLES',
+ description='Output engine',
+ update=search.search_update
+ )
+ search_engine_other: StringProperty(
+ name="Engine",
+ description="engine not specified by addon",
+ default="",
+ update=search.search_update
+ )
+
+ search_advanced: BoolProperty(name="Advanced Search Options", description="use advanced search properties",
+ default=False)
+
+ # CONDITION
+ search_condition: EnumProperty(
+ items=conditions,
+ default='UNSPECIFIED',
+ description='condition of the object',
+ update=search.search_update
+ )
+
+ search_adult: BoolProperty(
+ name="Adult Content",
+ description="You're adult and agree with searching adult content",
+ default=False,
+ update=search.search_update
+ )
+
+ # DESIGN YEAR
+ search_design_year: BoolProperty(name="Sesigned in Year",
+ description="when the object was approximately designed",
+ default=False)
+
+ search_design_year_min: IntProperty(name="Min Age",
+ description="when the object was approximately designed",
+ default=1950, min=-100000000, max=1000000000)
+
+ search_design_year_max: IntProperty(name="Max Age",
+ description="when the object was approximately designed",
+ default=2017,
+ min=0,
+ max=10000000)
+
+ # TEXTURE RESOLUTION
+ search_texture_resolution: BoolProperty(name="Texture Resolution",
+ description="Span of the texture resolutions",
+ default=False)
+
+ search_texture_resolution_min: IntProperty(name="Min Texture Resolution",
+ description="when the object was approximately designed",
+ default=256,
+ min=0,
+ max=32768)
+
+ search_texture_resolution_max: IntProperty(name="Max Texture Resolution",
+ description="when the object was approximately designed",
+ default=4096,
+ min=0,
+ max=32768)
+
+ # POLYCOUNT
+ search_polycount: BoolProperty(name="Use Polycount",
+ description="use polycount of object search tag",
+ default=False)
+
+ search_polycount_min: IntProperty(name="Min Polycount",
+ description="polycount of the asset minimum",
+ default=0,
+ min=0,
+ max=100000000)
+
+ search_polycount_max: IntProperty(name="Max Polycount",
+ description="polycount of the asset maximum",
+ default=100000000,
+ min=0,
+ max=100000000)
+
+ append_method: EnumProperty(
+ name="Import Method",
+ items=(
+ ('LINK_GROUP', 'Link Group', ''),
+ ('APPEND_OBJECTS', 'Append Objects', ''),
+ ),
+ description="choose if the assets will be linked or appended",
+ default="LINK_GROUP"
+ )
+ append_link: EnumProperty(
+ name="How to Attach",
+ items=(
+ ('LINK', 'Link', ''),
+ ('APPEND', 'Append', ''),
+ ),
+ description="choose if the assets will be linked or appended",
+ default="LINK"
+ )
+ import_as: EnumProperty(
+ name="Import as",
+ items=(
+ ('GROUP', 'group', ''),
+ ('INDIVIDUAL', 'objects', ''),
+
+ ),
+ description="choose if the assets will be linked or appended",
+ default="GROUP"
+ )
+ randomize_rotation: BoolProperty(name='Randomize Rotation',
+ description="randomize rotation at placement",
+ default=False)
+ randomize_rotation_amount: FloatProperty(name="Randomization Max Angle",
+ description="maximum angle for random rotation",
+ default=math.pi / 36,
+ min=0,
+ max=2 * math.pi,
+ subtype='ANGLE')
+ offset_rotation_amount: FloatProperty(name="Offset Rotation",
+ description="offset rotation, hidden prop",
+ default=0,
+ min=0,
+ max=360,
+ subtype='ANGLE')
+ offset_rotation_step: FloatProperty(name="Offset Rotation Step",
+ description="offset rotation, hidden prop",
+ default=math.pi / 2,
+ min=0,
+ max=180,
+ subtype='ANGLE')
+
+
+class BlenderKitSceneSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
+ search_keywords: StringProperty(
+ name="Search",
+ description="Search for these keywords",
+ default="",
+ update=search.search_update
+ )
+ search_style: EnumProperty(
+ name="Style",
+ items=search_model_styles,
+ description="keywords defining style (realistic, painted, polygonal, other)",
+ default="ANY",
+ update=search.search_update
+ )
+ search_style_other: StringProperty(
+ name="Style",
+ description="Search style - other",
+ default="",
+ update=search.search_update
+ )
+ search_engine: EnumProperty(
+ items=engines,
+ default='CYCLES',
+ description='Output engine',
+ update=search.search_update
+ )
+ search_engine_other: StringProperty(
+ name="Engine",
+ description="engine not specified by addon",
+ default="",
+ update=search.search_update
+ )
+
+
+class BlenderKitAddonPreferences(AddonPreferences):
+ # this must match the addon name, use '__package__'
+ # when defining this in a submodule of a python package.
+ bl_idname = __name__
+ from os.path import expanduser
+ home = expanduser("~")
+ default_global_dict = home + os.sep + 'blenderkit_data'
+
+ api_key: StringProperty(
+ name="BlenderKit API Key",
+ description="Your blenderkit API Key. Get it from your page on the website.",
+ default="",
+ subtype="PASSWORD",
+ update=utils.save_prefs
+ )
+
+ global_dir: StringProperty(
+ name="Global Files Directory",
+ description="Global storage for your assets, will use subdirectories for the contents.",
+ subtype='DIR_PATH',
+ default=default_global_dict,
+ update=utils.save_prefs
+ )
+
+ project_subdir: StringProperty(
+ name="Project Assets Subdirectory",
+ description="where data will be stored for individual projects.",
+ subtype='DIR_PATH',
+ default="//assets",
+ )
+
+ directory_behaviour: EnumProperty(
+ name="Use Directories",
+ items=(
+ ('BOTH', 'Global and subdir',
+ 'store files both in global lib and subdirectory of current project. '
+ 'Warning - each file can be many times on your harddrive, but helps you keep your projects in one piece.'),
+ ('GLOBAL', 'Global',
+ "store downloaded files only in global directory. \n "
+ "This can bring problems when moving your projects, \n"
+ "since assets won't be in subdirectory of current project"),
+ ('LOCAL', 'Local',
+ 'store downloaded files only in local directory.\n'
+ ' This can use more bandwidth when you reuse assets in different projects. ')
+
+ ),
+ description="Which directories will be used for storing downloaded data",
+ default="BOTH",
+ )
+ thumbnail_use_gpu: BoolProperty(
+ name="Use GPU for Thumbnails Rendering",
+ description="By default this is off so you can continue your work without any lag.",
+ default=True
+ )
+
+ panel_behaviour: EnumProperty(
+ name="Panels Locations",
+ items=(
+ ('BOTH', 'Both Types',
+ ''),
+ ('UNIFIED', 'Unified 3d View Panel',
+ ""),
+ ('LOCAL', 'Relative to Data',
+ '')
+
+ ),
+ description="Which directories will be used for storing downloaded data",
+ default="UNIFIED",
+ )
+
+ max_assetbar_rows: IntProperty(name="Max Assetbar Rows",
+ description="max rows of assetbar in the 3d view",
+ default=1,
+ min=0,
+ max=20)
+
+ # allow_proximity : BoolProperty(
+ # name="allow proximity data reports",
+ # description="This sends anonymized proximity data \n \
+ # and allows us to make relations between database objects \n \
+ # without user interaction.",
+ # default=False
+ # )
+
+ def draw(self, context):
+ layout = self.layout
+
+ if self.api_key.strip() == '':
+ op = layout.operator("wm.url_open", text="Register online and get your API Key",
+ icon='QUESTION')
+ op.url = paths.BLENDERKIT_SIGNUP_URL
+ layout.prop(self, "api_key", text='Your API Key')
+ # layout.label('After you paste API Key, categories are downloaded, so blender will freeze for a few seconds.')
+ layout.prop(self, "global_dir")
+ layout.prop(self, "project_subdir")
+ # layout.prop(self, "temp_dir")
+ layout.prop(self, "directory_behaviour")
+ layout.prop(self, "thumbnail_use_gpu")
+ # layout.prop(self, "allow_proximity")
+ # layout.prop(self, "panel_behaviour")
+ layout.prop(self, "max_assetbar_rows")
+
+
+# registration
+classes = (
+
+ BlenderKitAddonPreferences,
+ BlenderKitUIProps,
+
+ BlenderKitModelSearchProps,
+ BlenderKitModelUploadProps,
+
+ BlenderKitSceneSearchProps,
+ BlenderKitSceneUploadProps,
+
+ BlenderKitMaterialUploadProps,
+ BlenderKitMaterialSearchProps,
+
+ BlenderKitTextureUploadProps,
+
+ BlenderKitBrushSearchProps,
+ BlenderKitBrushUploadProps,
+
+ BlenderKitRatingProps,
+)
+
+
+def register():
+ for cls in classes:
+ bpy.utils.register_class(cls)
+
+ bpy.types.Scene.blenderkitUI = PointerProperty(
+ type=BlenderKitUIProps)
+
+ # MODELS
+ bpy.types.Scene.blenderkit_models = PointerProperty(
+ type=BlenderKitModelSearchProps)
+ bpy.types.Object.blenderkit = PointerProperty( # for uploads, not now...
+ type=BlenderKitModelUploadProps)
+ bpy.types.Object.bkit_ratings = PointerProperty( # for uploads, not now...
+ type=BlenderKitRatingProps)
+
+ # SCENES
+ bpy.types.Scene.blenderkit_scene = PointerProperty(
+ type=BlenderKitSceneSearchProps)
+ bpy.types.Scene.blenderkit = PointerProperty( # for uploads, not now...
+ type=BlenderKitSceneUploadProps)
+ bpy.types.Scene.bkit_ratings = PointerProperty( # for uploads, not now...
+ type=BlenderKitRatingProps)
+
+ # MATERIALS
+ bpy.types.Scene.blenderkit_mat = PointerProperty(
+ type=BlenderKitMaterialSearchProps)
+ bpy.types.Material.blenderkit = PointerProperty( # for uploads, not now...
+ type=BlenderKitMaterialUploadProps)
+ bpy.types.Material.bkit_ratings = PointerProperty( # for uploads, not now...
+ type=BlenderKitRatingProps)
+
+ # BRUSHES
+ bpy.types.Scene.blenderkit_brush = PointerProperty(
+ type=BlenderKitBrushSearchProps)
+ bpy.types.Brush.blenderkit = PointerProperty( # for uploads, not now...
+ type=BlenderKitBrushUploadProps)
+ bpy.types.Brush.bkit_ratings = PointerProperty( # for uploads, not now...
+ type=BlenderKitRatingProps)
+
+ search.register_search()
+ asset_inspector.register_asset_inspector()
+ download.register_download()
+ upload.register_upload()
+ ratings.register_ratings()
+ autothumb.register_thumbnailer()
+ ui.register_ui()
+ ui_panels.register_ui_panels()
+ bg_blender.register()
+ bpy.app.handlers.load_post.append(scene_load)
+ utils.load_prefs()
+ overrides.register_overrides()
+
+
+def unregister():
+ for cls in classes:
+ bpy.utils.unregister_class(cls)
+
+ del bpy.types.Scene.blenderkit_models
+ del bpy.types.Scene.blenderkit_scene
+ del bpy.types.Scene.blenderkit_brush
+ del bpy.types.Scene.blenderkit_mat
+
+ del bpy.types.Scene.blenderkit
+ del bpy.types.Object.blenderkit
+ del bpy.types.Material.blenderkit
+ del bpy.types.Brush.blenderkit
+
+ search.unregister_search()
+ asset_inspector.unregister_asset_inspector()
+ download.unregister_download()
+ upload.unregister_upload()
+ ratings.unregister_ratings()
+ autothumb.unregister_thumbnailer()
+ ui.unregister_ui()
+ ui_panels.unregister_ui_panels()
+ bg_blender.unregister()
+ overrides.unregister_overrides()
diff --git a/blenderkit/append_link.py b/blenderkit/append_link.py
new file mode 100644
index 00000000..7be4d795
--- /dev/null
+++ b/blenderkit/append_link.py
@@ -0,0 +1,193 @@
+# ##### 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 #####
+
+import bpy
+import os, uuid
+from blenderkit import utils
+
+
+def append_brush(file_name, brushname=None, link=False, fake_user=True):
+ '''append a brush'''
+ with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to):
+ for m in data_from.brushes:
+ if m == brushname or brushname is None:
+ data_to.brushes = [m]
+ brushname = m
+ brush = bpy.data.brushes[brushname]
+ if fake_user:
+ brush.use_fake_user = True
+ return brush
+
+
+def append_material(file_name, matname=None, link=False, fake_user=True):
+ '''append a material type asset'''
+ # first, we have to check if there is a material with same name
+ # in previous step there's check if the imported material
+ # is allready in the scene, so we know same name != same material
+
+ mats_before = bpy.data.materials.keys()
+
+ with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to):
+ for m in data_from.materials:
+ if m == matname or matname is None:
+ data_to.materials = [m]
+ print(m, type(m))
+ matname = m
+ break;
+
+ # we have to find the new material :(
+ for mname in bpy.data.materials.keys():
+ if mname not in mats_before:
+ mat = bpy.data.materials[mname]
+ break
+
+ if fake_user:
+ mat.use_fake_user = True
+
+ return mat
+
+
+def append_scene(file_name, scenename=None, link=False, fake_user=False):
+ '''append a scene type asset'''
+ with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to):
+ for s in data_from.scenes:
+ if s == scenename or scenename is None:
+ data_to.scenes = [s]
+ scenename = s
+ scene = bpy.data.scenes[scenename]
+ if fake_user:
+ scene.use_fake_user = True
+ # scene has to have a new uuid, so user reports aren't screwed.
+ scene['uuid'] = str(uuid.uuid4())
+ return scene
+
+
+def link_group(file_name, obnames=[], location=(0, 0, 0), link=False, **kwargs):
+ '''link an instanced group - model type asset'''
+ sel = utils.selection_get()
+ bpy.ops.wm.link(directory=file_name + "/Collection/", filename=kwargs['name'], link=link, instance_collections=True,
+ autoselect=True)
+
+ main_object = bpy.context.active_object
+ if kwargs.get('rotation') is not None:
+ main_object.rotation_euler = kwargs['rotation']
+ main_object.location = location
+ utils.selection_set(sel)
+ return main_object, []
+
+
+def append_particle_system(file_name, obnames=[], location=(0, 0, 0), link=False, **kwargs):
+ '''link an instanced group - model type asset'''
+
+ pss = []
+ with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to):
+ for ps in data_from.particles:
+ pss.append(ps)
+ data_to.particles = pss
+
+ s = bpy.context.scene
+ sel = utils.selection_get()
+
+ target_object = bpy.context.scene.objects.get(kwargs['target_object'])
+ if target_object is not None and target_object.type == 'MESH':
+ target_object.select_set(True)
+ bpy.context.view_layer.objects.active = target_object
+
+ for ps in pss:
+ # now let's tune this ps to the particular objects area:
+ totarea = 0
+ for p in target_object.data.polygons:
+ totarea += p.area
+ count = int(ps.count * totarea)
+ if ps.child_type in ('INTERPOLATED', 'SIMPLE'):
+ total_count = count * ps.rendered_child_count
+ disp_count = count * ps.child_nbr
+ else:
+ total_count = count
+ threshold = 2000
+ total_max_threshold = 50000
+ # emitting too many parent particles just kills blender now:
+ if count > total_max_threshold:
+ ratio = round(count / total_max_threshold)
+
+ if ps.child_type in ('INTERPOLATED', 'SIMPLE'):
+ ps.rendered_child_count *= ratio
+ else:
+ ps.child_type = 'INTERPOLATED'
+ ps.rendered_child_count = ratio
+ count = max(2, int(count / ratio))
+ ps.display_percentage = min(ps.display_percentage, max(1, int(100 * threshold / total_count)))
+
+ print('count', count)
+ print('total count', total_count)
+ ps.count = count
+ print('got here')
+ bpy.ops.object.particle_system_add()
+ target_object.particle_systems[-1].settings = ps
+
+ target_object.select_set(False)
+ utils.selection_set(sel)
+ return target_object, []
+
+
+def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwargs):
+ '''append objects into scene individually'''
+ with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to):
+ sobs = []
+ for ob in data_from.objects:
+ if ob in obnames or obnames == []:
+ sobs.append(ob)
+ data_to.objects = sobs
+ # data_to.objects = data_from.objects#[name for name in data_from.objects if name.startswith("house")]
+
+ # link them to scene
+ scene = bpy.context.scene
+ sel = utils.selection_get()
+ bpy.ops.object.select_all(action='DESELECT')
+
+ return_obs = [] # this might not be needed, but better be sure to rewrite the list.
+ main_object = None
+ hidden_objects = []
+ #
+ for obj in data_to.objects:
+ if obj is not None:
+ # if obj.name not in scene.objects:
+ scene.collection.objects.link(obj)
+ if obj.parent is None:
+ obj.location = location
+ main_object = obj
+ obj.select_set(True)
+ # we need to unhide object so make_local op can use those too.
+ if link == True:
+ if obj.hide_viewport:
+ hidden_objects.append(obj)
+ obj.hide_viewport = False
+ return_obs.append(obj)
+
+ # Only after all objects are in scene! Otherwise gets broken relationships
+ if link == True:
+ bpy.ops.object.make_local(type='SELECT_OBJECT')
+ for ob in hidden_objects:
+ ob.hide_viewport = True
+
+ if kwargs.get('rotation') is not None:
+ main_object.rotation_euler = kwargs['rotation']
+ bpy.ops.object.select_all(action='DESELECT')
+
+ utils.selection_set(sel)
+ return main_object, return_obs
diff --git a/blenderkit/asset_inspector.py b/blenderkit/asset_inspector.py
new file mode 100644
index 00000000..7c5d24f7
--- /dev/null
+++ b/blenderkit/asset_inspector.py
@@ -0,0 +1,376 @@
+# ##### 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 #####
+
+if "bpy" in locals():
+ import imp
+
+ imp.reload(utils)
+else:
+ from blenderkit import utils
+
+import bpy, bmesh
+import mathutils
+import object_print3d_utils
+from object_print3d_utils import operators as ops
+from mathutils import Vector
+
+RENDER_OBTYPES = ['MESH', 'CURVE', 'SURFACE', 'METABALL', 'TEXT']
+
+
+def check_material(props, mat):
+ e = bpy.context.scene.render.engine
+ shaders = []
+ if e == 'CYCLES':
+
+ if mat.node_tree is not None:
+ checknodes = mat.node_tree.nodes[:]
+ while len(checknodes) > 0:
+ n = checknodes.pop()
+ if n.type == 'GROUP': # dive deeper here.
+ checknodes.extend(n.node_tree.nodes)
+ if len(n.outputs) == 1 and n.outputs[0].type == 'SHADER' and n.type != 'GROUP':
+ if n.type not in shaders:
+ shaders.append(n.type)
+ if n.type == 'TEX_IMAGE':
+ mattype = 'image based'
+ if n.image is not None:
+
+ maxres = max(n.image.size[0], n.image.size[1])
+
+ props.texture_resolution_max = max(props.texture_resolution_max, maxres)
+
+ minres = min(n.image.size[0], n.image.size[1])
+
+ if props.texture_resolution_min == 0:
+ props.texture_resolution_min = minres
+ else:
+ props.texture_resolution_min = min(props.texture_resolution_min, minres)
+
+ props.shaders = ''
+ for s in shaders:
+ if s.startswith('BSDF_'):
+ s = s[5:]
+ s = s.lower().replace('_', ' ')
+ props.shaders += (s + ', ')
+
+
+def check_render_engine(props, obs):
+ ob = obs[0]
+ m = None
+
+ e = bpy.context.scene.render.engine
+ mattype = None
+ materials = []
+ shaders = []
+ props.uv = False
+
+ for ob in obs: # TODO , this is duplicated here for other engines, otherwise this should be more clever.
+ for ms in ob.material_slots:
+ if ms.material is not None:
+ m = ms.material
+ if m.name not in materials:
+ materials.append(m.name)
+ if ob.type == 'MESH' and len(ob.data.uv_layers) > 0:
+ props.uv = True
+
+ if e == 'BLENDER_RENDER':
+ props.engine = 'BLENDER_INTERNAL'
+ elif e == 'CYCLES':
+
+ props.engine = 'CYCLES'
+
+
+ for mname in materials:
+ m = bpy.data.materials[mname]
+ if m is not None and m.node_tree is not None:
+ checknodes = m.node_tree.nodes[:]
+ while len(checknodes) > 0:
+ n = checknodes.pop()
+ if n.type == 'GROUP': # dive deeper here.
+ checknodes.extend(n.node_tree.nodes)
+ if len(n.outputs) == 1 and n.outputs[0].type == 'SHADER' and n.type != 'GROUP':
+ if n.type not in shaders:
+ shaders.append(n.type)
+ if n.type == 'TEX_IMAGE':
+ mattype = 'image based'
+ if n.image is not None:
+
+ maxres = max(n.image.size[0], n.image.size[1])
+
+ props.texture_resolution_max = max(props.texture_resolution_max, maxres)
+
+ minres = min(n.image.size[0], n.image.size[1])
+
+ if props.texture_resolution_min == 0:
+ props.texture_resolution_min = minres
+ else:
+ props.texture_resolution_min = min(props.texture_resolution_min, minres)
+
+ # if mattype == None:
+ # mattype = 'procedural'
+ # tags['material type'] = mattype
+
+ elif e == 'BLENDER_GAME':
+ props.engine = 'BLENDER_GAME'
+
+ # write to object properties.
+ print(materials)
+ print(shaders)
+ props.materials = ''
+ props.shaders = ''
+ for m in materials:
+ props.materials += (m + ', ')
+ for s in shaders:
+ if s.startswith('BSDF_'):
+ s = s[5:]
+ s = s.lower()
+ s = s.replace('_', ' ')
+ props.shaders += (s + ', ')
+ print(props.shaders)
+
+
+def check_printable(props, obs):
+ if len(obs) == 1:
+ check_cls = (
+ ops.Print3DCheckSolid,
+ ops.Print3DCheckIntersections,
+ ops.Print3DCheckDegenerate,
+ ops.Print3DCheckDistorted,
+ ops.Print3DCheckThick,
+ ops.Print3DCheckSharp,
+ # ops.Print3DCheckOverhang,
+ )
+
+ ob = obs[0]
+
+ info = []
+ for cls in check_cls:
+ cls.main_check(ob, info)
+
+ printable = True
+ for item in info:
+ print(item)
+ passed = item[0].endswith(' 0')
+ print('passed', passed)
+ if not passed:
+ print(item[0])
+ printable = False
+
+ props.printable_3d = printable
+
+
+def check_rig(props, obs):
+ for ob in obs:
+ if ob.type == 'ARMATURE':
+ props.rig = True
+
+
+def check_anim(props, obs):
+ animated = False
+ for ob in obs:
+ if ob.animation_data is not None:
+ a = ob.animation_data.action
+ if a is not None:
+ for c in a.fcurves:
+ if len(c.keyframe_points) > 1:
+ animated = True
+
+ # c.keyframe_points.remove(c.keyframe_points[0])
+ if animated:
+ props.animated = True
+
+
+def check_meshprops(props, obs):
+ ''' checks polycount, manifold, mesh parts (not implemented)'''
+ fc = 0
+ fcr = 0
+ tris = 0
+ quads = 0
+ ngons = 0
+ vc = 0
+
+ edges_counts = {}
+ manifold = True
+
+ for ob in obs:
+ if ob.type == 'MESH' or ob.type == 'CURVE':
+ if ob.type == 'CURVE':
+ mesh = ob.to_mesh(depsgraph=bpy.context.depsgraph, apply_modifiers=True, calc_undeformed=False)
+ else:
+ mesh = ob.data
+ fco = len(mesh.polygons)
+ fc += fco
+ vc += len(mesh.vertices)
+ fcor = fco
+ for f in mesh.polygons:
+ # face sides counter
+ if len(f.vertices) == 3:
+ tris += 1
+ elif len(f.vertices) == 4:
+ quads += 1
+ elif len(f.vertices) > 4:
+ ngons += 1
+
+ # manifold counter
+ for i, v in enumerate(f.vertices):
+ v1 = f.vertices[i - 1]
+ e = (min(v, v1), max(v, v1))
+ edges_counts[e] = edges_counts.get(e, 0) + 1
+
+ # all meshes have to be manifold for this to work.
+ manifold = manifold and not any(i in edges_counts.values() for i in [0, 1, 3, 4])
+
+ for m in ob.modifiers:
+ if m.type == 'SUBSURF' or m.type == 'MULTIRES':
+ fcor *= 4 ** m.render_levels
+ if m.type == 'SOLIDIFY': # this is rough estimate, not to waste time with evaluating all nonmanifold edges
+ fcor *= 2
+ if m.type == 'ARRAY':
+ fcor *= m.count
+ if m.type == 'MIRROR':
+ fcor *= 2
+ if m.type == 'DECIMATE':
+ fcor *= m.ratio
+ fcr += fcor
+
+ # write out props
+ props.face_count = fc
+ props.face_count_render = fcr
+ # print(tris, quads, ngons)
+ if quads > 0 and tris == 0 and ngons == 0:
+ props.mesh_poly_type = 'QUAD'
+ elif quads > tris and quads > ngons:
+ props.mesh_poly_type = 'QUAD_DOMINANT'
+ elif tris > quads and tris > quads:
+ props.mesh_poly_type = 'TRI_DOMINANT'
+ elif quads == 0 and tris > 0 and ngons == 0:
+ props.mesh_poly_type = 'TRI'
+ elif ngons > quads and ngons > tris:
+ props.mesh_poly_type = 'NGON'
+ else:
+ props.mesh_poly_type = 'OTHER'
+
+ props.manifold = manifold
+
+
+def countObs(props, obs):
+ ob_types = {}
+ count = len(obs)
+ for ob in obs:
+ otype = ob.type.lower()
+ ob_types[otype] = ob_types.get(otype, 0) + 1
+ print(count, ob_types)
+ props.object_count = count
+
+
+def check_modifiers(props, obs):
+ # modif_mapping = {
+ # }
+ modifiers = []
+ for ob in obs:
+ for m in ob.modifiers:
+ mtype = m.type
+ mtype = mtype.replace('_', ' ')
+ mtype = mtype.lower()
+ # mtype = mtype.capitalize()
+ if mtype not in modifiers:
+ modifiers.append(mtype)
+ if m.type == 'SMOKE':
+ if m.smoke_type == 'FLOW':
+ smt = m.flow_settings.smoke_flow_type
+ if smt == 'BOTH' or smt == 'FIRE':
+ modifiers.append('fire')
+ print(mtype)
+
+ # for mt in modifiers:
+ effectmodifiers = ['soft body', 'fluid simulation', 'particle system', 'collision', 'smoke', 'cloth',
+ 'dynamic paint']
+ for m in modifiers:
+ if m in effectmodifiers:
+ props.simulation = True
+ if ob.rigid_body is not None:
+ props.simulation = True
+ modifiers.append('rigid body')
+ finalstr = ''
+ for m in modifiers:
+ finalstr += m
+ finalstr += ','
+ props.modifiers = finalstr
+
+
+def get_autotags():
+ """ call all analysis functions """
+ ui = bpy.context.scene.blenderkitUI
+ if ui.asset_type =='MODEL':
+ ob = utils.get_active_model()
+ obs = utils.get_hierarchy(ob)
+ props = ob.blenderkit
+ if props.name == "":
+ props.name = ob.name
+
+ # reset some properties here, because they might not get re-filled at all when they aren't needed anymore.
+ props.texture_resolution_max = 0
+ props.texture_resolution_min = 0
+ # disabled printing checking, some 3d print addon bug.
+ # check_printable( props, obs)
+ check_render_engine(props, obs)
+
+ dim, bbox_min, bbox_max = utils.get_dimensions(obs)
+ props.dimensions = dim
+ props.bbox_min = bbox_min
+ props.bbox_max = bbox_max
+
+ check_rig(props, obs)
+ check_anim(props, obs)
+ check_meshprops(props, obs)
+ check_modifiers(props, obs)
+ countObs(props, obs)
+ elif ui.asset_type =='MATERIAL':
+ # reset some properties here, because they might not get re-filled at all when they aren't needed anymore.
+
+ mat = utils.get_active_asset()
+ props = mat.blenderkit
+ props.texture_resolution_max = 0
+ props.texture_resolution_min = 0
+ check_material(props, mat)
+
+
+class AutoFillTags(bpy.types.Operator):
+ """Fill tags for asset. Now run before upload, no need to interact from user side."""
+ bl_idname = "object.blenderkit_auto_tags"
+ bl_label = "Generate Auto Tags for BlenderKit"
+
+ @classmethod
+ def poll(cls, context):
+ return context.active_object is not None
+
+ def execute(self, context):
+ get_autotags()
+ return {'FINISHED'}
+
+
+def register_asset_inspector():
+ bpy.utils.register_class(AutoFillTags)
+
+
+def unregister_asset_inspector():
+ bpy.utils.unregister_class(AutoFillTags)
+
+
+if __name__ == "__main__":
+ register()
diff --git a/blenderkit/autothumb.py b/blenderkit/autothumb.py
new file mode 100644
index 00000000..00445629
--- /dev/null
+++ b/blenderkit/autothumb.py
@@ -0,0 +1,329 @@
+# ##### 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 #####
+
+import importlib
+
+if "bpy" in locals():
+ from importlib import reload
+
+ if "paths" in locals():
+ reload(paths)
+ if "utils" in locals():
+ reload(utils)
+ if "bg_blender" in locals():
+ reload(bg_blender)
+else:
+ from blenderkit import paths, utils, bg_blender
+
+import tempfile, os, subprocess, json, sys
+
+import bpy
+
+BLENDERKIT_EXPORT_DATA_FILE = "data.json"
+
+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 check_thumbnail(props, imgpath):
+ img = utils.get_hidden_image(imgpath, 'upload_preview', force_reload=True)
+ if img is not None: # and img.size[0] == img.size[1] and img.size[0] >= 512 and (
+ # img.file_format == 'JPEG' or img.file_format == 'PNG'):
+ props.has_thumbnail = True
+ props.thumbnail_generating_state = ''
+ return
+ else:
+ props.has_thumbnail = False
+ output = ''
+ if img is None or img.size[0] == 0 or img.filepath.find('thumbnail_notready.jpg') > -1:
+ output += 'No thumbnail or wrong file path\n'
+ else:
+ pass;
+ # this is causing problems on some platforms, don't know why..
+ # if img.size[0] != img.size[1]:
+ # output += 'image not a square\n'
+ # if img.size[0] < 512:
+ # output += 'image too small, should be at least 512x512\n'
+ # if img.file_format != 'JPEG' or img.file_format != 'PNG':
+ # output += 'image has to be a jpeg or png'
+ props.thumbnail_generating_state = output
+
+
+def update_upload_model_preview(self, context):
+ ob = utils.get_active_model()
+ if ob is not None:
+ props = ob.blenderkit
+ imgpath = props.thumbnail
+ check_thumbnail(props, imgpath)
+
+
+def update_upload_scene_preview(self, context):
+ s = bpy.context.scene
+ props = s.blenderkit
+ imgpath = props.thumbnail
+ check_thumbnail(props, imgpath)
+
+
+def update_upload_material_preview(self, context):
+ if hasattr(bpy.context, 'active_object') \
+ and bpy.context.active_object is not None \
+ and bpy.context.active_object.active_material is not None:
+ mat = bpy.context.active_object.active_material
+ props = mat.blenderkit
+ imgpath = props.thumbnail
+ check_thumbnail(props, imgpath)
+
+
+def update_upload_brush_preview(self, context):
+ brush = utils.get_active_brush()
+ if brush is not None:
+ props = brush.blenderkit
+ imgpath = bpy.path.abspath(brush.icon_filepath)
+ check_thumbnail(props, imgpath)
+
+
+def start_thumbnailer(self, context):
+ # Prepare to save the file
+ mainmodel = utils.get_active_model()
+ mainmodel.blenderkit.is_generating_thumbnail = True
+ mainmodel.blenderkit.thumbnail_generating_state = 'starting blender instance'
+
+ binary_path = bpy.app.binary_path
+ script_path = os.path.dirname(os.path.realpath(__file__))
+ basename, ext = os.path.splitext(bpy.data.filepath)
+ if not basename:
+ basename = os.path.join(basename, "temp")
+ if not ext:
+ ext = ".blend"
+ asset_name = mainmodel.blenderkit.name
+ tempdir = tempfile.mkdtemp()
+
+ file_dir = os.path.dirname(bpy.data.filepath)
+ thumb_path = os.path.join(file_dir, asset_name)
+ rel_thumb_path = os.path.join('//', asset_name)
+
+ i = 0
+ while os.path.isfile(thumb_path + '.jpg'):
+ thumb_path = os.path.join(file_dir, asset_name + '_' + str(i).zfill(4))
+ rel_thumb_path = os.path.join('//', asset_name + '_' + str(i).zfill(4))
+ i += 1
+
+ filepath = os.path.join(tempdir, "thumbnailer_blenderkit" + ext)
+ tfpath = paths.get_thumbnailer_filepath()
+ datafile = os.path.join(tempdir, BLENDERKIT_EXPORT_DATA_FILE)
+ try:
+ # save a copy of actual scene but don't interfere with the users models
+ bpy.ops.wm.save_as_mainfile(filepath=filepath, compress=False, copy=True)
+
+ obs = utils.get_hierarchy(mainmodel)
+ obnames = []
+ for ob in obs:
+ obnames.append(ob.name)
+ with open(datafile, 'w') as s:
+ bkit = mainmodel.blenderkit
+ json.dump({
+ "type": "model",
+ "models": str(obnames),
+ "thumbnail_angle": bkit.thumbnail_angle,
+ "thumbnail_snap_to": bkit.thumbnail_snap_to,
+ "thumbnail_background_lightness": bkit.thumbnail_background_lightness,
+ "thumbnail_samples": bkit.thumbnail_samples,
+ "thumbnail_denoising": bkit.thumbnail_denoising,
+ }, s)
+
+ flags = BELOW_NORMAL_PRIORITY_CLASS
+ if sys.platform != 'win32': # TODO test this on windows and find out how to change process priority on linux
+ # without psutil - we don't want any more libs in the addon
+ flags = 0
+
+ proc = subprocess.Popen([
+ binary_path,
+ "--background",
+ "-noaudio",
+ tfpath,
+ "--python", os.path.join(script_path, "autothumb_model_bg.py"),
+ "--", datafile, filepath, thumb_path, tempdir
+ ], bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, creationflags=flags)
+
+ eval_path_computing = "bpy.data.objects['%s'].blenderkit.is_generating_thumbnail" % mainmodel.name
+ eval_path_state = "bpy.data.objects['%s'].blenderkit.thumbnail_generating_state" % mainmodel.name
+ eval_path = "bpy.data.objects['%s']" % mainmodel.name
+
+ bg_blender.add_bg_process(eval_path_computing=eval_path_computing, eval_path_state=eval_path_state,
+ eval_path=eval_path, process_type='THUMBNAILER', process=proc)
+
+ mainmodel.blenderkit.thumbnail = rel_thumb_path + '.jpg'
+ mainmodel.blenderkit.thumbnail_generating_state = 'Saving .blend file'
+
+ except Exception as e:
+ self.report({'WARNING'}, "Error while exporting file: %s" % str(e))
+ return {'FINISHED'}
+
+
+def start_material_thumbnailer(self, context):
+ # Prepare to save the file
+ mat = bpy.context.active_object.active_material
+ mat.blenderkit.is_generating_thumbnail = True
+ mat.blenderkit.thumbnail_generating_state = 'starting blender instance'
+
+ binary_path = bpy.app.binary_path
+ script_path = os.path.dirname(os.path.realpath(__file__))
+ basename, ext = os.path.splitext(bpy.data.filepath)
+ if not basename:
+ basename = os.path.join(basename, "temp")
+ if not ext:
+ ext = ".blend"
+ asset_name = mat.name
+ tempdir = tempfile.mkdtemp()
+
+ file_dir = os.path.dirname(bpy.data.filepath)
+
+ thumb_path = os.path.join(file_dir, asset_name)
+ rel_thumb_path = os.path.join('//', mat.name)
+ i = 0
+ while os.path.isfile(thumb_path + '.png'):
+ thumb_path = os.path.join(file_dir, mat.name + '_' + str(i).zfill(4))
+ rel_thumb_path = os.path.join('//', mat.name + '_' + str(i).zfill(4))
+ i += 1
+
+ filepath = os.path.join(tempdir, "material_thumbnailer_cycles" + ext)
+ tfpath = paths.get_material_thumbnailer_filepath()
+ datafile = os.path.join(tempdir, BLENDERKIT_EXPORT_DATA_FILE)
+ try:
+ # save a copy of actual scene but don't interfere with the users models
+ bpy.ops.wm.save_as_mainfile(filepath=filepath, compress=False, copy=True)
+
+ with open(datafile, 'w') as s:
+ bkit = mat.blenderkit
+ json.dump({
+ "type": "material",
+ "material": mat.name,
+ "thumbnail_type": bkit.thumbnail_generator_type,
+ "thumbnail_scale": bkit.thumbnail_scale,
+ "thumbnail_background": bkit.thumbnail_background,
+ "thumbnail_background_lightness": bkit.thumbnail_background_lightness,
+ "thumbnail_samples": bkit.thumbnail_samples,
+ "thumbnail_denoising": bkit.thumbnail_denoising,
+ "adaptive_subdivision": bkit.adaptive_subdivision,
+ "texture_size_meters": bkit.texture_size_meters,
+ }, s)
+
+ flags = BELOW_NORMAL_PRIORITY_CLASS
+ if sys.platform != 'win32': # TODO test this on windows
+ flags = 0
+
+ proc = subprocess.Popen([
+ binary_path,
+ "--background",
+ "-noaudio",
+ tfpath,
+ "--python", os.path.join(script_path, "autothumb_material_bg.py"),
+ "--", datafile, filepath, thumb_path, tempdir
+ ], bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, creationflags=flags)
+
+ eval_path_computing = "bpy.data.materials['%s'].blenderkit.is_generating_thumbnail" % mat.name
+ eval_path_state = "bpy.data.materials['%s'].blenderkit.thumbnail_generating_state" % mat.name
+ eval_path = "bpy.data.materials['%s']" % mat.name
+
+ bg_blender.add_bg_process(eval_path_computing=eval_path_computing, eval_path_state=eval_path_state,
+ eval_path=eval_path, process_type='THUMBNAILER', process=proc)
+
+ mat.blenderkit.thumbnail = rel_thumb_path + '.png'
+ mat.blenderkit.thumbnail_generating_state = 'Saving .blend file'
+ except Exception as e:
+ self.report({'WARNING'}, "Error while packing file: %s" % str(e))
+ return {'FINISHED'}
+
+
+class GenerateThumbnailOperator(bpy.types.Operator):
+ """Tooltip"""
+ bl_idname = "object.blenderkit_generate_thumbnail"
+ bl_label = "BlenderKit Thumbnail Generator"
+
+ @classmethod
+ def poll(cls, context):
+ return bpy.context.active_object is not None
+
+ def draw(self, context):
+ ob = bpy.context.active_object
+ while ob.parent is not None:
+ ob = ob.parent
+ props = ob.blenderkit
+ layout = self.layout
+ layout.label(text='thumbnailer settings')
+ layout.prop(props, 'thumbnail_background_lightness')
+ layout.prop(props, 'thumbnail_angle')
+ layout.prop(props, 'thumbnail_snap_to')
+ layout.prop(props, 'thumbnail_samples')
+ layout.prop(props, 'thumbnail_denoising')
+
+ def execute(self, context):
+ start_thumbnailer(self, context)
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ wm = context.window_manager
+ return wm.invoke_props_dialog(self)
+
+
+class GenerateMaterialThumbnailOperator(bpy.types.Operator):
+ """Tooltip"""
+ bl_idname = "object.blenderkit_material_thumbnail"
+ bl_label = "BlenderKit Material Thumbnail Generator"
+
+ @classmethod
+ def poll(cls, context):
+ return bpy.context.active_object is not None
+
+ def check(self, context):
+ return True
+
+ def draw(self, context):
+ layout = self.layout
+ props = bpy.context.active_object.active_material.blenderkit
+ layout.prop(props, 'thumbnail_generator_type')
+ layout.prop(props, 'thumbnail_scale')
+ layout.prop(props, 'thumbnail_background')
+ if props.thumbnail_background:
+ layout.prop(props, 'thumbnail_background_lightness')
+ layout.prop(props, 'thumbnail_samples')
+ layout.prop(props, 'thumbnail_denoising')
+ layout.prop(props, 'adaptive_subdivision')
+
+ def execute(self, context):
+ start_material_thumbnailer(self, context)
+
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ wm = context.window_manager
+ return wm.invoke_props_dialog(self)
+
+
+def register_thumbnailer():
+ bpy.utils.register_class(GenerateThumbnailOperator)
+ bpy.utils.register_class(GenerateMaterialThumbnailOperator)
+
+
+def unregister_thumbnailer():
+ bpy.utils.unregister_class(GenerateThumbnailOperator)
+ bpy.utils.unregister_class(GenerateMaterialThumbnailOperator)
diff --git a/blenderkit/autothumb_material_bg.py b/blenderkit/autothumb_material_bg.py
new file mode 100644
index 00000000..824a0190
--- /dev/null
+++ b/blenderkit/autothumb_material_bg.py
@@ -0,0 +1,121 @@
+# ##### 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 #####
+
+import sys, json, math
+import bpy
+from pathlib import Path
+from blenderkit import utils, append_link, bg_blender
+
+BLENDERKIT_EXPORT_TEMP_DIR = sys.argv[-1]
+BLENDERKIT_THUMBNAIL_PATH = sys.argv[-2]
+BLENDERKIT_EXPORT_FILE_INPUT = sys.argv[-3]
+BLENDERKIT_EXPORT_DATA = sys.argv[-4]
+
+
+def render_thumbnails():
+ bpy.ops.render.render(write_still=True, animation=False)
+
+
+def unhide_collection(cname):
+ collection = bpy.context.scene.collection.children[cname]
+ collection.hide_viewport = False
+ collection.hide_render = False
+ collection.hide_select = False
+
+
+if __name__ == "__main__":
+ try:
+ bg_blender.progress('preparing thumbnail scene')
+ with open(BLENDERKIT_EXPORT_DATA, 'r') as s:
+ data = json.load(s)
+ # append_material(file_name, matname = None, link = False, fake_user = True)
+ mat = append_link.append_material(file_name=BLENDERKIT_EXPORT_FILE_INPUT, matname=data["material"], link=True,
+ fake_user=False)
+
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+
+ s = bpy.context.scene
+
+ colmapdict = {
+ 'BALL': 'Ball',
+ 'CUBE': 'Cube',
+ 'FLUID': 'Fluid',
+ 'CLOTH': 'Cloth',
+ 'HAIR': 'Hair'
+ }
+
+ unhide_collection(colmapdict[data["thumbnail_type"]])
+ if data['thumbnail_background']:
+ unhide_collection('Collection 13')
+ bpy.data.materials["bg checker colorable"].node_tree.nodes['input_level'].outputs['Value'].default_value \
+ = data['thumbnail_background_lightness']
+ tscale = data["thumbnail_scale"]
+ bpy.context.view_layer.objects['scaler'].scale = (tscale, tscale, tscale)
+ s.update()
+ for ob in bpy.context.visible_objects:
+ if ob.name[:15] == 'MaterialPreview':
+ ob.material_slots[0].material = mat
+ ob.data.texspace_size.x = 1 / tscale
+ ob.data.texspace_size.y = 1 / tscale
+ ob.data.texspace_size.z = 1 / tscale
+ if data["adaptive_subdivision"] == True:
+ ob.cycles.use_adaptive_subdivision = True
+
+ else:
+ ob.cycles.use_adaptive_subdivision = False
+ ts = data['texture_size_meters']
+ # if data["thumbnail_type"] in ['BALL', 'CUBE']:
+ # utils.automap(ob.name, tex_size = ts / tscale, bg_exception=True)
+ s.update()
+
+ s.cycles.volume_step_size = tscale * .1
+
+ if user_preferences.thumbnail_use_gpu:
+ bpy.context.scene.cycles.device = 'GPU'
+
+ s.cycles.samples = data['thumbnail_samples']
+ bpy.context.view_layer.cycles.use_denoising = data['thumbnail_denoising']
+
+ # import blender's HDR here
+ hdr_path = Path('datafiles/studiolights/world/interior.exr')
+ bpath = Path(bpy.utils.resource_path('LOCAL'))
+ ipath = bpath / hdr_path
+ ipath = str(ipath)
+
+ # this stuff is for mac and possibly linux. For blender // means relative path.
+ # for Mac, // means start of absolute path
+ if ipath.startswith('//'):
+ ipath = ipath[1:]
+
+ img = bpy.data.images['interior.exr']
+ img.filepath = ipath
+ img.reload()
+
+ bpy.context.scene.render.filepath = BLENDERKIT_THUMBNAIL_PATH
+ bg_blender.progress('rendering thumbnail')
+ render_thumbnails()
+ bg_blender.progress('background autothumbnailer finished successfully')
+
+
+ except Exception as e:
+ print(e)
+ import traceback
+
+ traceback.print_exc()
+
+ sys.exit(1)
diff --git a/blenderkit/autothumb_model_bg.py b/blenderkit/autothumb_model_bg.py
new file mode 100644
index 00000000..dc34d59c
--- /dev/null
+++ b/blenderkit/autothumb_model_bg.py
@@ -0,0 +1,158 @@
+# ##### 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 #####
+
+
+import sys, json, math
+from pathlib import Path
+import bpy
+from blenderkit import utils, append_link, bg_blender
+import mathutils
+
+BLENDERKIT_EXPORT_TEMP_DIR = sys.argv[-1]
+BLENDERKIT_THUMBNAIL_PATH = sys.argv[-2]
+BLENDERKIT_EXPORT_FILE_INPUT = sys.argv[-3]
+BLENDERKIT_EXPORT_DATA = sys.argv[-4]
+
+
+def get_obnames():
+ with open(BLENDERKIT_EXPORT_DATA, 'r') as s:
+ data = json.load(s)
+ obnames = eval(data['models'])
+ return obnames
+
+
+def center_obs_for_thumbnail(obs):
+ s = bpy.context.scene
+ # obs = bpy.context.selected_objects
+ parent = obs[0]
+
+ while parent.parent != None:
+ parent = parent.parent
+ # reset parent rotation, so we see how it really snaps.
+ parent.rotation_euler = (0, 0, 0)
+ bpy.context.scene.update()
+ minx, miny, minz, maxx, maxy, maxz = utils.get_bounds_worldspace(obs)
+
+ cx = (maxx - minx) / 2 + minx
+ cy = (maxy - miny) / 2 + miny
+ for ob in s.collection.objects:
+ ob.select_set(False)
+
+ bpy.context.view_layer.objects.active = parent
+ parent.location += mathutils.Vector((-cx, -cy, -minz))
+
+ camZ = s.camera.parent.parent
+ camZ.location.z = (maxz - minz) / 2
+ dx = (maxx - minx)
+ dy = (maxy - miny)
+ dz = (maxz - minz)
+ r = math.sqrt(dx * dx + dy * dy + dz * dz)
+
+ scaler = bpy.context.view_layer.objects['scaler']
+ scaler.scale = (r, r, r)
+ coef = .7
+ r *= coef
+ camZ.scale = (r, r, r)
+ s.update()
+
+
+def render_thumbnails():
+ bpy.ops.render.render(write_still=True, animation=False)
+
+
+if __name__ == "__main__":
+ try:
+ with open(BLENDERKIT_EXPORT_DATA, 'r') as s:
+ data = json.load(s)
+
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+
+ bg_blender.progress('preparing thumbnail scene')
+ obnames = get_obnames()
+ main_object, allobs = append_link.append_objects(file_name=BLENDERKIT_EXPORT_FILE_INPUT,
+ obnames=obnames,
+ link=True)
+ bpy.context.scene.update()
+
+ camdict = {
+ 'GROUND': 'camera ground',
+ 'WALL': 'camera wall',
+ 'CEILING': 'camera ceiling',
+ 'FLOAT': 'camera float'
+ }
+
+ bpy.context.scene.camera = bpy.data.objects[camdict[data['thumbnail_snap_to']]]
+ center_obs_for_thumbnail(allobs)
+ bpy.context.scene.render.filepath = BLENDERKIT_THUMBNAIL_PATH
+ if user_preferences.thumbnail_use_gpu:
+ bpy.context.scene.cycles.device = 'GPU'
+
+ fdict = {
+ 'DEFAULT': 1,
+ 'FRONT': 2,
+ 'SIDE': 3,
+ 'TOP': 4,
+ }
+ s = bpy.context.scene
+ s.frame_set(fdict[data['thumbnail_angle']])
+
+ snapdict = {
+ 'GROUND': 'Ground',
+ 'WALL': 'Wall',
+ 'CEILING': 'Ceiling',
+ 'FLOAT': 'Float'
+ }
+
+ collection = bpy.context.scene.collection.children[snapdict[data['thumbnail_snap_to']]]
+ collection.hide_viewport = False
+ collection.hide_render = False
+ collection.hide_select = False
+
+ main_object.rotation_euler = (0, 0, 0)
+ bpy.data.materials['bkit background'].node_tree.nodes['Value'].outputs['Value'].default_value \
+ = data['thumbnail_background_lightness']
+ s.cycles.samples = data['thumbnail_samples']
+ bpy.context.view_layer.cycles.use_denoising = data['thumbnail_denoising']
+ bpy.context.scene.update()
+
+ # import blender's HDR here
+ hdr_path = Path('datafiles/studiolights/world/interior.exr')
+ bpath = Path(bpy.utils.resource_path('LOCAL'))
+ ipath = bpath / hdr_path
+ ipath = str(ipath)
+
+ # this stuff is for mac and possibly linux. For blender // means relative path.
+ # for Mac, // means start of absolute path
+ if ipath.startswith('//'):
+ ipath = ipath[1:]
+
+ img = bpy.data.images['interior.exr']
+ img.filepath = ipath
+ img.reload()
+
+ bg_blender.progress('rendering thumbnail')
+ render_thumbnails()
+ fpath = BLENDERKIT_THUMBNAIL_PATH + '0001.jpg'
+ bg_blender.progress('background autothumbnailer finished successfully')
+
+
+ except:
+ import traceback
+
+ traceback.print_exc()
+ sys.exit(1)
diff --git a/blenderkit/bg_blender.py b/blenderkit/bg_blender.py
new file mode 100644
index 00000000..e594fa9a
--- /dev/null
+++ b/blenderkit/bg_blender.py
@@ -0,0 +1,237 @@
+# ##### 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 #####
+
+
+import bpy
+import sys, threading, os
+import re
+
+from bpy.props import (
+ EnumProperty,
+)
+
+from blenderkit import utils
+
+bg_processes = []
+
+
+class threadCom: # object passed to threads to read background process stdout info
+ ''' Object to pass data between thread and '''
+
+ def __init__(self, eval_path_computing, eval_path_state, eval_path, process_type, proc, location=None, name=''):
+ # self.obname=ob.name
+ self.name = name
+ self.eval_path_computing = eval_path_computing # property that gets written to.
+ self.eval_path_state = eval_path_state # property that gets written to.
+ self.eval_path = eval_path # property that gets written to.
+ self.process_type = process_type
+ self.outtext = ''
+ self.proc = proc
+ self.lasttext = ''
+ self.message = '' # the message to be sent.
+ self.progress = 0.0
+ self.location = location
+ self.error = False
+ self.log = ''
+
+
+def threadread(tcom):
+ '''reads stdout of background process, done this way to have it non-blocking. this threads basically waits for a stdout line to come in, fills the data, dies.'''
+ found = False
+ while not found:
+ inline = tcom.proc.stdout.readline()
+ # print('readthread', time.time())
+ inline = str(inline)
+ s = inline.find('progress{')
+ if s > -1:
+ e = inline.find('}')
+ tcom.outtext = inline[s + 9:e]
+ found = True
+ if tcom.outtext.find('%') > -1:
+ tcom.progress = float(re.findall('\d+\.\d+|\d+', tcom.outtext)[0])
+ return
+ if s == -1:
+ s = inline.find('Remaining')
+ if s > -1:
+ # e=inline.find('}')
+ tcom.outtext = inline[s: s + 18]
+ found = True
+ return
+ if len(inline) > 3:
+ print(inline, len(inline))
+ # if inline.find('Error'):
+ # tcom.error = True
+ # tcom.outtext = inline[2:]
+
+
+def progress(text, n=None):
+ '''function for reporting during the script, works for background operations in the header.'''
+ # for i in range(n+1):
+ # sys.stdout.flush()
+ text = str(text)
+ if n is None:
+ n = ''
+ else:
+ n = ' ' + ' ' + str(int(n * 1000) / 1000) + '% '
+ spaces = ' ' * (len(text) + 55)
+ sys.stdout.write('progress{%s%s}\n' % (text, n))
+ sys.stdout.flush()
+
+
+@bpy.app.handlers.persistent
+def bg_update():
+ '''monitoring of background process'''
+ text = ''
+ s = bpy.context.scene
+
+ global bg_processes
+ if len(bg_processes) == 0:
+ return 2
+
+ for p in bg_processes:
+ # proc=p[1].proc
+ readthread = p[0]
+ tcom = p[1]
+ if not readthread.is_alive():
+ readthread.join()
+ # readthread.
+ if tcom.error:
+ estring = tcom.eval_path_computing + ' = False'
+ exec (estring)
+
+ tcom.lasttext = tcom.outtext
+ if tcom.outtext != '':
+ tcom.outtext = ''
+ estring = tcom.eval_path_state + ' = tcom.lasttext'
+
+ exec (estring)
+ # print(tcom.lasttext)
+ if 'finished successfully' in tcom.lasttext:
+ bg_processes.remove(p)
+ estring = tcom.eval_path_computing + ' = False'
+ exec (estring)
+ else:
+ readthread = threading.Thread(target=threadread, args=([tcom]), daemon=True)
+ readthread.start()
+ p[0] = readthread
+ if len(bg_processes) == 0:
+ bpy.app.timers.unregister(bg_update)
+
+ return .1
+
+
+process_types = (
+ ('UPLOAD', 'Upload', ''),
+ ('THUMBNAILER', 'Thumbnailer', ''),
+)
+
+process_sources = (
+ ('MODEL', 'Model', 'set of objects'),
+ ('SCENE', 'Scene', 'set of scenes'),
+ ('MATERIAL', 'Material', 'any .blend Material'),
+ ('TEXTURE', 'Texture', 'a texture, or texture set'),
+ ('BRUSH', 'Brush', 'brush, can be any type of blender brush'),
+)
+
+
+class KillBgProcess(bpy.types.Operator):
+ '''Remove processes in background.'''
+ bl_idname = "object.kill_bg_process"
+ bl_label = "Kill Background Process"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ process_type: EnumProperty(
+ name="Type",
+ items=process_types,
+ description="Type of process",
+ default="UPLOAD",
+ )
+
+ process_source: EnumProperty(
+ name="Source",
+ items=process_sources,
+ description="Source of process",
+ default="MODEL",
+ )
+
+ def execute(self, context):
+ s = bpy.context.scene
+
+ cls = bpy.ops.object.convert.__class__
+ # first do the easy stuff...TODO all cases.
+ props = utils.get_upload_props()
+ if self.process_type == 'UPLOAD':
+ props.uploading = False
+ if self.process_type == 'THUMBNAILER':
+ props.is_generating_thumbnail = False
+ global blenderkit_bg_process
+ print('killing', self.process_source, self.process_type)
+ # then go kill the process. this wasn't working for unsetting props and that was the reason for changing to the method above.
+
+ processes = bg_processes
+ for p in processes:
+
+ tcom = p[1]
+ print(tcom.process_type, self.process_type)
+ if tcom.process_type == self.process_type:
+ source = eval(tcom.eval_path)
+ print(source.bl_rna.name, self.process_source)
+ print(source.name)
+ kill = False
+ if source.bl_rna.name == 'Object' and self.process_source == 'MODEL':
+ if source.name == bpy.context.active_object.name:
+ kill = True
+ if source.bl_rna.name == 'Material' and self.process_source == 'MATERIAL':
+ if source.name == bpy.context.active_object.active_material.name:
+ kill = True
+ if source.bl_rna.name == 'Brush' and self.process_source == 'BRUSH':
+ brush = utils.get_active_brush()
+ if brush is not None and source.name == brush.name:
+ kill = True
+ if kill:
+ estring = tcom.eval_path_computing + ' = False'
+ exec (estring)
+ processes.remove(p)
+ tcom.proc.kill()
+
+ return {'FINISHED'}
+
+
+def add_bg_process(location=None, name=None, eval_path_computing='', eval_path_state='', eval_path='', process_type='',
+ process=None):
+ '''adds process for monitoring'''
+ global bg_processes
+ tcom = threadCom(eval_path_computing, eval_path_state, eval_path, process_type, process, location, name)
+ readthread = threading.Thread(target=threadread, args=([tcom]), daemon=True)
+ readthread.start()
+
+ bg_processes.append([readthread, tcom])
+ if not bpy.app.timers.is_registered(bg_update):
+ bpy.app.timers.register(bg_update, persistent=True)
+
+
+def stert_bg_blender():
+ pass;
+
+
+def register():
+ bpy.utils.register_class(KillBgProcess)
+
+
+def unregister():
+ bpy.utils.unregister_class(KillBgProcess)
diff --git a/blenderkit/blendfiles/cleaned.blend b/blenderkit/blendfiles/cleaned.blend
new file mode 100644
index 00000000..4ad5f57c
--- /dev/null
+++ b/blenderkit/blendfiles/cleaned.blend
Binary files differ
diff --git a/blenderkit/blendfiles/material_thumbnailer_cycles.blend b/blenderkit/blendfiles/material_thumbnailer_cycles.blend
new file mode 100644
index 00000000..4e73329e
--- /dev/null
+++ b/blenderkit/blendfiles/material_thumbnailer_cycles.blend
Binary files differ
diff --git a/blenderkit/blendfiles/thumbnailer.blend b/blenderkit/blendfiles/thumbnailer.blend
new file mode 100644
index 00000000..76a7f2cd
--- /dev/null
+++ b/blenderkit/blendfiles/thumbnailer.blend
Binary files differ
diff --git a/blenderkit/categories.py b/blenderkit/categories.py
new file mode 100644
index 00000000..e4ff9be1
--- /dev/null
+++ b/blenderkit/categories.py
@@ -0,0 +1,89 @@
+import requests
+import json
+import os
+from blenderkit import paths
+import shutil
+import threading
+
+
+def count_to_parent(parent):
+ for c in parent['children']:
+ count_to_parent(c)
+ parent['assetCount'] += c['assetCount']
+
+
+def fix_category_counts(categories):
+ for c in categories:
+ count_to_parent(c)
+
+
+def filter_category(category):
+ if category['assetCount'] < 1:
+ return True
+ else:
+ to_remove = []
+ for c in category['children']:
+ if filter_category(c):
+ to_remove.append(c)
+ for c in to_remove:
+ category['children'].remove(c)
+
+
+def filter_categories(categories):
+ for category in categories:
+ filter_category(category)
+
+
+def get_category(categories, cat_path=()):
+ for category in cat_path:
+ for c in categories:
+ if c['slug'] == category:
+ categories = c['children']
+ if category == cat_path[-1]:
+ return (c)
+ break;
+
+
+def copy_categories():
+ # this creates the categories system on only
+ tempdir = paths.get_temp_dir()
+ categories_filepath = os.path.join(tempdir, 'categories.json')
+ if not os.path.exists(categories_filepath):
+ source_path = paths.get_addon_file(subpath='data' + os.sep + 'categories.json')
+ print('attempt to copy categories from: %s to %s' % (categories_filepath, source_path))
+ try:
+ shutil.copy(source_path, categories_filepath)
+ except:
+ print("couldn't copy categories file")
+
+
+def fetch_categories(API_key):
+ BLENDERKIT_API_MAIN = "https://www.blenderkit.com/api/v1/"
+
+ url = paths.get_bkit_url() + 'categories/'
+
+ headers = {
+ "accept": "application/json",
+ "Authorization": "Bearer %s" % API_key
+ }
+ tempdir = paths.get_temp_dir()
+ categories_filepath = os.path.join(tempdir, 'categories.json')
+
+ try:
+ r = requests.get(url, headers=headers)
+ rdata = r.json()
+ categories = rdata['results']
+ fix_category_counts(categories)
+ # filter_categories(categories) #TODO this should filter categories for search, but not for upload. by now off.
+ with open(categories_filepath, 'w') as s:
+ json.dump(categories, s, indent=4)
+ except:
+ print('category fetching failed')
+ if not os.path.exists(categories_filepath):
+ source_path = paths.get_addon_file(subpath='data' + os.sep + 'categories.json')
+ shutil.copy(source_path, categories_filepath)
+
+
+def fetch_categories_thread(API_key):
+ cat_thread = threading.Thread(target=fetch_categories, args=([API_key]), daemon=True)
+ cat_thread.start()
diff --git a/blenderkit/data/categories.json b/blenderkit/data/categories.json
new file mode 100644
index 00000000..652f3934
--- /dev/null
+++ b/blenderkit/data/categories.json
@@ -0,0 +1,3145 @@
+[
+ {
+ "alternateUrl": "",
+ "slug": "addon",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "addon",
+ "thumbnailHeight": null,
+ "name": "addon",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "anatomy-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 5,
+ "active": true,
+ "alternateTitle": "anatomy",
+ "thumbnailHeight": null,
+ "name": "anatomy",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "animal-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 4,
+ "active": true,
+ "alternateTitle": "animal",
+ "thumbnailHeight": null,
+ "name": "animal",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "art-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "art",
+ "thumbnailHeight": null,
+ "name": "art",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "clothing-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 8,
+ "active": true,
+ "alternateTitle": "clothing",
+ "thumbnailHeight": null,
+ "name": "clothing",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "crack",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "crack",
+ "thumbnailHeight": null,
+ "name": "crack",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "cut",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 2,
+ "active": true,
+ "alternateTitle": "cut",
+ "thumbnailHeight": null,
+ "name": "cut",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "damage",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 4,
+ "active": true,
+ "alternateTitle": "damage",
+ "thumbnailHeight": null,
+ "name": "damage",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "dirt-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "dirt",
+ "thumbnailHeight": null,
+ "name": "dirt",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "fabric-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 8,
+ "active": true,
+ "alternateTitle": "fabric",
+ "thumbnailHeight": null,
+ "name": "fabric",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "geometric",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 2,
+ "active": true,
+ "alternateTitle": "geometric",
+ "thumbnailHeight": null,
+ "name": "geometric",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "human-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 7,
+ "active": true,
+ "alternateTitle": "human",
+ "thumbnailHeight": null,
+ "name": "human",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "industrial-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 10,
+ "active": true,
+ "alternateTitle": "industrial",
+ "thumbnailHeight": null,
+ "name": "industrial",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "landscape-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 9,
+ "active": true,
+ "alternateTitle": "landscape",
+ "thumbnailHeight": null,
+ "name": "landscape",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "misc",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "misc",
+ "thumbnailHeight": null,
+ "name": "misc",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "nature-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "nature",
+ "thumbnailHeight": null,
+ "name": "nature",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "pattern",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 7,
+ "active": true,
+ "alternateTitle": "pattern",
+ "thumbnailHeight": null,
+ "name": "pattern",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "rock-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 7,
+ "active": true,
+ "alternateTitle": "rock",
+ "thumbnailHeight": null,
+ "name": "rock",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "rust-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "rust",
+ "thumbnailHeight": null,
+ "name": "rust",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "sculpture-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "sculpture",
+ "thumbnailHeight": null,
+ "name": "sculpture",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "stitches",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 12,
+ "active": true,
+ "alternateTitle": "stitches",
+ "thumbnailHeight": null,
+ "name": "stitches",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "stone-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "stone",
+ "thumbnailHeight": null,
+ "name": "stone",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "tree-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 4,
+ "active": true,
+ "alternateTitle": "tree",
+ "thumbnailHeight": null,
+ "name": "tree",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "wood-brush",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "wood",
+ "thumbnailHeight": null,
+ "name": "wood",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 95,
+ "active": true,
+ "alternateTitle": "brush",
+ "thumbnailHeight": null,
+ "name": "brush",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "material",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "animal-material",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "animal",
+ "thumbnailHeight": null,
+ "name": "animal",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "asphalt",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 5,
+ "active": true,
+ "alternateTitle": "asphalt",
+ "thumbnailHeight": null,
+ "name": "asphalt",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "bricks",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 5,
+ "active": true,
+ "alternateTitle": "bricks",
+ "thumbnailHeight": null,
+ "name": "bricks",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "ceramic",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "ceramic",
+ "thumbnailHeight": null,
+ "name": "ceramic",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "concrete",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 12,
+ "active": true,
+ "alternateTitle": "concrete",
+ "thumbnailHeight": null,
+ "name": "concrete",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "dirt",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 5,
+ "active": true,
+ "alternateTitle": "dirt",
+ "thumbnailHeight": null,
+ "name": "dirt",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "fabric",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 2,
+ "active": true,
+ "alternateTitle": "fabric",
+ "thumbnailHeight": null,
+ "name": "fabric",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "floor",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 15,
+ "active": true,
+ "alternateTitle": "floor",
+ "thumbnailHeight": null,
+ "name": "floor",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "food-material",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 2,
+ "active": true,
+ "alternateTitle": "food",
+ "thumbnailHeight": null,
+ "name": "food",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "fx",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "fx",
+ "thumbnailHeight": null,
+ "name": "fx",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "glass",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 2,
+ "active": true,
+ "alternateTitle": "glass",
+ "thumbnailHeight": null,
+ "name": "glass",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "grass",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "grass",
+ "thumbnailHeight": null,
+ "name": "grass",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "ground-material",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 6,
+ "active": true,
+ "alternateTitle": "ground",
+ "thumbnailHeight": null,
+ "name": "ground",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "human",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "human",
+ "thumbnailHeight": null,
+ "name": "human",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "ice",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "ice",
+ "thumbnailHeight": null,
+ "name": "ice",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "leather",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "leather",
+ "thumbnailHeight": null,
+ "name": "leather",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "liquid",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 4,
+ "active": true,
+ "alternateTitle": "liquid",
+ "thumbnailHeight": null,
+ "name": "liquid",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "marble",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "marble",
+ "thumbnailHeight": null,
+ "name": "marble",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "metal",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 16,
+ "active": true,
+ "alternateTitle": "metal",
+ "thumbnailHeight": null,
+ "name": "metal",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "organic",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "organic",
+ "thumbnailHeight": null,
+ "name": "organic",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "ornaments",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "ornaments",
+ "thumbnailHeight": null,
+ "name": "ornaments",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "paper",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "paper",
+ "thumbnailHeight": null,
+ "name": "paper",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "paving",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "paving",
+ "thumbnailHeight": null,
+ "name": "paving",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "plaster",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 4,
+ "active": true,
+ "alternateTitle": "plaster",
+ "thumbnailHeight": null,
+ "name": "plaster",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "plastic",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "plastic",
+ "thumbnailHeight": null,
+ "name": "plastic",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "rock",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 5,
+ "active": true,
+ "alternateTitle": "rock",
+ "thumbnailHeight": null,
+ "name": "rock",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "roofing",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "roofing",
+ "thumbnailHeight": null,
+ "name": "roofing",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "rubber",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "rubber",
+ "thumbnailHeight": null,
+ "name": "rubber",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "rust",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 4,
+ "active": true,
+ "alternateTitle": "rust",
+ "thumbnailHeight": null,
+ "name": "rust",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "sand",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 2,
+ "active": true,
+ "alternateTitle": "sand",
+ "thumbnailHeight": null,
+ "name": "sand",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "soil",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "soil",
+ "thumbnailHeight": null,
+ "name": "soil",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "stone",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 8,
+ "active": true,
+ "alternateTitle": "stone",
+ "thumbnailHeight": null,
+ "name": "stone",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "tech",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "tech",
+ "thumbnailHeight": null,
+ "name": "tech",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "tiles",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 10,
+ "active": true,
+ "alternateTitle": "tiles",
+ "thumbnailHeight": null,
+ "name": "tiles",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "wood",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 8,
+ "active": true,
+ "alternateTitle": "wood",
+ "thumbnailHeight": null,
+ "name": "wood",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 127,
+ "active": true,
+ "alternateTitle": "materials",
+ "thumbnailHeight": null,
+ "name": "material",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "model",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "aircraft",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "commercial",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "commercial",
+ "thumbnailHeight": null,
+ "name": "commercial",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "helicopter",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "helicopter",
+ "thumbnailHeight": null,
+ "name": "helicopter",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "historic-aircraft",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "historic",
+ "thumbnailHeight": null,
+ "name": "historic",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "jet",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "jet",
+ "thumbnailHeight": null,
+ "name": "jet",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "part-aircraft",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "part",
+ "thumbnailHeight": null,
+ "name": "part",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "private",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "private",
+ "thumbnailHeight": null,
+ "name": "private",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "aircraft",
+ "thumbnailHeight": null,
+ "name": "aircraft",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "art",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "design",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "design",
+ "thumbnailHeight": null,
+ "name": "design",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "drawing",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "drawing",
+ "thumbnailHeight": null,
+ "name": "drawing",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "literature",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 10,
+ "active": true,
+ "alternateTitle": "literature",
+ "thumbnailHeight": null,
+ "name": "literature",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "painting",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "painting",
+ "thumbnailHeight": null,
+ "name": "painting",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "sculpture",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "sculpture",
+ "thumbnailHeight": null,
+ "name": "sculpture",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "supplies",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "supplies",
+ "thumbnailHeight": null,
+ "name": "supplies",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 13,
+ "active": true,
+ "alternateTitle": "art",
+ "thumbnailHeight": null,
+ "name": "art",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "character",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "anatomy",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "anatomy",
+ "thumbnailHeight": null,
+ "name": "anatomy",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "child",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "child",
+ "thumbnailHeight": null,
+ "name": "child",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "clothing",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 4,
+ "active": true,
+ "alternateTitle": "clothing",
+ "thumbnailHeight": null,
+ "name": "clothing",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "fantasy",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "fantasy",
+ "thumbnailHeight": null,
+ "name": "fantasy",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "man",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 6,
+ "active": true,
+ "alternateTitle": "man",
+ "thumbnailHeight": null,
+ "name": "man",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "people",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "people",
+ "thumbnailHeight": null,
+ "name": "people",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "sci-fi-character",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "sci-fi",
+ "thumbnailHeight": null,
+ "name": "sci-fi",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "woman",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "woman",
+ "thumbnailHeight": null,
+ "name": "woman",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 14,
+ "active": true,
+ "alternateTitle": "character",
+ "thumbnailHeight": null,
+ "name": "character",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "exterior",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "building",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "building",
+ "thumbnailHeight": null,
+ "name": "building",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "cityspace",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "cityspace",
+ "thumbnailHeight": null,
+ "name": "cityspace",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "historic",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "historic",
+ "thumbnailHeight": null,
+ "name": "historic",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "house",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "house",
+ "thumbnailHeight": null,
+ "name": "house",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "industrial-exterior",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "industrial",
+ "thumbnailHeight": null,
+ "name": "industrial",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "landmark",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "landmark",
+ "thumbnailHeight": null,
+ "name": "landmark",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "landscape",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 14,
+ "active": true,
+ "alternateTitle": "landscape",
+ "thumbnailHeight": null,
+ "name": "landscape",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "public",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "public",
+ "thumbnailHeight": null,
+ "name": "public",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "sci-fi",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "sci-fi",
+ "thumbnailHeight": null,
+ "name": "sci-fi",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "stadium",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "stadium",
+ "thumbnailHeight": null,
+ "name": "stadium",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "street",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "street",
+ "thumbnailHeight": null,
+ "name": "street",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 17,
+ "active": true,
+ "alternateTitle": "exterior",
+ "thumbnailHeight": null,
+ "name": "exterior",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "food-drink",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "container",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "container",
+ "thumbnailHeight": null,
+ "name": "container",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "drink",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 12,
+ "active": true,
+ "alternateTitle": "drink",
+ "thumbnailHeight": null,
+ "name": "drink",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "drugs",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "drugs",
+ "thumbnailHeight": null,
+ "name": "drugs",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "food",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 4,
+ "active": true,
+ "alternateTitle": "food",
+ "thumbnailHeight": null,
+ "name": "food",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 19,
+ "active": true,
+ "alternateTitle": "food & drink",
+ "thumbnailHeight": null,
+ "name": "food & drink",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "furniture",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "bed",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "bed",
+ "thumbnailHeight": null,
+ "name": "bed",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "carpet",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 6,
+ "active": true,
+ "alternateTitle": "carpet",
+ "thumbnailHeight": null,
+ "name": "carpet",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "desk",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "desk",
+ "thumbnailHeight": null,
+ "name": "desk",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "fireplace",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "fireplace",
+ "thumbnailHeight": null,
+ "name": "fireplace",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "lighting",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 10,
+ "active": true,
+ "alternateTitle": "lighting",
+ "thumbnailHeight": null,
+ "name": "lighting",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "seating",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 14,
+ "active": true,
+ "alternateTitle": "seating",
+ "thumbnailHeight": null,
+ "name": "seating",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "shelving",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 9,
+ "active": true,
+ "alternateTitle": "shelving",
+ "thumbnailHeight": null,
+ "name": "shelving",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "sofa",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 7,
+ "active": true,
+ "alternateTitle": "sofa",
+ "thumbnailHeight": null,
+ "name": "sofa",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "storage",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "storage",
+ "thumbnailHeight": null,
+ "name": "storage",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "table",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 9,
+ "active": true,
+ "alternateTitle": "table",
+ "thumbnailHeight": null,
+ "name": "table",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 56,
+ "active": true,
+ "alternateTitle": "furniture",
+ "thumbnailHeight": null,
+ "name": "furniture",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "industrial",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "agriculture",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "agriculture",
+ "thumbnailHeight": null,
+ "name": "agriculture",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "communication",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "communication",
+ "thumbnailHeight": null,
+ "name": "communication",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "construction",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "construction",
+ "thumbnailHeight": null,
+ "name": "construction",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "container-industrial",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "container",
+ "thumbnailHeight": null,
+ "name": "container",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "machine",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "machine",
+ "thumbnailHeight": null,
+ "name": "machine",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "tool",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "tool",
+ "thumbnailHeight": null,
+ "name": "tool",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "utility-industrial",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "part",
+ "thumbnailHeight": null,
+ "name": "utility",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 6,
+ "active": true,
+ "alternateTitle": "industrial",
+ "thumbnailHeight": null,
+ "name": "industrial",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "interior",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "bathroom",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "bathroom",
+ "thumbnailHeight": null,
+ "name": "bathroom",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "bedroom",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 7,
+ "active": true,
+ "alternateTitle": "bedroom",
+ "thumbnailHeight": null,
+ "name": "bedroom",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "decoration",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 38,
+ "active": true,
+ "alternateTitle": "decoration",
+ "thumbnailHeight": null,
+ "name": "decoration",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "hall",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "hall",
+ "thumbnailHeight": null,
+ "name": "hall",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "kids-room",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 4,
+ "active": true,
+ "alternateTitle": "kids room",
+ "thumbnailHeight": null,
+ "name": "kids room",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "kitchen",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 23,
+ "active": true,
+ "alternateTitle": "kitchen",
+ "thumbnailHeight": null,
+ "name": "kitchen",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "living-room",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "living room",
+ "thumbnailHeight": null,
+ "name": "living room",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "office",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 11,
+ "active": true,
+ "alternateTitle": "office",
+ "thumbnailHeight": null,
+ "name": "office",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "utility",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 18,
+ "active": true,
+ "alternateTitle": "part",
+ "thumbnailHeight": null,
+ "name": "utility",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 106,
+ "active": true,
+ "alternateTitle": "interior",
+ "thumbnailHeight": null,
+ "name": "interior",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "military",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "air",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "air",
+ "thumbnailHeight": null,
+ "name": "air",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "equipment",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "equipment",
+ "thumbnailHeight": null,
+ "name": "equipment",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "ground",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "ground",
+ "thumbnailHeight": null,
+ "name": "ground",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "historic-military",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "historic",
+ "thumbnailHeight": null,
+ "name": "historic",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "naval",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "naval",
+ "thumbnailHeight": null,
+ "name": "naval",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "weapon",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "weapon",
+ "thumbnailHeight": null,
+ "name": "weapon",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "military",
+ "thumbnailHeight": null,
+ "name": "military",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "music",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "accessories",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "accessories",
+ "thumbnailHeight": null,
+ "name": "accessories",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "instruments",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "instruments",
+ "thumbnailHeight": null,
+ "name": "instruments",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "stage",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "stage",
+ "thumbnailHeight": null,
+ "name": "stage",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "studio",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "studio",
+ "thumbnailHeight": null,
+ "name": "studio",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "music",
+ "thumbnailHeight": null,
+ "name": "music",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "nature",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "animal-nature",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "animal",
+ "thumbnailHeight": null,
+ "name": "animal",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "atmosphere",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "atmosphere",
+ "thumbnailHeight": null,
+ "name": "atmosphere",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "landscape-nature",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "landscape",
+ "thumbnailHeight": null,
+ "name": "landscape",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "plant",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "plant",
+ "thumbnailHeight": null,
+ "name": "plant",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "tree",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "tree",
+ "thumbnailHeight": null,
+ "name": "tree",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "weather",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "weather",
+ "thumbnailHeight": null,
+ "name": "weather",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "nature",
+ "thumbnailHeight": null,
+ "name": "nature",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "space",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "astronomy",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "astronomy",
+ "thumbnailHeight": null,
+ "name": "astronomy",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "planets",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "planets",
+ "thumbnailHeight": null,
+ "name": "planets",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "sci-fi-space",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "sci-fi",
+ "thumbnailHeight": null,
+ "name": "sci-fi",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "spacecraft",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "spacecraft",
+ "thumbnailHeight": null,
+ "name": "spacecraft",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "space",
+ "thumbnailHeight": null,
+ "name": "space",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "sports",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "animal",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "animal",
+ "thumbnailHeight": null,
+ "name": "animal",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "exercise",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "exercise",
+ "thumbnailHeight": null,
+ "name": "exercise",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "extreme",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "extreme",
+ "thumbnailHeight": null,
+ "name": "extreme",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "individual",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "individual",
+ "thumbnailHeight": null,
+ "name": "individual",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "outdoor",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "outdoor",
+ "thumbnailHeight": null,
+ "name": "outdoor",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "team",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "team",
+ "thumbnailHeight": null,
+ "name": "team",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "sports",
+ "thumbnailHeight": null,
+ "name": "sports",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "technology",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "ai",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "ai",
+ "thumbnailHeight": null,
+ "name": "ai",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "audio",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "audio",
+ "thumbnailHeight": null,
+ "name": "audio",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "computer",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "computer",
+ "thumbnailHeight": null,
+ "name": "computer",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "medical",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "medical",
+ "thumbnailHeight": null,
+ "name": "medical",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "phone",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 2,
+ "active": true,
+ "alternateTitle": "phone",
+ "thumbnailHeight": null,
+ "name": "phone",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "photography",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 1,
+ "active": true,
+ "alternateTitle": "photography",
+ "thumbnailHeight": null,
+ "name": "photography",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "science",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "science",
+ "thumbnailHeight": null,
+ "name": "science",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "video",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "video",
+ "thumbnailHeight": null,
+ "name": "video",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 3,
+ "active": true,
+ "alternateTitle": "technology",
+ "thumbnailHeight": null,
+ "name": "technology",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "vehicle",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "bicycle",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "bicycle",
+ "thumbnailHeight": null,
+ "name": "bicycle",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "bus",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "bus",
+ "thumbnailHeight": null,
+ "name": "bus",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "car",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "car",
+ "thumbnailHeight": null,
+ "name": "car",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "historic-vehicle",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "historic",
+ "thumbnailHeight": null,
+ "name": "historic",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "industrial-vehicle",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "industrial",
+ "thumbnailHeight": null,
+ "name": "industrial",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "motorcycle",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "motorcycle",
+ "thumbnailHeight": null,
+ "name": "motorcycle",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "part-vehicle",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "part",
+ "thumbnailHeight": null,
+ "name": "part",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "train",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "train",
+ "thumbnailHeight": null,
+ "name": "train",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "truck",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "truck",
+ "thumbnailHeight": null,
+ "name": "truck",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "vehicle",
+ "thumbnailHeight": null,
+ "name": "vehicle",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "watercraft",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "historic-watercraft",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "historic",
+ "thumbnailHeight": null,
+ "name": "historic",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "industrial-watercraft",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "industrial",
+ "thumbnailHeight": null,
+ "name": "industrial",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "part-watercraft",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "part",
+ "thumbnailHeight": null,
+ "name": "part",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "personal",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "personal",
+ "thumbnailHeight": null,
+ "name": "personal",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "recreational",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "recreational",
+ "thumbnailHeight": null,
+ "name": "recreational",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "watercraft",
+ "thumbnailHeight": null,
+ "name": "watercraft",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 245,
+ "active": true,
+ "alternateTitle": "model",
+ "thumbnailHeight": null,
+ "name": "model",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "scene",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "templates",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "brush-templates",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 6,
+ "active": true,
+ "alternateTitle": "brush templates",
+ "thumbnailHeight": null,
+ "name": "brush templates",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 6,
+ "active": true,
+ "alternateTitle": "templates",
+ "thumbnailHeight": null,
+ "name": "templates",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 12,
+ "active": true,
+ "alternateTitle": "scene",
+ "thumbnailHeight": null,
+ "name": "scene",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "texture",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "texture",
+ "thumbnailHeight": null,
+ "name": "texture",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "world",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "animals",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [
+ {
+ "alternateUrl": "",
+ "slug": "mammals",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "Mammals",
+ "thumbnailHeight": null,
+ "name": "Mammals",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "Animals",
+ "thumbnailHeight": null,
+ "name": "Animals",
+ "thumbnail": null,
+ "description": ""
+ },
+ {
+ "alternateUrl": "",
+ "slug": "plants",
+ "metaKeywords": "",
+ "thumbnailWidth": null,
+ "metaExtra": "",
+ "children": [],
+ "order": 0,
+ "assetCount": 0,
+ "active": true,
+ "alternateTitle": "Plants",
+ "thumbnailHeight": null,
+ "name": "Plants",
+ "thumbnail": null,
+ "description": ""
+ }
+ ],
+ "order": 0,
+ "assetCount": 7,
+ "active": true,
+ "alternateTitle": "World",
+ "thumbnailHeight": null,
+ "name": "World",
+ "thumbnail": null,
+ "description": ""
+ }
+] \ No newline at end of file
diff --git a/blenderkit/download.py b/blenderkit/download.py
new file mode 100644
index 00000000..cfe5d1f9
--- /dev/null
+++ b/blenderkit/download.py
@@ -0,0 +1,832 @@
+# ##### 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 #####
+
+import threading
+import time
+import requests
+import shutil, sys, os, math
+import random
+import uuid
+import copy
+
+if "bpy" in locals():
+ import imp
+
+ imp.reload(paths)
+ imp.reload(append_link)
+
+else:
+ from blenderkit import paths, append_link, utils
+import bpy
+from bpy.props import (
+ IntProperty,
+ FloatProperty,
+ FloatVectorProperty,
+ StringProperty,
+ EnumProperty,
+ BoolProperty,
+ PointerProperty,
+)
+from bpy.app.handlers import persistent
+
+download_threads = []
+
+
+def check_missing():
+ '''checks for missing files, and possibly starts re-download of these into the scene'''
+ s = bpy.context.scene
+ # missing libs:
+ # TODO: put these into a panel and let the user decide if these should be downloaded.
+ missing = []
+ for l in bpy.data.libraries:
+ fp = l.filepath
+ if fp.startswith('//'):
+ fp = bpy.path.abspath(fp)
+ if not os.path.exists(fp) and l.get('asset_data') is not None:
+ missing.append(l)
+
+ print('missing libraries', missing)
+
+ for l in missing:
+ asset_data = l['asset_data']
+ downloaded = check_existing(asset_data)
+ if downloaded:
+ try:
+ l.reload()
+ except:
+ download(l['asset_data'], redownload=True)
+ else:
+ download(l['asset_data'], redownload=True)
+
+
+def check_unused():
+ '''find assets that have been deleted from scene but their library is still present.'''
+
+ used_libs = []
+ for ob in bpy.data.objects:
+ if ob.instance_collection is not None and ob.instance_collection.library is not None:
+ # used_libs[ob.instance_collection.name] = True
+ if ob.instance_collection.library not in used_libs:
+ used_libs.append(ob.instance_collection.library)
+
+ for ps in ob.particle_systems:
+ set = ps.settings
+ if ps.settings.render_type == 'GROUP' \
+ and ps.settings.instance_collection is not None \
+ and ps.settings.instance_collection.library not in used_libs:
+ used_libs.append(ps.settings.instance_collection)
+
+ for l in bpy.data.libraries:
+ if l not in used_libs:
+ print('attempt to remove this library: ', l.filepath)
+ # have to unlink all groups, since the file is a 'user' even if the groups aren't used at all...
+ for user_id in l.users_id:
+ if type(user_id) == bpy.types.Collection:
+ bpy.data.collections.remove(user_id)
+ l.user_clear()
+
+
+@persistent
+def scene_save(context):
+ ''' does cleanup of blenderkit props and sends a message to the server about assets used.'''
+ # TODO this can be optimized by merging these 2 functions, since both iterate over all objects.
+ check_unused()
+ report_usages()
+
+
+@persistent
+def scene_load(context):
+ '''restart broken downloads on scene load'''
+ t = time.time()
+ s = bpy.context.scene
+ global download_threads
+ download_threads = []
+
+ # commenting this out - old restore broken download on scene start. Might come back if downloads get recorded in scene
+ # reset_asset_ids = {}
+ # reset_obs = {}
+ # for ob in bpy.context.scene.collection.objects:
+ # if ob.name[:12] == 'downloading ':
+ # obn = ob.name
+ #
+ # asset_data = ob['asset_data']
+ #
+ # # obn.replace('#', '')
+ # # if asset_data['id'] not in reset_asset_ids:
+ #
+ # if reset_obs.get(asset_data['id']) is None:
+ # reset_obs[asset_data['id']] = [obn]
+ # reset_asset_ids[asset_data['id']] = asset_data
+ # else:
+ # reset_obs[asset_data['id']].append(obn)
+ # for asset_id in reset_asset_ids:
+ # asset_data = reset_asset_ids[asset_id]
+ # done = False
+ # if check_existing(asset_data):
+ # for obname in reset_obs[asset_id]:
+ # downloader = s.collection.objects[obname]
+ # done = try_finished_append(asset_data,
+ # model_location=downloader.location,
+ # model_rotation=downloader.rotation_euler)
+ #
+ # if not done:
+ # downloading = check_downloading(asset_data)
+ # if not downloading:
+ # print('redownloading %s' % asset_data['name'])
+ # download(asset_data, downloaders=reset_obs[asset_id], delete=True)
+
+ # check for group users that have been deleted, remove the groups /files from the file...
+ # TODO scenes fixing part... download the assets not present on drive,
+ # and erase from scene linked files that aren't used in the scene.
+ print('continue downlaods ', time.time() - t)
+ t = time.time()
+ check_missing()
+ print('missing check', time.time() - t)
+
+
+def get_scene_id():
+ '''gets scene id and possibly also generates a new one'''
+ bpy.context.scene['uuid'] = bpy.context.scene.get('uuid', str(uuid.uuid4()))
+ return bpy.context.scene['uuid']
+
+
+def report_usages():
+ '''report the usage of assets to the server.'''
+ mt = time.time()
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ api_key = user_preferences.api_key
+ sid = get_scene_id()
+ headers = {"accept": "application/json", "Authorization": "Bearer %s" % api_key}
+ url = paths.get_bkit_url() + paths.BLENDERKIT_REPORT_URL
+
+ assets = {}
+ asset_obs = []
+ scene = bpy.context.scene
+ asset_usages = {}
+
+ for ob in scene.collection.objects:
+ if ob.get('asset_data') != None:
+ asset_obs.append(ob)
+
+ for ob in asset_obs:
+ asset_data = ob['asset_data']
+ abid = asset_data['asset_base_id']
+
+ if assets.get(abid) is None:
+ asset_usages[abid] = {'count': 1}
+ assets[abid] = asset_data
+ else:
+ asset_usages[abid]['count'] += 1
+
+ # brushes
+ for b in bpy.data.brushes:
+ if b.get('asset_data') != None:
+ abid = b['asset_data']['asset_base_id']
+ asset_usages[abid] = {'count': 1}
+ assets[abid] = b['asset_data']
+ # materials
+ for ob in scene.collection.objects:
+ for ms in ob.material_slots:
+ m = ms.material
+
+ if m is not None and m.get('asset_data') is not None:
+
+ abid = m['asset_data']['asset_base_id']
+ if assets.get(abid) is None:
+ asset_usages[abid] = {'count': 1}
+ assets[abid] = m['asset_data']
+ else:
+ asset_usages[abid]['count'] += 1
+
+ assets_list = []
+ assets_reported = scene.get('assets reported', {})
+
+ new_assets_count = 0
+ for k in asset_usages.keys():
+ if k not in assets_reported.keys():
+ data = asset_usages[k]
+ list_item = {
+ 'asset': k,
+ 'usageCount': data['count'],
+ 'proximitySet': data.get('proximity', [])
+ }
+ assets_list.append(list_item)
+ new_assets_count += 1
+ if k not in assets_reported.keys():
+ assets_reported[k] = True
+
+ scene['assets reported'] = assets_reported
+
+ if new_assets_count == 0:
+ print('no new assets were added')
+ return;
+ usage_report = {
+ 'scene': sid,
+ 'reportType': 'save',
+ 'assetusageSet': assets_list
+ }
+
+ au = scene.get('assets used', {})
+ ad = scene.get('assets deleted', {})
+
+ ak = assets.keys()
+ for k in au.keys():
+ if k not in ak:
+ ad[k] = au[k]
+ else:
+ if k in ad:
+ ad.pop(k)
+
+ # scene['assets used'] = {}
+ for k in ak: # rewrite assets used.
+ scene['assets used'][k] = assets[k]
+
+ ###########check ratings herer too:
+ scene['assets rated'] = scene.get('assets rated', {})
+ for k in assets.keys():
+ scene['assets rated'][k] = scene['assets rated'].get(k, False)
+ thread = threading.Thread(target=utils.requests_post_thread, args=(url, usage_report, headers))
+ thread.start()
+ # r = requests.post(url, headers=headers, json=usage_report)
+ mt = time.time() - mt
+ print('report generation: ', mt)
+
+
+def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
+ '''Link asset to the scene'''
+
+ file_names = paths.get_download_filenames(asset_data)
+ props = None
+ #####
+ # how to do particle drop:
+ # link the group we are interested in( there are more groups in File!!!! , have to get the correct one!)
+ #
+ scene = bpy.context.scene
+ scene['assets used'] = scene.get('assets used', {})
+ scene['assets used'][asset_data['asset_base_id']] = asset_data.copy()
+
+ scene['assets rated'] = scene.get('assets rated', {})
+
+ id = asset_data['asset_base_id']
+ scene['assets rated'][id] = scene['assets rated'].get(id, False)
+
+ if asset_data['asset_type'] == 'scene':
+ scene = append_link.append_scene(file_names[0], link=False, fake_user=False)
+ props = scene.blenderkit
+ parent = scene
+
+ if asset_data['asset_type'] == 'model':
+ s = bpy.context.scene
+ downloaders = kwargs.get('downloaders')
+ s = bpy.context.scene
+ sprops = s.blenderkit_models
+ # TODO this is here because combinations of linking objects or appending groups are rather not-userfull
+ if sprops.append_method == 'LINK_GROUP':
+ sprops.append_link = 'LINK'
+ sprops.import_as = 'GROUP'
+ else:
+ sprops.append_link = 'APPEND'
+ sprops.import_as = 'INDIVIDUAL'
+
+ # set consistency for objects allready in scene, otherwise this literally breaks blender :)
+ ain = asset_in_scene(asset_data)
+ if ain is not False:
+ if ain == 'LINKED':
+ sprops.append_link = 'LINK'
+ sprops.import_as = 'GROUP'
+ else:
+ sprops.append_link = 'APPEND'
+ sprops.import_as = 'INDIVIDUAL'
+
+ # first get conditions for append link
+ link = sprops.append_link == 'LINK'
+ # then append link
+ if downloaders:
+ for downloader in downloaders:
+ # this cares for adding particle systems directly to target mesh, but I had to block it now,
+ # because of the sluggishnes of it. Possibly re-enable when it's possible to do this faster?
+ if 0: # 'particle_plants' in asset_data['tags']:
+ append_link.append_particle_system(file_names[-1],
+ target_object=kwargs['target_object'],
+ rotation=downloader['rotation'],
+ link=False,
+ name=asset_data['name'])
+ return
+
+ if sprops.import_as == 'GROUP':
+ parent, newobs = append_link.link_group(file_names[-1],
+ location=downloader['location'],
+ rotation=downloader['rotation'],
+ link=link,
+ name=asset_data['name'])
+
+ else:
+ parent, newobs = append_link.append_objects(file_names[-1],
+ location=downloader['location'],
+ rotation=downloader['rotation'],
+ link=link,
+ name=asset_data['name'])
+ if parent.type == 'EMPTY' and link:
+ bmin = asset_data['bbox_min']
+ bmax = asset_data['bbox_max']
+ size_min = min(1.0, (bmax[0] - bmin[0] + bmax[1] - bmin[1] + bmax[2] - bmin[2]) / 3)
+ parent.empty_display_size = size_min
+
+ elif kwargs.get('model_location') is not None:
+ if sprops.import_as == 'GROUP':
+ parent, newobs = append_link.link_group(file_names[-1],
+ location=kwargs['model_location'],
+ rotation=kwargs['model_rotation'],
+ link=link,
+ name=asset_data['name'])
+ else:
+ parent, newobs = append_link.append_objects(file_names[-1],
+ location=kwargs['model_location'],
+ rotation=kwargs['model_rotation'],
+ link=link)
+ if parent.type == 'EMPTY' and link:
+ bmin = asset_data['bbox_min']
+ bmax = asset_data['bbox_max']
+ size_min = min(1.0, (bmax[0] - bmin[0] + bmax[1] - bmin[1] + bmax[2] - bmin[2]) / 3)
+ parent.empty_display_size = size_min
+
+ if link:
+ group = parent.instance_collection
+
+ lib = group.library
+ lib['asset_data'] = asset_data
+
+ elif asset_data['asset_type'] == 'brush':
+
+ # TODO if allready in scene, should avoid reappending.
+ inscene = False
+ for b in bpy.data.brushes:
+
+ if b.blenderkit.id == asset_data['id']:
+ inscene = True
+ brush = b
+ break;
+ if not inscene:
+ brush = append_link.append_brush(file_names[-1], link=False, fake_user=False)
+
+ thumbnail_name = asset_data['thumbnail'].split(os.sep)[-1]
+ tempdir = paths.get_temp_dir('brush_search')
+ thumbpath = os.path.join(tempdir, thumbnail_name)
+ asset_thumbs_dir = paths.get_download_dirs('brush')[0]
+ asset_thumb_path = os.path.join(asset_thumbs_dir, thumbnail_name)
+ shutil.copy(thumbpath, asset_thumb_path)
+ brush.icon_filepath = asset_thumb_path
+
+ if bpy.context.sculpt_object:
+ bpy.context.tool_settings.sculpt.brush = brush
+ elif bpy.context.image_paint_object: # could be just else, but for future possible more types...
+ bpy.context.tool_settings.image_paint.brush = brush
+ # TODO set brush by by asset data(user can be downloading while switching modes.)
+
+ # bpy.context.tool_settings.image_paint.brush = brush
+ props = brush.blenderkit
+ parent = brush
+
+ elif asset_data['asset_type'] == 'material':
+ inscene = False
+ for m in bpy.data.materials:
+ if m.blenderkit.id == asset_data['id']:
+ inscene = True
+ material = m
+ break;
+ if not inscene:
+ material = append_link.append_material(file_names[-1], link=False, fake_user=False)
+ target_object = bpy.data.objects[kwargs['target_object']]
+
+ if len(target_object.material_slots) == 0:
+ target_object.data.materials.append(material)
+ else:
+ target_object.material_slots[kwargs['material_target_slot']].material = material
+
+ props = material.blenderkit
+ parent = material
+
+ parent['asset_data'] = asset_data # TODO remove this??? should write to blenderkit Props?
+ # moving reporting to on save.
+ # report_use_success(asset_data['id'])
+
+
+@bpy.app.handlers.persistent
+def timer_update(): # TODO might get moved to handle all blenderkit stuff, not to slow down.
+ '''check for running and finished downloads and react. write progressbars too.'''
+ global download_threads
+ if len(download_threads) == 0:
+ return 1
+ s = bpy.context.scene
+ for threaddata in download_threads:
+ t = threaddata[0]
+ asset_data = threaddata[1]
+ tcom = threaddata[2]
+
+ progress_bars = []
+ downloaders = []
+
+ if t.is_alive(): # set downloader size
+ sr = bpy.context.scene.get('search results')
+ if sr is not None:
+ for r in sr:
+ if asset_data['id'] == r['id']:
+ r['downloaded'] = tcom.progress
+
+ if not t.is_alive():
+ if tcom.error:
+ sprops = utils.get_search_props()
+ sprops.report = tcom.report
+ download_threads.remove(threaddata)
+ return
+ file_names = paths.get_download_filenames(asset_data)
+ wm = bpy.context.window_manager
+
+ at = asset_data['asset_type']
+ if ((bpy.context.mode == 'OBJECT' and (at == 'model' \
+ or at == 'material'))) \
+ or ((at == 'brush') \
+ and wm.get(
+ 'appendable') == True) or at == 'scene': # don't do this stuff in editmode and other modes, just wait...
+ download_threads.remove(threaddata)
+
+ # duplicate file if the global and subdir are used in prefs
+ if len(file_names) == 2: # todo this should try to check if both files exist and are ok.
+ shutil.copyfile(file_names[0], file_names[1])
+
+ print('appending asset')
+ # progress bars:
+ if bpy.context.scene['search results'] is not None:
+ for sres in bpy.context.scene['search results']:
+ if asset_data['id'] == sres['id']:
+ sres['downloaded'] = 100
+ # we need to check if mouse isn't down, which means an operator can be running.
+ # Especially for sculpt mode, where appending a brush during a sculpt stroke causes crasehes
+ #
+
+ if tcom.passargs.get('redownload'):
+ # handle lost libraries here:
+ for l in bpy.data.libraries:
+ if l.get('asset_data') is not None and l['asset_data']['id'] == asset_data['id']:
+ l.filepath = file_names[-1]
+ l.reload()
+ else:
+ done = try_finished_append(asset_data, **tcom.passargs)
+ if not done:
+ at = asset_data['asset_type']
+ if at in ('model', 'material'):
+ download(asset_data, **tcom.passargs)
+ elif asset_data['asset_type'] == 'material':
+ download(asset_data, **tcom.passargs)
+ elif asset_data['asset_type'] == 'scene':
+ download(asset_data, **tcom.passargs)
+ elif asset_data['asset_type'] == 'brush' or asset_data['asset_type'] == 'texture':
+ download(asset_data, **tcom.passargs)
+ print('finished download thread')
+ return .2
+
+
+def main_thread(asset_data, tcom, scene_id, api_key):
+ '''try to download file from blenderkit'''
+
+ # TODO get real link here...
+ get_download_url(asset_data, scene_id, api_key, tcom=tcom)
+ if tcom.error:
+ return
+ # only now we can check if the file allready exists. This should have 2 levels, for materials and for brushes
+ # different than for the non free content. delete is here when called after failed append tries.
+ if check_existing(asset_data) and not tcom.passargs.get('delete'):
+ # this sends the thread for processing, where another check should occur, since the file might be corrupted.
+ tcom.downloaded = 100
+ print('not downloading, trying to append again')
+ return;
+ file_name = paths.get_download_filenames(asset_data)[0] # prefer global dir if possible.
+ # for k in asset_data:
+ # print(asset_data[k])
+
+ with open(file_name, "wb") as f:
+ print("Downloading %s" % file_name)
+ headers = {"accept": "application/json",
+ "Authorization": "Bearer %s" % api_key}
+
+ response = requests.get(asset_data['url'], stream=True)
+ total_length = response.headers.get('Content-Length')
+
+ if total_length is None: # no content length header
+ f.write(response.content)
+ else:
+ tcom.file_size = int(total_length)
+ dl = 0
+ for data in response.iter_content(chunk_size=4096):
+ dl += len(data)
+ tcom.downloaded = dl
+ tcom.progress = int(100 * tcom.downloaded / tcom.file_size)
+ f.write(data)
+
+
+class ThreadCom: # object passed to threads to read background process stdout info
+ def __init__(self):
+ self.file_size = 1000000000000000 # property that gets written to.
+ self.downloaded = 0
+ self.lasttext = ''
+ self.error = False
+ self.report = ''
+ self.progress = 0.0
+ self.passargs = {}
+
+
+def download(asset_data, **kwargs):
+ '''start the download thread'''
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ api_key = user_preferences.api_key
+ scene_id = get_scene_id()
+ if api_key == '':
+ props = utils.get_search_props()
+ props.report = 'Register online to use the free library.'
+ return
+ tcom = ThreadCom()
+
+ tcom.passargs = kwargs
+
+ # incoming data can be either directly dict from python, or blender id property
+ # (recovering failed downloads on reload)
+ if type(asset_data) == dict:
+ asset_data = copy.deepcopy(asset_data)
+ else:
+ asset_data = asset_data.to_dict()
+
+ # main_thread(asset_data, tcom, scene_id, api_key)
+ readthread = threading.Thread(target=main_thread, args=([asset_data, tcom, scene_id, api_key]), daemon=True)
+ readthread.start()
+
+ global download_threads
+ download_threads.append(
+ [readthread, asset_data, tcom])
+
+
+def check_downloading(asset_data, **kwargs):
+ ''' check if an asset is allready downloading, if yes, just make a progress bar with downloader object.'''
+ global download_threads
+
+ downloading = False
+
+ for p in download_threads:
+ p_asset_data = p[1]
+ if p_asset_data['id'] == asset_data['id']:
+ at = asset_data['asset_type']
+ if at in ('model', 'material'):
+ downloader = {'location': kwargs['model_location'],
+ 'rotation': kwargs['model_rotation']}
+ p[2].passargs['downloaders'].append(downloader)
+ downloading = True
+
+ return downloading
+
+
+def check_existing(asset_data):
+ ''' check if the object exists on the hard drive'''
+ fexists = False
+
+ file_names = paths.get_download_filenames(asset_data)
+
+ print('check if file allready exists')
+ if len(file_names) == 2:
+ # TODO this should check also for failed or running downloads.
+ # If download is running, assign just the running thread. if download isn't running but the file is wrong size,
+ # delete file and restart download (or continue downoad? if possible.)
+ if os.path.isfile(file_names[0]) and not os.path.isfile(file_names[1]):
+ shutil.copy(file_names[0], file_names[1])
+ elif not os.path.isfile(file_names[0]) and os.path.isfile(
+ file_names[1]): # only in case of changed settings or deleted/moved global dict.
+ shutil.copy(file_names[1], file_names[0])
+
+ if len(file_names) > 0 and os.path.isfile(file_names[0]):
+ fexists = True
+ return fexists
+
+
+def try_finished_append(asset_data, **kwargs): # location=None, material_target=None):
+ ''' try to append asset, if not successfull delete source files.
+ This means probably wrong download, so download should restart'''
+ file_names = paths.get_download_filenames(asset_data)
+ done = False
+ print('try to append allready existing asset')
+ if len(file_names) > 0:
+ if os.path.isfile(file_names[-1]):
+ kwargs['name'] = asset_data['name']
+ try:
+ append_asset(asset_data, **kwargs)
+ done = True
+ except Exception as e:
+ print(e)
+ for f in file_names:
+ try:
+ os.remove(f)
+ except:
+ e = sys.exc_info()[0]
+ print(e)
+ pass;
+ done = False
+ return done
+
+
+def asset_in_scene(asset_data):
+ '''checks if the asset is allready in scene. If yes, modifies asset data so the asset can be reached again.'''
+ scene = bpy.context.scene
+ au = scene.get('assets used', {})
+
+ if id in au.keys():
+ ad = au[id]
+ if ad.get('file_name') != None:
+ asset_data['file_name'] = ad['file_name']
+ asset_data['url'] = ad['url']
+
+ c = bpy.data.collections.get(ad['name'])
+ if c is not None:
+ if c.users > 0:
+ return 'LINKED'
+ return 'APPENDED'
+ return False
+
+
+def fprint(text):
+ print('###################################################################################')
+ print('\n\n\n')
+ print(text)
+ print('\n\n\n')
+ print('###################################################################################')
+
+
+def get_download_url(asset_data, scene_id, api_key, tcom=None):
+ ''''retrieves the download url. The server checks if user can download the item.'''
+ mt = time.time()
+ headers = {
+ "accept": "application/json",
+ "Authorization": "Bearer %s" % api_key,
+ }
+ data = {
+ 'scene_uuid': scene_id
+ }
+ r = None
+ try:
+ r = requests.get(asset_data['download_url'], params=data, headers=headers)
+ fprint(r.text)
+ except Exception as e:
+ print(e)
+ if tcom is not None:
+ tcom.error = True
+
+ if r == None:
+ tcom.report = 'Connection Error'
+ tcom.error = True
+ return 'Connection Error'
+ if r.status_code < 400:
+ data = r.json()
+ url = data['filePath']
+ asset_data['url'] = url
+ asset_data['file_name'] = paths.extract_filename_from_url(url)
+ return True
+
+ if r.status_code == 403:
+ tcom.report = 'Available only in higher plans.'
+ tcom.error = True
+ return 'Available only in higher plans.'
+ if r.status_code == 401:
+ tcom.report = 'Invalid API key'
+ tcom.error = True
+ return 'Invalid API key'
+ elif r.status_code >= 500:
+ tcom.report = 'Server error'
+ tcom.error = True
+
+
+def start_download(asset_data, **kwargs):
+ '''
+ check if file isn't downloading or doesn't exist, then start new download
+ '''
+ # first check if the asset is allready in scene. We can use that asset without checking with server
+ quota_ok = asset_in_scene(asset_data) is not False
+
+ # otherwise, check on server
+
+ s = bpy.context.scene
+ done = False
+ # is the asseet being currently downloaded?
+ downloading = check_downloading(asset_data, **kwargs)
+ if not downloading:
+ # check if there are files allready. This check happens 2x once here(for free assets),
+ # once in thread(for non-free)
+ fexists = check_existing(asset_data)
+
+ if fexists and quota_ok:
+ done = try_finished_append(asset_data, **kwargs)
+ # else:
+ # props = utils.get_search_props()
+ # props.report = str('asset ')
+ if not done:
+ at = asset_data['asset_type']
+ if at in ('model', 'material'):
+ downloader = {'location': kwargs['model_location'],
+ 'rotation': kwargs['model_rotation']}
+ download(asset_data, downloaders=[downloader], **kwargs)
+
+ elif asset_data['asset_type'] == 'scene':
+ download(asset_data, **kwargs)
+ elif asset_data['asset_type'] == 'brush' or asset_data['asset_type'] == 'texture':
+ download(asset_data)
+
+
+asset_types = (
+ ('MODEL', 'Model', 'set of objects'),
+ ('SCENE', 'Scene', 'scene'),
+ ('MATERIAL', 'Material', 'any .blend Material'),
+ ('TEXTURE', 'Texture', 'a texture, or texture set'),
+ ('BRUSH', 'Brush', 'brush, can be any type of blender brush'),
+ ('ADDON', 'Addon', 'addnon'),
+)
+
+
+class BlenderkitDownloadOperator(bpy.types.Operator):
+ """Download and link asset to scene. Only link if asset allready available locally."""
+ bl_idname = "scene.blenderkit_download"
+ bl_label = "BlenderKit Asset Download"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ asset_type: EnumProperty(
+ name="Type",
+ items=asset_types,
+ description="Type of download",
+ default="MODEL",
+ )
+ asset_index: IntProperty(name="Asset Index", description='asset index in search results', default=-1)
+
+ target_object: StringProperty(
+ name="Material Target Object",
+ description="",
+ default="")
+ material_target_slot: IntProperty(name="Asset Index", description='asset index in search results', default=0)
+ model_location: FloatVectorProperty(name='Asset Location', default=(0, 0, 0))
+ model_rotation: FloatVectorProperty(name='Asset Rotation', default=(0, 0, 0))
+
+ cast_parent: StringProperty(
+ name="Particles Target Object",
+ description="",
+ default="")
+
+ # @classmethod
+ # def poll(cls, context):
+ # return bpy.context.window_manager.BlenderKitModelThumbnails is not ''
+
+ def execute(self, context):
+ s = bpy.context.scene
+ sr = s['search results']
+
+ asset_data = sr[self.asset_index].to_dict() # TODO CHECK ALL OCCURANCES OF PASSING BLENDER ID PROPS TO THREADS!
+ au = s.get('assets used')
+ if au == None:
+ s['assets used'] = {}
+ if asset_data['asset_base_id'] in s.get('assets used'):
+ asset_data = s['assets used'][asset_data['asset_base_id']].to_dict()
+ atype = asset_data['asset_type']
+ if bpy.context.mode != 'OBJECT' and (
+ atype == 'model' or atype == 'material') and bpy.context.active_object is not None:
+ bpy.ops.object.mode_set(mode='OBJECT')
+ kwargs = {
+ 'cast_parent': self.cast_parent,
+ 'target_object': self.target_object,
+ 'material_target_slot': self.material_target_slot,
+ 'model_location': tuple(self.model_location),
+ 'model_rotation': tuple(self.model_rotation)
+ }
+
+ start_download(asset_data, **kwargs)
+ return {'FINISHED'}
+
+
+def register_download():
+ bpy.utils.register_class(BlenderkitDownloadOperator)
+ bpy.app.handlers.load_post.append(scene_load)
+ bpy.app.handlers.save_pre.append(scene_save)
+ # bpy.app.timers.register(timer_update, persistent = True)
+
+
+def unregister_download():
+ bpy.utils.unregister_class(BlenderkitDownloadOperator)
+ bpy.app.handlers.load_post.remove(scene_load)
+ bpy.app.handlers.save_pre.remove(scene_save)
+ # bpy.app.timers.unregister(timer_update)
diff --git a/blenderkit/overrides.py b/blenderkit/overrides.py
new file mode 100644
index 00000000..d9fc8028
--- /dev/null
+++ b/blenderkit/overrides.py
@@ -0,0 +1,261 @@
+# ##### 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 #####
+
+import bpy, mathutils
+from bpy.types import (
+ Operator)
+
+from blenderkit import utils
+
+
+def getNodes(nt, node_type='OUTPUT_MATERIAL'):
+ chnodes = nt.nodes[:]
+ nodes = []
+ while len(chnodes) > 0:
+ n = chnodes.pop()
+ if n.type == node_type:
+ nodes.append(n)
+ if n.type == 'GROUP':
+ chnodes.extend(n.node_tree.nodes)
+ return nodes
+
+
+def getShadersCrawl(nt, chnodes):
+ shaders = []
+ done_nodes = chnodes[:]
+
+ while len(chnodes) > 0:
+ check_node = chnodes.pop()
+ is_shader = False
+ for o in check_node.outputs:
+ if o.type == 'SHADER':
+ is_shader = True
+ for i in check_node.inputs:
+ if i.type == 'SHADER':
+ is_shader = False # this is for mix nodes and group inputs..
+ if len(i.links) > 0:
+ for l in i.links:
+ fn = l.from_node
+ if fn not in done_nodes:
+ done_nodes.append(fn)
+ chnodes.append(fn)
+ if fn.type == 'GROUP':
+ group_outputs = getNodes(fn.node_tree, node_type='GROUP_OUTPUT')
+ shaders.extend(getShadersCrawl(fn.node_tree, group_outputs))
+
+ if check_node.type == 'GROUP':
+ is_shader = False
+
+ if is_shader:
+ shaders.append((check_node, nt))
+
+ return (shaders)
+
+
+def addColorCorrectors(material):
+ nt = material.node_tree
+ output = getNodes(nt, 'OUTPUT_MATERIAL')[0]
+ shaders = getShadersCrawl(nt, [output])
+
+ correctors = []
+ for shader, nt in shaders:
+
+ if shader.type != 'BSDF_TRANSPARENT': # exclude transparent for color tweaks
+ for i in shader.inputs:
+ if i.type == 'RGBA':
+ if len(i.links) > 0:
+ l = i.links[0]
+ if not (l.from_node.type == 'GROUP' and l.from_node.node_tree.name == 'bkit_asset_tweaker'):
+ from_socket = l.from_socket
+ to_socket = l.to_socket
+
+ g = nt.nodes.new(type='ShaderNodeGroup')
+ g.node_tree = bpy.data.node_groups['bkit_asset_tweaker']
+ g.location = shader.location
+ g.location.x -= 100
+
+ nt.links.new(from_socket, g.inputs[0])
+ nt.links.new(g.outputs[0], to_socket)
+ else:
+ g = l.from_node
+ tweakers.append(g)
+ else:
+ g = nt.nodes.new(type='ShaderNodeGroup')
+ g.node_tree = bpy.data.node_groups['bkit_asset_tweaker']
+ g.location = shader.location
+ g.location.x -= 100
+
+ nt.links.new(g.outputs[0], i)
+ correctors.append(g)
+
+
+def modelProxy():
+ s = bpy.context.scene
+ ao = bpy.context.active_object
+ if utils.is_linked_asset(ao):
+ utils.activate(ao)
+
+ g = ao.instance_collection
+
+ rigs = []
+
+ for ob in g.objects:
+ if ob.type == 'ARMATURE':
+ rigs.append(ob)
+
+ if len(rigs) == 1:
+
+ ao.instance_collection = None
+ bpy.ops.object.duplicate()
+ new_ao = bpy.context.view_layer.objects.active
+ new_ao.instance_collection = g
+ new_ao.empty_display_type = 'SPHERE'
+ new_ao.empty_display_size *= 0.1
+
+ bpy.ops.object.proxy_make(object=rigs[0].name)
+ proxy = bpy.context.active_object
+ bpy.context.view_layer.objects.active = ao
+ ao.select_set(True)
+ new_ao.select_set(True)
+ new_ao.use_extra_recalc_object = True
+ new_ao.use_extra_recalc_data = True
+ bpy.ops.object.parent_set(type='OBJECT', keep_transform=True)
+ return True
+ else: # TODO report this to ui
+ print('not sure what to proxify')
+ return False
+
+
+class BringToScene(Operator):
+ """Bring linked object hierarchy to scene and make it editable."""
+
+ bl_idname = "object.blenderkit_bring_to_scene"
+ bl_label = "BlenderKit bring objects to scene"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ return bpy.context.active_object is not None
+
+ def execute(self, context):
+ import bpy
+
+ s = bpy.context.scene
+ sobs = s.collection.all_objects
+ aob = bpy.context.active_object
+ dg = aob.instance_collection
+ instances = []
+
+ # first, find instances of this collection in the scene
+ for ob in sobs:
+ if ob.instance_collection == dg and ob not in instances:
+ instances.append(ob)
+ ob.instance_collection = None
+ ob.instance_type = 'NONE'
+ # dg.make_local
+ parent = None
+ obs = []
+ for ob in dg.objects:
+ dg.objects.unlink(ob)
+ try:
+ s.collection.objects.link(ob)
+
+ if ob.parent == None:
+ parent = ob
+ bpy.context.view_layer.objects.active = parent
+ except Exception as e:
+ print(e)
+ ob.select_set(True)
+ obs.append(ob)
+ bpy.ops.object.make_local(type='ALL')
+ for ob in obs:
+ ob.select_set(True)
+
+ related = []
+
+ for i, ob in enumerate(instances):
+ if i > 0:
+ bpy.ops.object.duplicate(linked=True)
+
+ related.append([ob, bpy.context.active_object, mathutils.Vector(bpy.context.active_object.scale)])
+
+ for relation in related:
+ bpy.ops.object.select_all(action='DESELECT')
+ bpy.context.view_layer.objects.active = relation[0]
+ relation[0].select_set(True)
+ relation[1].select_set(True)
+ relation[1].matrix_world = relation[0].matrix_world
+ relation[1].scale.x = relation[2].x * relation[0].scale.x
+ relation[1].scale.y = relation[2].y * relation[0].scale.y
+ relation[1].scale.z = relation[2].z * relation[0].scale.z
+ bpy.ops.object.parent_set(type='OBJECT', keep_transform=True)
+
+ return {'FINISHED'}
+
+
+class ModelProxy(Operator):
+ """Tooltip"""
+ bl_idname = "object.blenderkit_make_proxy"
+ bl_label = "BlenderKit Make Proxy"
+
+ @classmethod
+ def poll(cls, context):
+ return bpy.context.active_object is not None
+
+ def execute(self, context):
+ result = modelProxy()
+ if not result:
+ self.report({'INFO'}, 'No proxy made.There is no armature or more than one in the model.')
+ return {'FINISHED'}
+
+
+class ColorCorrector(Operator):
+ """Tooltip"""
+ bl_idname = "object.blenderkit_color_corrector"
+ bl_label = "Add color corrector"
+
+ @classmethod
+ def poll(cls, context):
+ return bpy.context.active_object is not None
+
+ def execute(self, context):
+ ao = bpy.context.active_object
+ g = ao.instance_collection
+ ao['color correctors'] = []
+ mats = []
+
+ for o in g.objects:
+ for ms in o.material_slots:
+ if ms.material not in mats:
+ mats.append(ms.material)
+ for mat in mats:
+ correctors = addColorCorrectors(mat)
+
+ return 'FINISHED'
+
+
+def register_overrides():
+ bpy.utils.register_class(BringToScene)
+ bpy.utils.register_class(ModelProxy)
+ bpy.utils.register_class(ColorCorrector)
+
+
+def unregister_overrides():
+ bpy.utils.unregister_class(BringToScene)
+ bpy.utils.unregister_class(ModelProxy)
+ bpy.utils.unregister_class(ColorCorrector)
diff --git a/blenderkit/paths.py b/blenderkit/paths.py
new file mode 100644
index 00000000..71479c7e
--- /dev/null
+++ b/blenderkit/paths.py
@@ -0,0 +1,217 @@
+# ##### 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 #####
+
+import bpy, os, sys
+
+BLENDERKIT_API_LOCAL = "http://localhost:8001/api/v1/"
+BLENDERKIT_API_MAIN = "https://www.blenderkit.com/api/v1/"
+BLENDERKIT_API_DEVEL = "https://devel.blenderkit.com/api/v1/"
+BLENDERKIT_REPORT_URL = "usage_report/"
+BLENDERKIT_USER_ASSETS = "https://www.blenderkit.com/my-assets"
+BLENDERKIT_PLANS = "https://www.blenderkit.com/plans/pricing/"
+BLENDERKIT_MANUAL = "https://youtu.be/1hVgcQhIAo8"
+BLENDERKIT_MODEL_UPLOAD_INSTRUCTIONS_URL = "https://www.blenderkit.com/docs/upload/"
+BLENDERKIT_MATERIAL_UPLOAD_INSTRUCTIONS_URL = "https://www.blenderkit.com/docs/uploading-material/"
+BLENDERKIT_SIGNUP_URL = "https://www.blenderkit.com/accounts/register/"
+BLENDERKIT_ADDON_URL = "https://www.blenderkit.com/api/v1/assets/6923b215-7df0-46f3-95ae-a2b5ff44ddd5/"
+BLENDERKIT_ADDON_FILE_URL = "https://www.blenderkit.com/get-blenderkit/"
+_presets = os.path.join(bpy.utils.user_resource('SCRIPTS'), "presets")
+BLENDERKIT_SETTINGS_FILENAME = os.path.join(_presets, "bkit.json")
+
+
+def get_bkit_url():
+ d = bpy.app.debug_value
+ # d = 2
+ if d == 1:
+ url = BLENDERKIT_API_LOCAL
+ elif d == 2:
+ url = BLENDERKIT_API_DEVEL
+ else:
+ url = BLENDERKIT_API_MAIN
+ return url
+
+
+def get_categories_filepath():
+ tempdir = get_temp_dir()
+ return os.path.join(tempdir, 'categories.json')
+
+
+def get_temp_dir(subdir=None):
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+
+ # tempdir = user_preferences.temp_dir
+ tempdir = os.path.join(user_preferences.global_dir, 'temp')
+ if tempdir.startswith('//'):
+ tempdir = bpy.path.abspath(tempdir)
+ if not os.path.exists(tempdir):
+ os.makedirs(tempdir)
+ if subdir is not None:
+ tempdir = tempdir + os.sep + subdir
+ if not os.path.exists(tempdir):
+ os.makedirs(tempdir)
+ return tempdir
+
+
+def get_download_dirs(asset_type):
+ ''' get directories where assets will be downloaded'''
+ subdmapping = {'brush': 'brushes', 'texture': 'textures', 'model': 'models', 'scene': 'scenes',
+ 'material': 'materials'}
+
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ dirs = []
+ if user_preferences.directory_behaviour == 'BOTH' or 'GLOBAL':
+ ddir = user_preferences.global_dir
+ if ddir.startswith('//'):
+ ddir = bpy.path.abspath(ddir)
+ if not os.path.exists(ddir):
+ os.makedirs(ddir)
+
+ subdirs = ['brushes', 'textures', 'models', 'scenes', 'materials']
+ for subd in subdirs:
+ subdir = ddir + os.sep + subd
+ if not os.path.exists(subdir):
+ os.makedirs(subdir)
+ if subdmapping[asset_type] == subd:
+ dirs.append(subdir)
+ if (
+ user_preferences.directory_behaviour == 'BOTH' or user_preferences.directory_behaviour == 'LOCAL') and bpy.data.is_saved: # it's important local get's solved as second, since for the linking process only last filename will be taken. For download process first name will be taken and if 2 filenames were returned, file will be copied to the 2nd path.
+ ddir = user_preferences.project_subdir
+ if ddir.startswith('//'):
+ ddir = bpy.path.abspath(ddir)
+ if not os.path.exists(ddir):
+ os.makedirs(ddir)
+
+ subdirs = ['textures', 'models', 'scenes', 'materials'] # brushes get stored only globally.
+ for subd in subdirs:
+ subdir = ddir + os.sep + subd
+ if not os.path.exists(subdir):
+ os.makedirs(subdir)
+ if subdmapping[asset_type] == subd:
+ dirs.append(subdir)
+
+ return dirs
+
+
+def slugify(slug):
+ """
+ Normalizes string, converts to lowercase, removes non-alpha characters,
+ and converts spaces to hyphens.
+ """
+
+ import unicodedata, re
+ slug = slug.lower()
+ slug.replace('.', '_')
+ slug = slug.replace(' ', '_')
+ # import re
+ # slug = unicodedata.normalize('NFKD', slug)
+ # slug = slug.encode('ascii', 'ignore').lower()
+ slug = re.sub(r'[^a-z0-9]+.- ', '-', slug).strip('-')
+ slug = re.sub(r'[-]+', '-', slug)
+ slug = re.sub(r'/', '_', slug)
+ return slug
+
+
+def extract_filename_from_url(url):
+ if url is not None:
+ imgname = url.split('/')[-1]
+ imgname = imgname.split('?')[0]
+ return imgname
+ return ''
+
+
+def get_download_filenames(asset_data):
+ dirs = get_download_dirs(asset_data['asset_type'])
+ file_names = []
+ # fn = asset_data['file_name'].replace('blend_', '')
+ if asset_data.get('url') is not None:
+ # this means asset is allready in scene and we don't nedd to check
+
+ fn = extract_filename_from_url(asset_data['url'])
+ fn.replace('_blend', '')
+ n = slugify(asset_data['name']) + '_' + fn
+ # n = 'x.blend'
+ # strs = (n, asset_data['name'], asset_data['file_name'])
+ for d in dirs:
+ file_name = os.path.join(d, n)
+ file_names.append(file_name)
+ return file_names
+
+
+def delete_asset_debug(asset_data):
+ from blenderkit import download
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ api_key = user_preferences.api_key
+
+ download.get_download_url(asset_data, download.get_scene_id(), api_key)
+
+ file_names = get_download_filenames(asset_data)
+ for f in file_names:
+ if os.path.isfile(f):
+ try:
+ print(f)
+ os.remove(f)
+ except:
+ e = sys.exc_info()[0]
+ print(e)
+ pass;
+
+
+def get_clean_filepath():
+ script_path = os.path.dirname(os.path.realpath(__file__))
+ subpath = "blendfiles" + os.sep + "cleaned.blend"
+ cp = os.path.join(script_path, subpath)
+ return cp
+
+
+def get_thumbnailer_filepath():
+ script_path = os.path.dirname(os.path.realpath(__file__))
+ # fpath = os.path.join(p, subpath)
+ subpath = "blendfiles" + os.sep + "thumbnailer.blend"
+ return os.path.join(script_path, subpath)
+
+
+def get_material_thumbnailer_filepath():
+ script_path = os.path.dirname(os.path.realpath(__file__))
+ # fpath = os.path.join(p, subpath)
+ subpath = "blendfiles" + os.sep + "material_thumbnailer_cycles.blend"
+ return os.path.join(script_path, subpath)
+ """
+ for p in bpy.utils.script_paths():
+ testfname= os.path.join(p, subpath)#p + '%saddons%sobject_fracture%sdata.blend' % (s,s,s)
+ if os.path.isfile( testfname):
+ fname=testfname
+ return(fname)
+ return None
+ """
+
+
+def get_addon_file(subpath=''):
+ script_path = os.path.dirname(os.path.realpath(__file__))
+ # fpath = os.path.join(p, subpath)
+ return os.path.join(script_path, subpath)
+
+
+def get_addon_thumbnail_path(name):
+ script_path = os.path.dirname(os.path.realpath(__file__))
+ # fpath = os.path.join(p, subpath)
+ ext = name.split('.')[-1]
+ next = ''
+ if not (ext == 'jpg' or ext == 'png'): # allready has ext?
+ next = '.jpg'
+ subpath = "thumbnails" + os.sep + name + next
+ return os.path.join(script_path, subpath)
diff --git a/blenderkit/ratings.py b/blenderkit/ratings.py
new file mode 100644
index 00000000..6928b87a
--- /dev/null
+++ b/blenderkit/ratings.py
@@ -0,0 +1,207 @@
+# ##### 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 #####
+
+if "bpy" in locals():
+ import imp
+
+ imp.reload(paths)
+
+else:
+ from blenderkit import paths, utils
+
+import bpy
+import requests, threading
+
+from bpy.props import (
+ IntProperty,
+ FloatProperty,
+ StringProperty,
+ EnumProperty,
+ BoolProperty,
+ PointerProperty,
+)
+from bpy.types import (
+ Operator,
+ Panel,
+)
+
+
+def pretty_print_POST(req):
+ """
+ At this point it is completely built and ready
+ to be fired; it is "prepared".
+
+ However pay attention at the formatting used in
+ this function because it is programmed to be pretty
+ printed and may differ from the actual request.
+ """
+ print('{}\n{}\n{}\n\n{}'.format(
+ '-----------START-----------',
+ req.method + ' ' + req.url,
+ '\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()),
+ req.body,
+ ))
+
+
+def uplaod_rating_thread(url, ratings, headers):
+ for rating_name, score in ratings:
+ if (score != -1 and score != 0):
+ rating_url = url + rating_name + '/'
+ data = {
+ "score": score, # todo this kind of mixing is too much. Should have 2 bkit structures, upload, use
+ }
+
+ try:
+ r = requests.put(rating_url, data=data, verify=True, headers=headers)
+
+ except requests.exceptions.RequestException as e:
+ print('ratings upload failed: %s' % str(e))
+
+
+def uplaod_review_thread(url, reviews, headers):
+ r = requests.put(url, data=reviews, verify=True, headers=headers)
+
+ # except requests.exceptions.RequestException as e:
+ # print('reviews upload failed: %s' % str(e))
+
+
+def upload_rating(asset):
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ api_key = user_preferences.api_key
+ headers = {"accept": "application/json", "Authorization": "Bearer %s" % api_key}
+
+ asset_data = asset['asset_data']
+
+ bkit_ratings = asset.bkit_ratings
+ print('rating asset', asset_data['name'], asset_data['asset_base_id'])
+ url = paths.get_bkit_url() + 'assets/' + asset['asset_data']['id'] + '/rating/'
+
+ ratings = [
+ ]
+
+ if bkit_ratings.rating_quality > 0.1:
+ ratings.append(('quality', bkit_ratings.rating_quality))
+ if bkit_ratings.rating_work_hours > 0.1:
+ ratings.append(('working_hours', round(bkit_ratings.rating_work_hours, 1)))
+
+ thread = threading.Thread(target=uplaod_rating_thread, args=(url, ratings, headers))
+ thread.start()
+
+ url = paths.get_bkit_url() + 'assets/' + asset['asset_data']['id'] + '/review'
+
+ reviews = {
+ 'reviewText': bkit_ratings.rating_compliments,
+ 'reviewTextProblems': bkit_ratings.rating_problems,
+ }
+ if not (bkit_ratings.rating_compliments == '' and bkit_ratings.rating_compliments == ''):
+ thread = threading.Thread(target=uplaod_review_thread, args=(url, reviews, headers))
+ thread.start()
+
+ # the info that the user rated an item is stored in the scene
+ s = bpy.context.scene
+ s['assets rated'] = s.get('assets rated', {})
+ if bkit_ratings.rating_quality > 0.1 and bkit_ratings.rating_work_hours > 0.1:
+ s['assets rated'][asset['asset_data']['asset_base_id']] = True
+
+
+class StarRatingOperator(bpy.types.Operator):
+ """Tooltip"""
+ bl_idname = "object.blenderkit_rating"
+ bl_label = "Rate the Asset"
+
+ property_name: StringProperty(
+ name="Rating Property",
+ description="Property that is rated",
+ default="",
+ )
+
+ rating: IntProperty(name="Rating", description="rating value", default=1, min=1, max=10)
+
+ def execute(self, context):
+ asset = utils.get_active_asset()
+ props = asset.bkit_ratings
+ props[self.property_name] = self.rating
+ return {'FINISHED'}
+
+
+asset_types = (
+ ('MODEL', 'Model', 'set of objects'),
+ ('SCENE', 'Scene', 'scene'),
+ ('MATERIAL', 'Material', 'any .blend Material'),
+ ('TEXTURE', 'Texture', 'a texture, or texture set'),
+ ('BRUSH', 'Brush', 'brush, can be any type of blender brush'),
+ ('ADDON', 'Addon', 'addnon'),
+)
+
+
+class UploadRatingOperator(bpy.types.Operator):
+ """Upload rating to the web db"""
+ bl_idname = "object.blenderkit_rating_upload"
+ bl_label = "Upload the Rating"
+
+ # type of upload - model, material, textures, e.t.c.
+ asset_type: EnumProperty(
+ name="Type",
+ items=asset_types,
+ description="Type of asset",
+ default="MODEL",
+ )
+
+ # @classmethod
+ # def poll(cls, context):
+ # return bpy.context.active_object != None and bpy.context.active_object.get('asset_id') is not None
+ def draw(self, context):
+ layout = self.layout
+ layout.label(text='Rating sent to server. Thanks for rating!')
+
+ def execute(self, context):
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ wm = context.window_manager
+ asset = utils.get_active_asset()
+ upload_rating(asset)
+ return wm.invoke_props_dialog(self)
+
+
+def draw_rating(layout, props, prop_name, name):
+ # layout.label(name)
+
+ row = layout.row(align=True)
+
+ for a in range(0, 10):
+ if eval('props.' + prop_name) < a + 1:
+ icon = 'SOLO_OFF'
+ else:
+ icon = 'SOLO_ON'
+
+ op = row.operator('object.blenderkit_rating', icon=icon, emboss=False, text='')
+ op.property_name = prop_name
+ op.rating = a + 1
+
+
+def register_ratings():
+ pass;
+ bpy.utils.register_class(StarRatingOperator)
+ bpy.utils.register_class(UploadRatingOperator)
+
+
+def unregister_ratings():
+ pass;
+ bpy.utils.unregister_class(StarRatingOperator)
+ bpy.utils.unregister_class(UploadRatingOperator)
diff --git a/blenderkit/registration.py b/blenderkit/registration.py
new file mode 100644
index 00000000..abcc3caf
--- /dev/null
+++ b/blenderkit/registration.py
@@ -0,0 +1,101 @@
+# ##### 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 #####
+
+
+import json
+import webbrowser
+from http.server import BaseHTTPRequestHandler, HTTPServer
+from urllib.parse import parse_qs, urlparse
+
+import requests
+
+SERVER_URL = "http://localhost:8001"
+# SERVER_URL = "https://devel.blenderkit.com"
+CLIENT_ID = "IdFRwa3SGA8eMpzhRVFMg5Ts8sPK93xBjif93x0F"
+PORTS = [62485, 1234]
+
+
+class SimpleOAuthAuthenticator(object):
+ def __init__(self, server_url, client_id, ports):
+ self.server_url = server_url
+ self.client_id = client_id
+ self.ports = ports
+
+ def _get_tokens(self, authorization_code=None, refresh_token=None, grant_type="authorization_code"):
+ data = {
+ "grant_type": grant_type,
+ "state": "random_state_string",
+ "client_id": self.client_id,
+ "scopes": "read write",
+ }
+ if authorization_code:
+ data['code'] = authorization_code
+ if refresh_token:
+ data['refresh_token'] = refresh_token
+ print(data)
+
+ response = requests.post(
+ '%s/o/token/' % self.server_url,
+ data=data
+ )
+ print(response.content)
+ if response.status_code != 200:
+ return None, None
+ refresh_token = json.loads(response.content)['refresh_token']
+ access_token = json.loads(response.content)['access_token']
+ return access_token, refresh_token
+
+ def get_new_token(self):
+ class HTTPServerHandler(BaseHTTPRequestHandler):
+ def do_GET(self):
+ self.send_response(200)
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+ if 'code' in self.path:
+ self.auth_code = self.path.split('=')[1]
+ # Display to the user that they no longer need the browser window
+ self.wfile.write(bytes('<html><h1>You may now close this window.</h1></html>', 'utf-8'))
+ qs = parse_qs(urlparse(self.path).query)
+ self.server.authorization_code = qs['code'][0]
+
+ for port in self.ports:
+ print(port)
+ try:
+ httpServer = HTTPServer(('localhost', port), HTTPServerHandler)
+ except OSError:
+ continue
+ break
+ print(port)
+ webbrowser.open_new(
+ "%s/o/authorize?client_id=%s&state=random_state_string&response_type=code&"
+ "redirect_uri=http://localhost:%s/consumer/exchange/" % (self.server_url, self.client_id, port),
+ )
+
+ httpServer.handle_request()
+ authorization_code = httpServer.authorization_code
+ return self._get_tokens(authorization_code=authorization_code)
+
+ def get_refreshed_token(self, refresh_token):
+ return self._get_tokens(refresh_token=refresh_token, grant_type="refresh_token")
+
+
+authenticator = SimpleOAuthAuthenticator(server_url=SERVER_URL, client_id=CLIENT_ID, ports=PORTS)
+auth_token, refresh_token = authenticator.get_new_token()
+print("auth token: %s, refresh_token: %s" % (auth_token, refresh_token))
+auth_token, refresh_token = authenticator.get_refreshed_token(refresh_token)
+print("auth_token: %s, new refresh_token: %s" % (auth_token, refresh_token))
diff --git a/blenderkit/search.py b/blenderkit/search.py
new file mode 100644
index 00000000..6a202f09
--- /dev/null
+++ b/blenderkit/search.py
@@ -0,0 +1,998 @@
+# ##### 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 #####
+
+if "bpy" in locals():
+ import imp
+
+ imp.reload(paths)
+ imp.reload(utils)
+else:
+ from blenderkit import paths, utils, categories, ui, version_checker
+import blenderkit
+from bpy.app.handlers import persistent
+
+from bpy.props import ( # TODO only keep the ones actually used when cleaning
+ IntProperty,
+ FloatProperty,
+ FloatVectorProperty,
+ StringProperty,
+ EnumProperty,
+ BoolProperty,
+ PointerProperty,
+)
+from bpy.types import (
+ Operator,
+ Panel,
+ AddonPreferences,
+ PropertyGroup,
+ UIList
+)
+
+import requests, os, random
+import time
+import threading
+import tempfile
+import json
+import bpy
+
+search_start_time = 0
+prev_time = 0
+
+
+def check_errors(rdata):
+ if rdata.get('statusCode') == 401:
+ if rdata.get('detail') == 'Invalid token.':
+ return False, 'Missing or wrong api_key in addon preferences'
+ return True, ''
+
+
+search_threads = []
+thumb_sml_download_threads = {}
+thumb_full_download_threads = {}
+reports = ''
+
+
+@persistent
+def scene_load(context):
+ wm = bpy.context.window_manager
+ fetch_server_data()
+ # following doesn't necessarilly happen if version isn't checked yet or similar, first run.
+ wm['bkit_update'] = version_checker.compare_versions(blenderkit)
+ utils.load_categories()
+
+
+def fetch_server_data():
+ ''' download categories and addon version'''
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ url = paths.BLENDERKIT_ADDON_URL
+ api_key = user_preferences.api_key
+ version_checker.check_version_thread(url, api_key, blenderkit)
+ categories.fetch_categories_thread(api_key)
+
+
+@bpy.app.handlers.persistent
+def timer_update(): # TODO might get moved to handle all blenderkit stuff.
+
+ global search_threads
+ # don't do anything while dragging - this could switch asset type during drag, and make results list lenght different,
+ # causing a lot of throuble literally.
+ if len(search_threads) == 0 or bpy.context.scene.blenderkitUI.dragging:
+ return 1
+ for thread in search_threads: # TODO this doesn't check all processess when removal... mostly 1 process will be running however.
+
+ if not thread[0].is_alive():
+ search_threads.remove(thread) #
+ icons_dir = thread[1]
+ scene = bpy.context.scene
+ # these 2 lines should update the previews enum and set the first result as active.
+ s = bpy.context.scene
+ asset_type = thread[2]
+ if asset_type == 'model':
+ props = scene.blenderkit_models
+ json_filepath = os.path.join(icons_dir, 'model_searchresult.json')
+ search_name = 'bkit model search'
+ if asset_type == 'scene':
+ props = scene.blenderkit_scene
+ json_filepath = os.path.join(icons_dir, 'scene_searchresult.json')
+ search_name = 'bkit scene search'
+ if asset_type == 'material':
+ props = scene.blenderkit_mat
+ json_filepath = os.path.join(icons_dir, 'material_searchresult.json')
+ search_name = 'bkit material search'
+ if asset_type == 'brush':
+ props = scene.blenderkit_brush
+ json_filepath = os.path.join(icons_dir, 'brush_searchresult.json')
+ search_name = 'bkit brush search'
+
+ s[search_name] = []
+
+ global reports
+ if reports != '':
+ props.report = str(reports)
+ return .2
+ with open(json_filepath, 'r') as data_file:
+ rdata = json.load(data_file)
+
+ result_field = []
+ ok, error = check_errors(rdata)
+ if ok:
+
+ for r in rdata['results']:
+
+ if r['assetType'] == asset_type:
+ # print(r)
+ if len(r['files']) > 0:
+ furl = None
+ tname = None
+ allthumbs = []
+ durl, tname = None, None
+ for f in r['files']:
+ if f['fileType'] == 'thumbnail':
+ tname = paths.extract_filename_from_url(f['fileThumbnailLarge'])
+ small_tname = paths.extract_filename_from_url(f['fileThumbnail'])
+ allthumbs.append(tname) # TODO just first thumb is used now.
+
+ tdict = {}
+ for i, t in enumerate(allthumbs):
+ tdict['thumbnail_%i'] = t
+ if f['fileType'] == 'blend':
+ durl = f['downloadUrl'].split('?')[0]
+ # fname = paths.extract_filename_from_url(f['filePath'])
+ if durl and tname:
+
+ tooltip = generate_tooltip(r)
+ # utils.pprint(print(r))
+ asset_data = {'thumbnail': tname,
+ 'thumbnail_small': small_tname,
+ # 'thumbnails':allthumbs,
+ 'download_url': durl,
+ 'id': r['id'],
+ 'asset_base_id': r['assetBaseId'],
+ 'name': r['name'],
+ 'asset_type': r['assetType'],
+ 'tooltip': tooltip,
+ 'tags': r['tags'],
+ 'can_download': r.get('canDownload', True),
+ 'verification_status': r['verificationStatus']
+ # 'description': r['description'],
+ # 'author': r['description'],
+ }
+ asset_data['downloaded'] = 0
+
+ # parse extra params needed for blender here
+ params = params_to_dict(r['parameters'])
+
+ if asset_type == 'model':
+ if params.get('boundBoxMinX') != None:
+ bbox = {
+ 'bbox_min': (
+ float(params['boundBoxMinX']),
+ float(params['boundBoxMinY']),
+ float(params['boundBoxMinZ'])),
+ 'bbox_max': (
+ float(params['boundBoxMaxX']),
+ float(params['boundBoxMaxY']),
+ float(params['boundBoxMaxZ']))
+ }
+
+ else:
+ bbox = {
+ 'bbox_min': (-.5, -.5, 0),
+ 'bbox_max': (.5, .5, 1)
+ }
+ asset_data.update(bbox)
+ if asset_type == 'material':
+ asset_data['texture_size_meters'] = params.get('textureSizeMeters', 1.0)
+
+ asset_data.update(tdict)
+ if r['assetBaseId'] in scene.get('assets used', {}).keys():
+ asset_data['downloaded'] = 100
+
+ result_field.append(asset_data)
+
+ # results = rdata['results']
+ s[search_name] = result_field
+ s['search results'] = result_field
+ s['search results orig'] = rdata
+ load_previews()
+ ui_props = bpy.context.scene.blenderkitUI
+ if len(result_field) < ui_props.scrolloffset:
+ ui_props.scrolloffset = 0
+ props.is_searching = False
+ props.search_error = False
+ props.report = 'Open assetbar to see %i results. ' % len(s['search results'])
+ if len(s['search results']) == 0:
+ props.report = 'No matching results found.'
+
+ # (rdata['next'])
+ # if rdata['next'] != None:
+ # search(False, get_next = True)
+ else:
+ print('error', error)
+ props.report = error
+ props.search_error = True
+
+ print('finished search thread')
+ mt('preview loading finished')
+ return .2
+
+
+def load_previews():
+ mappingdict = {
+ 'MODEL': 'model',
+ 'SCENE': 'scene',
+ 'MATERIAL': 'material',
+ 'TEXTURE': 'texture',
+ 'BRUSH': 'brush'
+ }
+ scene = bpy.context.scene
+ # FIRST START SEARCH
+ props = scene.blenderkitUI
+
+ directory = paths.get_temp_dir('%s_search' % mappingdict[props.asset_type])
+ s = bpy.context.scene
+ results = s.get('search results')
+ #
+ if results is not None:
+ inames = []
+ tpaths = []
+
+ i = 0
+ for r in results:
+
+ tpath = os.path.join(directory, r['thumbnail_small'])
+
+ iname = utils.previmg_name(i)
+
+ if os.path.exists(tpath): # sometimes we are unlucky...
+ img = bpy.data.images.get(iname)
+ if img is None:
+ img = bpy.data.images.load(tpath)
+ img.name = iname
+ elif img.filepath != tpath:
+ # had to add this check for autopacking files...
+ if img.packed_file is not None:
+ img.unpack(method='USE_ORIGINAL')
+ img.filepath = tpath
+ img.reload()
+
+ i += 1
+ print('previews loaded')
+
+
+# line splitting for longer texts...
+def split_subs(text):
+ if text == '':
+ return []
+ threshold = 40
+ text = text.rstrip()
+ lines = []
+ while len(text) > threshold:
+ i = text.rfind(' ', 0, threshold)
+ i1 = text.rfind(',', 0, threshold)
+ i = max(i, i1)
+ if i == -1:
+ i = threshold
+ lines.append(text[:i])
+ text = text[i:]
+ lines.append(text)
+ return lines
+
+
+def list_to_str(input):
+ output = ''
+ for i, text in enumerate(input):
+ output += text
+ if i < len(input) - 1:
+ output += ', '
+ return output
+
+
+def writeblock(t, input): # for longer texts
+ dlines = split_subs(input)
+ for i, l in enumerate(dlines):
+ t += '%s\n' % l
+ return t
+
+
+def writeblockm(tooltip, mdata, key='', pretext=None): # for longer texts
+ if mdata.get(key) == None:
+ return tooltip
+ else:
+ intext = mdata[key]
+ if type(intext) == list:
+ intext = list_to_str(intext)
+ intext = str(intext)
+ if intext.rstrip() == '':
+ return tooltip
+ if pretext == None:
+ pretext = key
+ if pretext != '':
+ pretext = pretext + ': '
+ text = pretext + intext
+ dlines = split_subs(text)
+ for i, l in enumerate(dlines):
+ tooltip += '%s\n' % l
+
+ return tooltip
+
+
+def fmt_length(prop):
+ prop = str(round(prop, 2)) + 'm'
+ return prop
+
+
+def has(mdata, prop):
+ if mdata.get(prop) is not None and mdata[prop] is not None and mdata[prop] is not False:
+ return True
+ else:
+ return False
+
+
+def params_to_dict(params):
+ params_dict = {}
+ for p in params:
+ params_dict[p['parameterType']] = p['value']
+ return params_dict
+
+
+def generate_tooltip(mdata):
+ if type(mdata['parameters']) == list:
+ mparams = params_to_dict(mdata['parameters'])
+ else:
+ mparams = mdata['parameters']
+ t = ''
+ t = writeblock(t, mdata['name'])
+
+ t = writeblockm(t, mdata, key='description', pretext='')
+ if mdata['description'] != '':
+ t += '\n'
+
+ bools = (('rig', None), ('animated', None), ('manifold', 'non-manifold'), ('scene', None), ('simulation', None),
+ ('uv', None))
+ for b in bools:
+ if mparams.get(b[0]):
+ mdata['tags'].append(b[0])
+ elif b[1] != None:
+ mdata['tags'].append(b[1])
+
+ bools_data = ('adult',)
+ for b in bools_data:
+ if mdata.get(b) and mdata[b]:
+ mdata['tags'].append(b)
+ t = writeblockm(t, mparams, key='designer', pretext='designer')
+ t = writeblockm(t, mparams, key='manufacturer', pretext='manufacturer')
+ t = writeblockm(t, mparams, key='designCollection', pretext='design collection')
+
+ # t = writeblockm(t, mparams, key='engines', pretext='engine')
+ # t = writeblockm(t, mparams, key='model_style', pretext='style')
+ # t = writeblockm(t, mparams, key='material_style', pretext='style')
+ # t = writeblockm(t, mdata, key='tags')
+ # t = writeblockm(t, mparams, key='condition', pretext='condition')
+ # t = writeblockm(t, mparams, key='productionLevel', pretext='production level')
+ if has(mdata, 'purePbr'):
+ t = writeblockm(t, mparams, key='pbrType', pretext='pbr')
+
+ t = writeblockm(t, mparams, key='designYear', pretext='design year')
+
+ if has(mparams, 'dimensionX'):
+ t += 'size: %s, %s, %s\n' % (fmt_length(mparams['dimensionX']),
+ fmt_length(mparams['dimensionY']),
+ fmt_length(mparams['dimensionZ']))
+ if has(mparams, 'faceCount'):
+ t += 'face count: %s, render: %s\n' % (mparams['faceCount'], mparams['faceCountRender'])
+
+ # t = writeblockm(t, mparams, key='meshPolyType', pretext='mesh type')
+ # t = writeblockm(t, mparams, key='objectCount', pretext='nubmber of objects')
+
+ # t = writeblockm(t, mparams, key='materials')
+ # t = writeblockm(t, mparams, key='modifiers')
+ # t = writeblockm(t, mparams, key='shaders')
+
+ if has(mparams, 'textureSizeMeters'):
+ t = writeblockm(t, mparams, key='textureSizeMeters', pretext='texture size in meters')
+
+ if has(mparams, 'textureResolutionMax') and mparams['textureResolutionMax'] > 0:
+ if mparams['textureResolutionMin'] == mparams['textureResolutionMax']:
+ t = writeblockm(t, mparams, key='textureResolutionMin', pretext='texture resolution')
+ else:
+ t += 'tex resolution: %i - %i\n' % (mparams['textureResolutionMin'], mparams['textureResolutionMax'])
+
+ if has(mparams, 'thumbnailScale'):
+ t = writeblockm(t, mparams, key='thumbnailScale', pretext='preview scale')
+
+ # t += 'uv: %s\n' % mdata['uv']
+ t += '\n'
+ # t = writeblockm(t, mdata, key='license')
+
+ # generator is for both upload preview and search, this is only after search
+ if mdata.get('versionNumber'):
+ # t = writeblockm(t, mdata, key='versionNumber', pretext='version')
+
+ t += 'author: %s %s\n' % (mdata['author']['firstName'], mdata['author']['lastName'])
+ # t += '\n'
+
+ at = mdata['assetType']
+ t += '\n'
+ if at == 'brush' or at == 'texture':
+ t += 'click to link %s' % mdata['assetType']
+ if at == 'model' or at == 'material':
+ t += 'click or drag in scene to link/append %s' % mdata['assetType']
+
+ return t
+
+
+def get_items_models(self, context):
+ global search_items_models
+ return search_items_models
+
+
+def get_items_brushes(self, context):
+ global search_items_brushes
+ return search_items_brushes
+
+
+def get_items_materials(self, context):
+ global search_items_materials
+ return search_items_materials
+
+
+def get_items_textures(self, context):
+ global search_items_textures
+ return search_items_textures
+
+
+class ThumbDownloader(threading.Thread):
+ query = None
+
+ def __init__(self, url, path):
+ super(ThumbDownloader, self).__init__()
+ self.url = url
+ self.path = path
+ self._stop_event = threading.Event()
+
+ def stop(self):
+ self._stop_event.set()
+
+ def stopped(self):
+ return self._stop_event.is_set()
+
+ def run(self):
+ r = requests.get(self.url, stream=False)
+ if r.status_code == 200:
+ with open(self.path, 'wb') as f:
+ f.write(r.content)
+ # ORIGINALLY WE DOWNLOADED THUMBNAILS AS STREAM, BUT THIS WAS TOO SLOW.
+ # with open(path, 'wb') as f:
+ # for chunk in r.iter_content(1048576*4):
+ # f.write(chunk)
+
+
+class Searcher(threading.Thread):
+ query = None
+
+ def __init__(self, query, params):
+ super(Searcher, self).__init__()
+ self.query = query
+ self.params = params
+ self._stop_event = threading.Event()
+
+ def stop(self):
+ self._stop_event.set()
+
+ def stopped(self):
+ return self._stop_event.is_set()
+
+ def run(self):
+ maxthreads = 300
+ maximages = 50
+ query = self.query
+ params = self.params
+ global reports
+
+ t = time.time()
+ mt('search thread started')
+ tempdir = paths.get_temp_dir('%s_search' % query['asset_type'])
+ json_filepath = os.path.join(tempdir, '%s_searchresult.json' % query['asset_type'])
+
+ if query['token'] != '':
+ headers = {
+ "accept": "application/json",
+ "Authorization": "Bearer %s" % query['token'],
+ # "Content-Type": "application/json",
+ }
+ else:
+ headers = {
+ "accept": "application/json",
+ # "Content-Type": "application/json",
+ }
+ rdata = {}
+ rdata['results'] = []
+
+ if params['get_next']:
+ with open(json_filepath, 'r') as infile:
+ try:
+ origdata = json.load(infile)
+ urlquery = origdata['next']
+ if urlquery == None:
+ return;
+ except:
+ # in case no search results found on drive we don't do next page loading.
+ params['get_next'] = False
+ if not params['get_next']:
+ # build a new request
+ url = paths.get_bkit_url() + 'search/'
+
+ nquery = {
+ # 'tags': query['keywords'],
+ 'asset_type': query['asset_type'],
+ }
+ if query.get('category'):
+ nquery['category_subtree'] = query['category']
+
+ # build request manually
+ # TODO use real queries
+ requeststring = '?query=' + query['keywords'].lower() + '+'
+ #
+ for i, q in enumerate(nquery):
+ requeststring += q + ':' + str(nquery[q])
+ if i < len(nquery) - 1:
+ requeststring += '+'
+
+ requeststring += '&addon_version=%s' % params['addon_version']
+ if params.get('scene_uuid') is not None:
+ requeststring += '&scene_uuid=%s' % params['scene_uuid']
+
+ urlquery = url + requeststring
+
+ try:
+ # print(urlquery)
+ r = requests.get(urlquery, headers=headers)
+ reports = ''
+ # print(r.text)
+ except requests.exceptions.RequestException as e:
+ print(e)
+ reports = e
+ # props.report = e
+ return
+ mt('response is back ')
+ try:
+ rdata = r.json()
+ except Exception as inst:
+ reports = r.text
+ print(inst)
+
+ mt('data parsed ')
+
+ # filter results here:
+ # todo remove this in future
+ nresults = []
+ for d in rdata.get('results', []):
+ # TODO this code is for filtering brush types, should vanish after we implement filter in Elastic
+ mode = None
+ if query['asset_type'] == 'brush':
+ for p in d['parameters']:
+ if p['parameterType'] == 'mode':
+ mode = p['value']
+ if query['asset_type'] != 'brush' or (
+ query.get('brushType') != None and query['brushType']) == mode:
+ nresults.append(d)
+ rdata['results'] = nresults
+
+ print('number of results: ', len(rdata.get('results', [])))
+ if self.stopped():
+ print('stopping search : ' + query['keywords'])
+ return
+
+ mt('search finished')
+ i = 0
+
+ thumb_small_urls = []
+ thumb_small_filepaths = []
+ thumb_full_urls = []
+ thumb_full_filepaths = []
+ # END OF PARSING
+ for d in rdata.get('results', []):
+ for f in d['files']:
+ # TODO move validation of published assets to server, too manmy checks here.
+ if f['fileType'] == 'thumbnail' and f['fileThumbnail'] != None and f['fileThumbnailLarge'] != None:
+ if f['fileThumbnail'] == None:
+ f['fileThumbnail'] = 'NONE'
+ if f['fileThumbnailLarge'] == None:
+ f['fileThumbnailLarge'] = 'NONE'
+
+ thumb_small_urls.append(f['fileThumbnail'])
+ thumb_full_urls.append(f['fileThumbnailLarge'])
+
+ imgname = paths.extract_filename_from_url(f['fileThumbnail'])
+ imgpath = os.path.join(tempdir, imgname)
+ thumb_small_filepaths.append(imgpath)
+
+ imgname = paths.extract_filename_from_url(f['fileThumbnailLarge'])
+ imgpath = os.path.join(tempdir, imgname)
+ thumb_full_filepaths.append(imgpath)
+
+ sml_thbs = zip(thumb_small_filepaths, thumb_small_urls)
+ full_thbs = zip(thumb_full_filepaths, thumb_full_urls)
+
+ # we save here because a missing thumbnail check is in the previous loop
+ # we can also prepend previous results. These have allready thumbnails downloaded...
+ if params['get_next']:
+ rdata['results'][0:0] = origdata['results']
+
+ with open(json_filepath, 'w') as outfile:
+ json.dump(rdata, outfile)
+
+ killthreads_sml = []
+ for k in thumb_sml_download_threads.keys():
+ if k not in thumb_small_filepaths:
+ killthreads_sml.append(k) # do actual killing here?
+
+ killthreads_full = []
+ for k in thumb_full_download_threads.keys():
+ if k not in thumb_full_filepaths:
+ killthreads_full.append(k) # do actual killing here?
+ # TODO do the killing/ stopping here! remember threads might have finished inbetween!
+
+ if self.stopped():
+ print('stopping search : ' + query['keywords'])
+ return
+
+ # this loop handles downloading of small thumbnails
+ for imgpath, url in sml_thbs:
+ if imgpath not in thumb_sml_download_threads and not os.path.exists(imgpath):
+ thread = ThumbDownloader(url, imgpath)
+ # thread = threading.Thread(target=download_thumbnail, args=([url, imgpath]),
+ # daemon=True)
+ thread.start()
+ thumb_sml_download_threads[imgpath] = thread
+ # threads.append(thread)
+
+ if len(thumb_sml_download_threads) > maxthreads:
+ while len(thumb_sml_download_threads) > maxthreads:
+ threads_copy = thumb_sml_download_threads.copy() # because for loop can erase some of the items.
+ for tk, thread in threads_copy.items():
+ if not thread.is_alive():
+ thread.join()
+ # print(x)
+ del (thumb_sml_download_threads[tk])
+ print('fetched thumbnail ', i)
+ i += 1
+ if self.stopped():
+ print('stopping search : ' + query['keywords'])
+ return
+ idx = 0
+ while len(thumb_sml_download_threads) > 0:
+ threads_copy = thumb_sml_download_threads.copy() # because for loop can erase some of the items.
+ for tk, thread in threads_copy.items():
+ if not thread.is_alive():
+ thread.join()
+ del (thumb_sml_download_threads[tk])
+ i += 1
+
+ if self.stopped():
+ print('stopping search : ' + query['keywords'])
+ return
+
+ # start downloading full thumbs in the end
+ for imgpath, url in full_thbs:
+ if imgpath not in thumb_full_download_threads and not os.path.exists(imgpath):
+ thread = ThumbDownloader(url, imgpath)
+ # thread = threading.Thread(target=download_thumbnail, args=([url, imgpath]),
+ # daemon=True)
+ thread.start()
+ thumb_full_download_threads[imgpath] = thread
+ mt('thumbnails finished')
+
+
+def build_query_common(query, props):
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ query_common = {
+ "token": user_preferences.api_key,
+ "keywords": props.search_keywords
+ }
+ query.update(query_common)
+
+
+# def query_add_range(query, name, rmin, rmax):
+
+def build_query_model():
+ '''use all search input to request results from server'''
+
+ props = bpy.context.scene.blenderkit_models
+ query = {
+ "asset_type": 'model',
+ "engine": props.search_engine,
+ "adult": props.search_adult,
+ }
+ if props.search_style != 'ANY':
+ if props.search_style != 'OTHER':
+ query["style"] = props.search_style
+ else:
+ query["style"] = props.search_style_other
+ if props.search_advanced:
+ if props.search_condition != 'UNSPECIFIED':
+ query["condition"] = props.search_condition
+ if props.search_design_year:
+ query["designYearMin"] = props.search_design_year_min
+ query["designYearMax"] = props.search_design_year_max
+ if props.search_polycount:
+ query["polyCountMin"] = props.search_polycount_min
+ query["polyCountMax"] = props.search_polycount_max
+ if props.search_texture_resolution:
+ query["textureResolutionMin"] = props.search_texture_resolution_min
+ query["textureResolutionMax"] = props.search_texture_resolution_max
+
+ build_query_common(query, props)
+ # query = {
+ # "query": {
+ # "exists": {"field": "faceCount"},
+ #
+ # "range": {
+ #
+ # "faceCount": {
+ # "gte": query["polyCountMin"],
+ # "lte": query["polyCountMax"],
+ # "boost": 2.0
+ # }
+ #
+ # },
+ #
+ # "match": {
+ # "asset_type": 'model',
+ # }
+ #
+ # }
+ # }
+
+ return query
+
+
+def build_query_scene():
+ '''use all search input to request results from server'''
+
+ props = bpy.context.scene.blenderkit_scene
+ query = {
+ "asset_type": 'scene',
+ "engine": props.search_engine,
+ # "adult": props.search_adult,
+ }
+ build_query_common(query, props)
+ return query
+
+
+def build_query_material():
+ props = bpy.context.scene.blenderkit_mat
+ query = {
+ "asset_type": 'material',
+
+ }
+ if props.search_engine == 'NONE':
+ query["engine"] = ''
+ if props.search_engine != 'OTHER':
+ query["engine"] = props.search_engine
+ else:
+ query["engine"] = props.search_engine_other
+ if props.search_style != 'ANY':
+ if props.search_style != 'OTHER':
+ query["style"] = props.search_style
+ else:
+ query["style"] = props.search_style_other
+ build_query_common(query, props)
+
+ return query
+
+
+def build_query_texture():
+ props = bpy.context.scene.blenderkit_tex
+ query = {
+ "asset_type": 'texture',
+
+ }
+
+ if props.search_style != 'ANY':
+ if props.search_style != 'OTHER':
+ query["search_style"] = props.search_style
+ else:
+ query["search_style"] = props.search_style_other
+
+ build_query_common(query, props)
+
+ return query
+
+
+def build_query_brush():
+ props = bpy.context.scene.blenderkit_brush
+
+ brush_type = ''
+ if bpy.context.sculpt_object is not None:
+ brush_type = 'sculpt'
+
+ elif bpy.context.image_paint_object: # could be just else, but for future p
+ brush_type = 'texture_paint'
+
+ query = {
+ "asset_type": 'brush',
+
+ "brushType": brush_type
+ }
+
+ build_query_common(query, props)
+
+ return query
+
+
+def mt(text):
+ global search_start_time, prev_time
+ alltime = time.time() - search_start_time
+ since_last = time.time() - prev_time
+ prev_time = time.time()
+ print(text, alltime, since_last)
+
+
+def add_search_process(query, params):
+ global search_threads
+
+ while (len(search_threads) > 0):
+ old_thread = search_threads.pop(0)
+ old_thread[0].stop()
+ # TODO CARE HERE FOR ALSO KILLING THE THREADS...AT LEAST NOW SEARCH DONE FIRST WON'T REWRITE AN OLDER ONE
+
+ tempdir = paths.get_temp_dir('%s_search' % query['asset_type'])
+ thread = Searcher(query, params)
+ # thread = threading.Thread(target=Searcher, args=([query]), daemon=True)
+ thread.start()
+
+ search_threads.append([thread, tempdir, query['asset_type']])
+
+ mt('thread started')
+
+
+def search(own=False, category='', get_next=False, free_only=False):
+ ''' initialize searching'''
+ global search_start_time
+
+ search_start_time = time.time()
+ mt('start')
+ scene = bpy.context.scene
+ uiprops = scene.blenderkitUI
+
+ if uiprops.asset_type == 'MODEL':
+ if not hasattr(scene, 'blenderkit'):
+ return;
+ props = scene.blenderkit_models
+ query = build_query_model()
+
+ if uiprops.asset_type == 'SCENE':
+ if not hasattr(scene, 'blenderkit_scene'):
+ return;
+ props = scene.blenderkit_scene
+ query = build_query_scene()
+
+ if uiprops.asset_type == 'MATERIAL':
+ if not hasattr(scene, 'blenderkit_mat'):
+ return;
+ props = scene.blenderkit_mat
+ query = build_query_material()
+
+ if uiprops.asset_type == 'TEXTURE':
+ if not hasattr(scene, 'blenderkit_tex'):
+ return;
+ # props = scene.blenderkit_tex
+ # query = build_query_texture()
+
+ if uiprops.asset_type == 'BRUSH':
+ if not hasattr(scene, 'blenderkit_brush'):
+ return;
+ props = scene.blenderkit_brush
+ query = build_query_brush()
+
+ if props.is_searching and get_next == True:
+ return;
+ query['own'] = own
+ if category != '':
+ query['category'] = category
+
+ print('searching')
+ props.is_searching = True
+
+ params = {
+ 'scene_uuid': bpy.context.scene.get('uuid', None),
+ 'addon_version': version_checker.get_addon_version(),
+ 'get_next': get_next
+ }
+
+ if free_only:
+ query['keywords'] += '+is_free:true'
+
+ add_search_process(query, params)
+ props.report = 'BlenderKit searching....'
+
+
+def search_update(self, context):
+ if self.search_keywords != '':
+ search()
+
+
+class SearchOperator(Operator):
+ """Tooltip"""
+ bl_idname = "view3d.blenderkit_search"
+ bl_label = "BlenderKit asset search"
+
+ own: BoolProperty(name="own assets only",
+ description="Find all own assets",
+ default=False)
+ category: StringProperty(
+ name="category",
+ description="search only subtree of this category",
+ default="")
+
+ get_next: BoolProperty(name="next page",
+ description="get next page from previous search",
+ default=False)
+
+ @classmethod
+ def poll(cls, context):
+ return True
+
+ def execute(self, context):
+ search(own=self.own, category=self.category, get_next=self.get_next)
+ bpy.ops.view3d.blenderkit_asset_bar()
+
+ return {'FINISHED'}
+
+
+classes = [
+ SearchOperator
+]
+
+
+def register_search():
+ bpy.app.handlers.load_post.append(scene_load)
+
+ for c in classes:
+ bpy.utils.register_class(c)
+
+ # bpy.app.timers.register(timer_update, persistent = True)
+
+ utils.load_categories()
+
+
+def unregister_search():
+ bpy.app.handlers.load_post.remove(scene_load)
+
+ for c in classes:
+ bpy.utils.unregister_class(c)
+
+ # bpy.app.timers.unregister(timer_update)
+
+
+'''
+search -
+build query
+START THREAD
+send query (bg allready)
+get result - metadata, small thumbnails, big thumbnails paths (now genereate this?)
+write metadata, possibly to
+download small thumbnails first
+start big thumbnails download. these don't have to be there on updates, if they aren't the Image in image editor doesn't get updated.
+parse metadata, save it in json in the temp dir which gets read on each update of the search.
+END THREAD
+when download is triggered, get also this metadata from json. E
+pass this single - item metadata in the download functions, threads.
+'''
diff --git a/blenderkit/thumbnails/arrow_left.png b/blenderkit/thumbnails/arrow_left.png
new file mode 100644
index 00000000..97565169
--- /dev/null
+++ b/blenderkit/thumbnails/arrow_left.png
Binary files differ
diff --git a/blenderkit/thumbnails/arrow_right.png b/blenderkit/thumbnails/arrow_right.png
new file mode 100644
index 00000000..fd16550b
--- /dev/null
+++ b/blenderkit/thumbnails/arrow_right.png
Binary files differ
diff --git a/blenderkit/thumbnails/bar_slider.png b/blenderkit/thumbnails/bar_slider.png
new file mode 100644
index 00000000..ec627318
--- /dev/null
+++ b/blenderkit/thumbnails/bar_slider.png
Binary files differ
diff --git a/blenderkit/thumbnails/locked.png b/blenderkit/thumbnails/locked.png
new file mode 100644
index 00000000..f308392c
--- /dev/null
+++ b/blenderkit/thumbnails/locked.png
Binary files differ
diff --git a/blenderkit/thumbnails/locked_large.png b/blenderkit/thumbnails/locked_large.png
new file mode 100644
index 00000000..eb2deb65
--- /dev/null
+++ b/blenderkit/thumbnails/locked_large.png
Binary files differ
diff --git a/blenderkit/thumbnails/rating_ui.png b/blenderkit/thumbnails/rating_ui.png
new file mode 100644
index 00000000..8d5bbdb5
--- /dev/null
+++ b/blenderkit/thumbnails/rating_ui.png
Binary files differ
diff --git a/blenderkit/thumbnails/star_white.png b/blenderkit/thumbnails/star_white.png
new file mode 100644
index 00000000..14e030cc
--- /dev/null
+++ b/blenderkit/thumbnails/star_white.png
Binary files differ
diff --git a/blenderkit/thumbnails/thumbnail_notready.jpg b/blenderkit/thumbnails/thumbnail_notready.jpg
new file mode 100644
index 00000000..2628d1e5
--- /dev/null
+++ b/blenderkit/thumbnails/thumbnail_notready.jpg
Binary files differ
diff --git a/blenderkit/thumbnails/vs_deleted.png b/blenderkit/thumbnails/vs_deleted.png
new file mode 100644
index 00000000..a7f4e134
--- /dev/null
+++ b/blenderkit/thumbnails/vs_deleted.png
Binary files differ
diff --git a/blenderkit/thumbnails/vs_on_hold.png b/blenderkit/thumbnails/vs_on_hold.png
new file mode 100644
index 00000000..eb797517
--- /dev/null
+++ b/blenderkit/thumbnails/vs_on_hold.png
Binary files differ
diff --git a/blenderkit/thumbnails/vs_ready.png b/blenderkit/thumbnails/vs_ready.png
new file mode 100644
index 00000000..ac52a3cd
--- /dev/null
+++ b/blenderkit/thumbnails/vs_ready.png
Binary files differ
diff --git a/blenderkit/thumbnails/vs_uploaded.png b/blenderkit/thumbnails/vs_uploaded.png
new file mode 100644
index 00000000..6ef39cb4
--- /dev/null
+++ b/blenderkit/thumbnails/vs_uploaded.png
Binary files differ
diff --git a/blenderkit/ui.py b/blenderkit/ui.py
new file mode 100644
index 00000000..40e9b1d8
--- /dev/null
+++ b/blenderkit/ui.py
@@ -0,0 +1,1424 @@
+# ##### 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 #####
+
+if "bpy" in locals():
+ import imp
+
+ imp.reload(paths)
+ imp.reload(ratings)
+ imp.reload(utils)
+ imp.reload(search)
+ imp.reload(upload)
+else:
+ from blenderkit import paths, ratings, utils, search, upload, ui_bgl, download, bg_blender
+
+import bpy
+
+import math, random
+
+from bpy.props import (
+ BoolProperty,
+ StringProperty
+)
+
+from bpy_extras import view3d_utils
+import mathutils
+from mathutils import Vector
+import time
+import os
+
+mappingdict = {
+ 'MODEL': 'model',
+ 'SCENE': 'scene',
+ 'MATERIAL': 'material',
+ 'TEXTURE': 'texture',
+ 'BRUSH': 'brush'
+}
+
+verification_icons = {
+ 'ready': 'vs_ready.png',
+ 'deleted': 'vs_deleted.png',
+ 'uploaded': 'vs_uploaded.png',
+ 'uploading': None,
+ 'on_hold': 'vs_on_hold.png',
+ 'validated': None
+
+}
+
+
+# class UI_region():
+# def _init__(self, parent = None, x = 10,y = 10 , width = 10, height = 10, img = None, col = None):
+
+def get_approximate_text_width(st):
+ size = 10
+ for s in st:
+ if s in 'i|':
+ size += 2
+ elif s in ' ':
+ size += 4
+ elif s in 'sfrt':
+ size += 5
+ elif s in 'ceghkou':
+ size += 6
+ elif s in 'PadnBCST3E':
+ size += 7
+ elif s in 'GMODVXYZ':
+ size += 8
+ elif s in 'w':
+ size += 9
+ elif s in 'm':
+ size += 10
+ else:
+ size += 7
+ return size # Convert to picas
+
+
+def get_asset_under_mouse(mousex, mousey):
+ s = bpy.context.scene
+ ui_props = bpy.context.scene.blenderkitUI
+ r = bpy.context.region
+
+ search_results = s.get('search results')
+ if search_results is not None:
+
+ h_draw = min(ui_props.hcount, math.ceil(len(search_results) / ui_props.wcount))
+ for b in range(0, h_draw):
+ w_draw = min(ui_props.wcount, len(search_results) - b * ui_props.wcount - ui_props.scrolloffset)
+ for a in range(0, w_draw):
+ x = ui_props.bar_x + a * (ui_props.margin + ui_props.thumb_size) + ui_props.margin + ui_props.drawoffset
+ y = ui_props.bar_y - ui_props.margin - (ui_props.thumb_size + ui_props.margin) * (b + 1)
+ w = ui_props.thumb_size
+ h = ui_props.thumb_size
+
+ if x < mousex < x + w and y < mousey < y + h:
+ return a + ui_props.wcount * b + ui_props.scrolloffset
+
+ # return search_results[a]
+
+ return -3
+
+
+def draw_bbox(location, rotation, bbox_min, bbox_max, progress=None, color=(0, 1, 0, 1)):
+ ui_props = bpy.context.scene.blenderkitUI
+
+ rotation = mathutils.Euler(rotation)
+
+ smin = Vector(bbox_min)
+ smax = Vector(bbox_max)
+ v0 = Vector(smin)
+ v1 = Vector((smax.x, smin.y, smin.z))
+ v2 = Vector((smax.x, smax.y, smin.z))
+ v3 = Vector((smin.x, smax.y, smin.z))
+ v4 = Vector((smin.x, smin.y, smax.z))
+ v5 = Vector((smax.x, smin.y, smax.z))
+ v6 = Vector((smax.x, smax.y, smax.z))
+ v7 = Vector((smin.x, smax.y, smax.z))
+
+ arrowx = smin.x + (smax.x - smin.x) / 2
+ arrowy = smin.y - (smax.x - smin.x) / 2
+ v8 = Vector((arrowx, arrowy, smin.z))
+
+ vertices = [v0, v1, v2, v3, v4, v5, v6, v7, v8]
+ for v in vertices:
+ v.rotate(rotation)
+ v += Vector(location)
+
+ lines = [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5],
+ [2, 6], [3, 7], [0, 8], [1, 8]]
+ ui_bgl.draw_lines(vertices, lines, color)
+ if progress != None:
+ color = (color[0], color[1], color[2], .2)
+ progress = progress * .01
+ vz0 = (v4 - v0) * progress + v0
+ vz1 = (v5 - v1) * progress + v1
+ vz2 = (v6 - v2) * progress + v2
+ vz3 = (v7 - v3) * progress + v3
+ rects = (
+ (v0, v1, vz1, vz0),
+ (v1, v2, vz2, vz1),
+ (v2, v3, vz3, vz2),
+ (v3, v0, vz0, vz3))
+ for r in rects:
+ ui_bgl.draw_rect_3d(r, color)
+
+
+def get_rating_scalevalues(asset_type):
+ xs = []
+ if asset_type == 'model':
+ scalevalues = (0.5, 1, 2, 5, 10, 25, 50, 100, 250)
+ for v in scalevalues:
+ a = math.log2(v)
+ x = (a + 1) * (1. / 9.)
+ xs.append(x)
+ else:
+ scalevalues = (0.2, 1, 2, 3, 4, 5)
+ for v in scalevalues:
+ a = v
+ x = v / 5.
+ xs.append(x)
+ return scalevalues, xs
+
+
+def draw_ratings_bgl():
+ # return;
+ ui = bpy.context.scene.blenderkitUI
+
+ rating_possible, rated, asset, asset_data = is_rating_possible()
+
+ if rating_possible: # (not rated or ui_props.rating_menu_on):
+ bkit_ratings = asset.bkit_ratings
+ bgcol = bpy.context.preferences.themes[0].user_interface.wcol_tooltip.inner
+ textcol = (1, 1, 1, 1)
+
+ r = bpy.context.region
+ font_size = int(ui.rating_ui_scale * 20)
+
+ if ui.rating_button_on:
+ img = utils.get_thumbnail('star_white.png')
+
+ ui_bgl.draw_image(ui.rating_x,
+ ui.rating_y - ui.rating_button_width,
+ ui.rating_button_width,
+ ui.rating_button_width,
+ img, 1)
+
+ # if ui_props.asset_type != 'BRUSH':
+ # thumbnail_image = props.thumbnail
+ # else:
+ # b = utils.get_active_brush()
+ # thumbnail_image = b.icon_filepath
+
+ directory = paths.get_temp_dir('%s_search' % asset_data['asset_type'])
+ tpath = os.path.join(directory, asset_data['thumbnail_small'])
+
+ img = utils.get_hidden_image(tpath, 'rating_preview')
+ ui_bgl.draw_image(ui.rating_x + ui.rating_button_width,
+ ui.rating_y - ui.rating_button_width,
+ ui.rating_button_width,
+ ui.rating_button_width,
+ img, 1)
+ # ui_bgl.draw_text( 'rate asset %s' % asset_data['name'],r.width - rating_button_width + margin, margin, font_size)
+ return
+
+ ui_bgl.draw_rect(ui.rating_x,
+ ui.rating_y - ui.rating_ui_height - 2 * ui.margin - font_size,
+ ui.rating_ui_width + ui.margin,
+ ui.rating_ui_height + 2 * ui.margin + font_size,
+ bgcol)
+ img = utils.get_thumbnail('rating_ui.png')
+ ui_bgl.draw_image(ui.rating_x,
+ ui.rating_y - ui.rating_ui_height - 2 * ui.margin,
+ ui.rating_ui_width,
+ ui.rating_ui_height,
+ img, 1)
+ img = utils.get_thumbnail('star_white.png')
+
+ quality = bkit_ratings.rating_quality
+ work_hours = bkit_ratings.rating_work_hours
+
+ for a in range(0, quality):
+ ui_bgl.draw_image(ui.rating_x + ui.quality_stars_x + a * ui.star_size,
+ ui.rating_y - ui.rating_ui_height + ui.quality_stars_y,
+ ui.star_size,
+ ui.star_size,
+ img, 1)
+
+ img = utils.get_thumbnail('bar_slider.png')
+ # for a in range(0,11):
+ if work_hours > 0.2:
+ if asset_data['asset_type'] == 'model':
+ complexity = math.log2(work_hours) + 2 # real complexity
+ complexity = (1. / 9.) * (complexity - 1) * ui.workhours_bar_x_max
+ else:
+ complexity = work_hours / 5 * ui.workhours_bar_x_max
+ ui_bgl.draw_image(
+ ui.rating_x + ui.workhours_bar_x + int(
+ complexity),
+ ui.rating_y - ui.rating_ui_height + ui.workhours_bar_y,
+ ui.workhours_bar_slider_size,
+ ui.workhours_bar_slider_size, img, 1)
+ ui_bgl.draw_text(
+ str(round(work_hours, 1)),
+ ui.rating_x + ui.workhours_bar_x - 50,
+ ui.rating_y - ui.rating_ui_height + ui.workhours_bar_y + 10, font_size)
+ # (0.5,1,2,4,8,16,32,64,128,256)
+ # ratings have to be different for models and brushes+materials.
+
+ scalevalues, xs = get_rating_scalevalues(asset_data['asset_type'])
+ for v, x in zip(scalevalues, xs):
+ ui_bgl.draw_rect(ui.rating_x + ui.workhours_bar_x + int(
+ x * ui.workhours_bar_x_max) - 1 + ui.workhours_bar_slider_size / 2,
+ ui.rating_y - ui.rating_ui_height + ui.workhours_bar_y,
+ 2,
+ 5,
+ textcol)
+ ui_bgl.draw_text(str(v),
+ ui.rating_x + ui.workhours_bar_x + int(
+ x * ui.workhours_bar_x_max),
+ ui.rating_y - ui.rating_ui_height + ui.workhours_bar_y - 30,
+ font_size)
+ if work_hours > 0.2 and quality > 0.2:
+ text = 'Thanks for rating asset %s' % asset_data['name']
+ else:
+ text = 'Rate asset %s.' % asset_data['name']
+ ui_bgl.draw_text(text,
+ ui.rating_x,
+ ui.rating_y - ui.margin - font_size,
+ font_size)
+
+
+def draw_tooltip(x, y, text, img):
+ region = bpy.context.region
+ scale = bpy.context.preferences.view.ui_scale
+ t = time.time()
+
+ ttipmargin = 10
+
+ font_height = int(12 * scale)
+ line_height = int(15 * scale)
+ nameline_height = int(23 * scale)
+
+ lines = text.split('\n')
+ ncolumns = 2
+ nlines = math.ceil((len(lines) - 1) / ncolumns)
+ texth = line_height * nlines + nameline_height
+
+ isizex = int(512 * scale * img.size[0] / max(img.size[0], img.size[1]))
+ isizey = int(512 * scale * img.size[1] / max(img.size[0], img.size[1]))
+
+ estimated_height = texth + 3 * ttipmargin + isizey
+
+ if estimated_height > y:
+ scaledown = y / (estimated_height)
+ scale *= scaledown
+ # we need to scale these down to have correct size if the tooltip wouldn't fit.
+ font_height = int(12 * scale)
+ line_height = int(15 * scale)
+ nameline_height = int(23 * scale)
+
+ lines = text.split('\n')
+ ncolumns = 2
+ nlines = math.ceil((len(lines) - 1) / ncolumns)
+ texth = line_height * nlines + nameline_height
+
+ isizex = int(512 * scale * img.size[0] / max(img.size[0], img.size[1]))
+ isizey = int(512 * scale * img.size[1] / max(img.size[0], img.size[1]))
+
+ name_height = int(18 * scale)
+
+ x += 2 * ttipmargin
+ y -= 2 * ttipmargin
+
+ width = isizex + 2 * ttipmargin
+ x = min(x + width, region.width) - width
+
+ bgcol = bpy.context.preferences.themes[0].user_interface.wcol_tooltip.inner
+ textcol = bpy.context.preferences.themes[0].user_interface.wcol_tooltip.text
+ textcol = (textcol[0], textcol[1], textcol[2], 1)
+ textcol1 = (textcol[0] * .8, textcol[1] * .8, textcol[2] * .8, 1)
+ white = (1, 1, 1, .1)
+
+ ui_bgl.draw_rect(x - ttipmargin,
+ y - texth - 2 * ttipmargin - isizey,
+ isizex + ttipmargin * 2,
+ texth + 3 * ttipmargin + isizey,
+ bgcol)
+
+ i = 0
+ column_break = -1 # start minus one for the name
+ xtext = x
+ fsize = name_height
+ tcol = textcol
+ for l in lines:
+ if column_break >= nlines:
+ xtext += int(isizex / ncolumns)
+ column_break = 0
+ ytext = y - column_break * line_height - nameline_height - ttipmargin
+ if i == 0:
+ ytext = y - name_height + 5
+ elif i == len(lines) - 1:
+ ytext = y - (nlines - 1) * line_height - nameline_height - ttipmargin
+ tcol = textcol
+ tsize = font_height
+ else:
+ if l[:5] == 'tags:':
+ tcol = textcol1
+ fsize = font_height
+ i += 1
+ column_break += 1
+ ui_bgl.draw_text(l, xtext, ytext, fsize, tcol)
+ t = time.time()
+ ui_bgl.draw_image(x, y - texth - isizey - ttipmargin, isizex, isizey, img, 1)
+
+
+def draw_callback_2d(self, context):
+ a = context.area
+ try:
+ # self.area might throw error just by itself.
+ a1 = self.area
+ go = True
+ except:
+ # bpy.types.SpaceView3D.draw_handler_remove(self._handle_2d, 'WINDOW')
+ # bpy.types.SpaceView3D.draw_handler_remove(self._handle_3d, 'WINDOW')
+ go = False
+ if go and a == a1:
+ props = context.scene.blenderkitUI
+ if props.down_up == 'SEARCH':
+ draw_ratings_bgl()
+ draw_callback_2d_search(self, context)
+ elif props.down_up == 'UPLOAD':
+ draw_callback_2d_upload_preview(self, context)
+
+
+def draw_downloader(x, y, percent=0, img=None):
+ if img is not None:
+ ui_bgl.draw_image(x, y, 50, 50, img, .5)
+ ui_bgl.draw_rect(x, y, 50, int(0.5 * percent), (.2, 1, .2, .3))
+ ui_bgl.draw_rect(x - 3, y - 3, 6, 6, (1, 0, 0, .3))
+
+
+def draw_progress(x, y, text='', percent=None, color=(.2, 1, .2, .3)):
+ ui_bgl.draw_rect(x, y, percent, 5, color)
+ ui_bgl.draw_text(text, x, y + 8, 10, color)
+
+
+def draw_callback_3d_progress(self, context):
+ # 'star trek' mode gets here, blocked by now ;)
+ for threaddata in download.download_threads:
+ asset_data = threaddata[1]
+ tcom = threaddata[2]
+ if tcom.passargs.get('downloaders'):
+ for d in tcom.passargs['downloaders']:
+ if asset_data['asset_type'] == 'model':
+ draw_bbox(d['location'], d['rotation'], asset_data['bbox_min'], asset_data['bbox_max'],
+ progress=tcom.progress)
+
+
+def draw_callback_2d_progress(self, context):
+ green = (.2, 1, .2, .3)
+ offset = 0
+ row_height = 35
+
+ ui = bpy.context.scene.blenderkitUI
+
+ x = ui.reports_x
+ y = ui.reports_y
+ index = 0
+ for threaddata in download.download_threads:
+ asset_data = threaddata[1]
+ tcom = threaddata[2]
+ if tcom.passargs.get('downloaders'):
+ for d in tcom.passargs['downloaders']:
+ directory = paths.get_temp_dir('%s_search' % asset_data['asset_type'])
+ tpath = os.path.join(directory, asset_data['thumbnail_small'])
+ img = utils.get_hidden_image(tpath, 'rating_preview')
+ loc = view3d_utils.location_3d_to_region_2d(bpy.context.region, bpy.context.space_data.region_3d,
+ d['location'])
+ if asset_data['asset_type'] == 'model':
+ # models now draw with star trek mode, no need to draw percent for the image.
+ draw_downloader(loc[0], loc[1], percent=tcom.progress, img=img)
+ else:
+ draw_downloader(loc[0], loc[1], percent=tcom.progress, img=img)
+
+
+ else:
+ draw_progress(x, y - index * 30, text='downloading %s' % asset_data['name'],
+ percent=tcom.progress)
+ index += 1
+ for process in bg_blender.bg_processes:
+ tcom = process[1]
+ draw_progress(x, y - index * 30, '%s' % tcom.lasttext,
+ tcom.progress)
+ index += 1
+
+
+def draw_callback_2d_upload_preview(self, context):
+ ui_props = context.scene.blenderkitUI
+
+ props = utils.get_upload_props()
+ if props != None and ui_props.draw_tooltip:
+ if ui_props.asset_type != 'BRUSH':
+ ui_props.thumbnail_image = props.thumbnail
+ else:
+ b = utils.get_active_brush()
+ ui_props.thumbnail_image = b.icon_filepath
+
+ img = utils.get_hidden_image(ui_props.thumbnail_image, 'upload_preview')
+
+ draw_tooltip(ui_props.bar_x, ui_props.bar_y, ui_props.tooltip, img)
+
+
+def draw_callback_2d_search(self, context):
+ s = bpy.context.scene
+ ui_props = context.scene.blenderkitUI
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+
+ r = self.region
+ # hc = bpy.context.preferences.themes[0].view_3d.space.header
+ # hc = bpy.context.preferences.themes[0].user_interface.wcol_menu_back.inner
+ # hc = (hc[0], hc[1], hc[2], .2)
+ hc = (1, 1, 1, .07)
+ # grey1 = (hc.r * .55, hc.g * .55, hc.b * .55, 1)
+ grey2 = (hc[0] * .8, hc[1] * .8, hc[2] * .8, .5)
+ # grey1 = (hc.r, hc.g, hc.b, 1)
+ white = (1, 1, 1, 0.2)
+ green = (.2, 1, .2, .7)
+ highlight = bpy.context.preferences.themes[0].user_interface.wcol_menu_item.inner_sel
+ highlight = (1, 1, 1, .2)
+ # highlight = (1, 1, 1, 0.8)
+ # background of asset bar
+ if not ui_props.dragging:
+ search_results = s.get('search results')
+ if search_results == None:
+ return
+ h_draw = min(ui_props.hcount, math.ceil(len(search_results) / ui_props.wcount))
+
+ if ui_props.wcount > len(search_results):
+ bar_width = len(search_results) * (ui_props.thumb_size + ui_props.margin) + ui_props.margin
+ else:
+ bar_width = ui_props.bar_width
+ row_height = ui_props.thumb_size + ui_props.margin
+ ui_bgl.draw_rect(ui_props.bar_x, ui_props.bar_y - ui_props.bar_height, bar_width,
+ ui_props.bar_height, hc)
+
+ if search_results is not None:
+ if ui_props.scrolloffset > 0 or ui_props.wcount * ui_props.hcount < len(search_results):
+ ui_props.drawoffset = 35
+ else:
+ ui_props.drawoffset = 0
+
+ if ui_props.wcount * ui_props.hcount < len(search_results):
+ # arrows
+ arrow_y = ui_props.bar_y - int((ui_props.bar_height + ui_props.thumb_size) / 2) + ui_props.margin
+ if ui_props.scrolloffset > 0:
+
+ if ui_props.active_index == -2:
+ ui_bgl.draw_rect(ui_props.bar_x, ui_props.bar_y - ui_props.bar_height, 25,
+ ui_props.bar_height, highlight)
+ img = utils.get_thumbnail('arrow_left.png')
+ ui_bgl.draw_image(ui_props.bar_x, arrow_y, 25,
+ ui_props.thumb_size,
+ img,
+ 1)
+ if len(search_results) - ui_props.scrolloffset > (ui_props.wcount * ui_props.hcount):
+ if ui_props.active_index == -1:
+ ui_bgl.draw_rect(ui_props.bar_x + ui_props.bar_width - 25,
+ ui_props.bar_y - ui_props.bar_height, 25,
+ ui_props.bar_height,
+ highlight)
+ img1 = utils.get_thumbnail('arrow_right.png')
+ ui_bgl.draw_image(ui_props.bar_x + ui_props.bar_width - 25,
+ arrow_y, 25,
+ ui_props.thumb_size, img1, 1)
+
+ for b in range(0, h_draw):
+ w_draw = min(ui_props.wcount, len(search_results) - b * ui_props.wcount - ui_props.scrolloffset)
+ y = ui_props.bar_y - (b + 1) * (row_height)
+ for a in range(0, w_draw):
+ x = ui_props.bar_x + a * (
+ ui_props.margin + ui_props.thumb_size) + ui_props.margin + ui_props.drawoffset
+
+ #
+ index = a + ui_props.scrolloffset + b * ui_props.wcount
+ iname = utils.previmg_name(index)
+ img = bpy.data.images.get(iname)
+
+ w = int(ui_props.thumb_size * img.size[0] / max(img.size[0], img.size[1]))
+ h = int(ui_props.thumb_size * img.size[1] / max(img.size[0], img.size[1]))
+ crop = (0, 0, 1, 1)
+ if img.size[0] > img.size[1]:
+ offset = (1 - img.size[1] / img.size[0]) / 2
+ crop = (offset, 0, 1 - offset, 1)
+ if img is not None:
+ ui_bgl.draw_image(x, y, w, w, img, 1,
+ crop=crop)
+ if index == ui_props.active_index:
+ ui_bgl.draw_rect(x - ui_props.highlight_margin, y - ui_props.highlight_margin,
+ w + 2 * ui_props.highlight_margin, w + 2 * ui_props.highlight_margin,
+ highlight)
+ # if index == ui_props.active_index:
+ # ui_bgl.draw_rect(x - highlight_margin, y - highlight_margin,
+ # w + 2*highlight_margin, h + 2*highlight_margin , highlight)
+
+ else:
+ ui_bgl.draw_rect(x, y, w, h, white)
+
+ result = search_results[index]
+ if result['downloaded'] > 0:
+ ui_bgl.draw_rect(x, y - 2, int(w * result['downloaded'] / 100.0), 2, green)
+ # object type icons - just a test..., adds clutter/ not so userfull:
+ # icons = ('type_finished.png', 'type_template.png', 'type_particle_system.png')
+
+ if not result.get('can_download', True) == True or user_preferences.api_key == '':
+ img = utils.get_thumbnail('locked.png')
+ ui_bgl.draw_image(x + 2, y + 2, 24, 24, img, 1)
+
+ v_icon = verification_icons[result.get('verification_status', 'validated')]
+ if v_icon is not None:
+ img = utils.get_thumbnail(v_icon)
+ ui_bgl.draw_image(x + ui_props.thumb_size - 26, y + 2, 24, 24, img, 1)
+
+ if user_preferences.api_key == '':
+ report = 'Please register on BlenderKit website to use the free content.'
+ ui_bgl.draw_text(report, ui_props.bar_x + ui_props.margin,
+ ui_props.bar_y - 25 - ui_props.margin - ui_props.bar_height, 15)
+ elif len(search_results) == 0:
+ report = 'BlenderKit - No matching results found.'
+ ui_bgl.draw_text(report, ui_props.bar_x + ui_props.margin,
+ ui_props.bar_y - 25 - ui_props.margin, 15)
+ s = bpy.context.scene
+ props = utils.get_search_props()
+ if props.report != '' and props.is_searching or props.search_error:
+ ui_bgl.draw_text(props.report, ui_props.bar_x,
+ ui_props.bar_y - 15 - ui_props.margin - ui_props.bar_height, 15)
+
+ props = s.blenderkitUI
+ if props.draw_tooltip:
+ # TODO move this lazy loading into a function and don't duplicate through the code
+ iname = utils.previmg_name(ui_props.active_index, fullsize=True)
+
+ directory = paths.get_temp_dir('%s_search' % mappingdict[props.asset_type])
+ sr = s.get('search results')
+ if sr != None and ui_props.active_index != -3:
+ r = sr[ui_props.active_index]
+ tpath = os.path.join(directory, r['thumbnail'])
+
+ img = bpy.data.images.get(iname)
+ if img == None or img.filepath != tpath:
+ if os.path.exists(tpath): # sometimes we are unlucky...
+
+ if img is None:
+ img = bpy.data.images.load(tpath)
+ img.name = iname
+ else:
+ if img.filepath != tpath:
+ # todo replace imgs reloads with a method that forces unpack for thumbs.
+ if img.packed_file is not None:
+ img.unpack(method='USE_ORIGINAL')
+ img.filepath = tpath
+ img.reload()
+ img.name = iname
+ else:
+ iname = utils.previmg_name(ui_props.active_index)
+ img = bpy.data.images.get(iname)
+
+ draw_tooltip(ui_props.mouse_x, ui_props.mouse_y, ui_props.tooltip, img)
+
+ if ui_props.dragging and (
+ ui_props.draw_drag_image or ui_props.draw_snapped_bounds) and ui_props.active_index > -1:
+ iname = utils.previmg_name(ui_props.active_index)
+ img = bpy.data.images.get(iname)
+ linelength = 35
+ ui_bgl.draw_image(ui_props.mouse_x + linelength, ui_props.mouse_y - linelength - ui_props.thumb_size,
+ ui_props.thumb_size, ui_props.thumb_size, img, 1)
+ ui_bgl.draw_line2d(ui_props.mouse_x, ui_props.mouse_y, ui_props.mouse_x + linelength,
+ ui_props.mouse_y - linelength, 2, white)
+
+
+def draw_callback_3d(self, context):
+ ''' Draw snapped bbox while dragging and in the future other blenderkit related stuff. '''
+ ui = context.scene.blenderkitUI
+
+ if ui.dragging and ui.asset_type == 'MODEL':
+ if ui.draw_snapped_bounds:
+ draw_bbox(ui.snapped_location, ui.snapped_rotation, ui.snapped_bbox_min, ui.snapped_bbox_max)
+
+
+def mouse_raycast(context, mx, my):
+ r = context.region
+ rv3d = context.region_data
+ coord = mx, my
+
+ # get the ray from the viewport and mouse
+ view_vector = view3d_utils.region_2d_to_vector_3d(r, rv3d, coord)
+ ray_origin = view3d_utils.region_2d_to_origin_3d(r, rv3d, coord)
+ ray_target = ray_origin + (view_vector * 1000000000)
+
+ vec = ray_target - ray_origin
+
+ has_hit, snapped_location, snapped_normal, face_index, object, matrix = bpy.context.scene.ray_cast(
+ bpy.context.view_layer, ray_origin, vec)
+
+ # rote = mathutils.Euler((0, 0, math.pi))
+ randoffset = math.pi
+ if has_hit:
+ snapped_rotation = snapped_normal.to_track_quat('Z', 'Y').to_euler()
+ up = Vector((0, 0, 1))
+ props = bpy.context.scene.blenderkit_models
+ if props.randomize_rotation and snapped_normal.angle(up) < math.radians(10.0):
+ randoffset = props.offset_rotation_amount + math.pi + (
+ random.random() - 0.5) * props.randomize_rotation_amount
+ else:
+ randoffset = props.offset_rotation_amount # we don't rotate this way on walls and ceilings. + math.pi
+ # snapped_rotation.z += math.pi + (random.random() - 0.5) * .2
+
+ else:
+ snapped_rotation = mathutils.Quaternion((0, 0, 0, 0)).to_euler()
+
+ snapped_rotation.rotate_axis('Z', randoffset)
+
+ return has_hit, snapped_location, snapped_normal, snapped_rotation, face_index, object, matrix
+
+
+def floor_raycast(context, mx, my):
+ r = context.region
+ rv3d = context.region_data
+ coord = mx, my
+
+ # get the ray from the viewport and mouse
+ view_vector = view3d_utils.region_2d_to_vector_3d(r, rv3d, coord)
+ ray_origin = view3d_utils.region_2d_to_origin_3d(r, rv3d, coord)
+ ray_target = ray_origin + (view_vector * 1000)
+
+ # various intersection plane normals are needed for corner cases that might actually happen quite often - in front and side view.
+ # default plane normal is scene floor.
+ plane_normal = (0, 0, 1)
+ if math.isclose(view_vector.x, 0, abs_tol=1e-4) and math.isclose(view_vector.z, 0, abs_tol=1e-4):
+ plane_normal = (0, 1, 0)
+ elif math.isclose(view_vector.z, 0, abs_tol=1e-4):
+ plane_normal = (1, 0, 0)
+
+ snapped_location = mathutils.geometry.intersect_line_plane(ray_origin, ray_target, (0, 0, 0), plane_normal,
+ False)
+ if snapped_location != None:
+ has_hit = True
+ snapped_normal = Vector((0, 0, 1))
+ face_index = None
+ object = None
+ matrix = None
+ snapped_rotation = snapped_normal.to_track_quat('Z', 'Y').to_euler()
+ props = bpy.context.scene.blenderkit_models
+ if props.randomize_rotation:
+ randoffset = props.offset_rotation_amount + math.pi + (
+ random.random() - 0.5) * props.randomize_rotation_amount
+ else:
+ randoffset = props.offset_rotation_amount + math.pi
+ snapped_rotation.rotate_axis('Z', randoffset)
+
+ return has_hit, snapped_location, snapped_normal, snapped_rotation, face_index, object, matrix
+
+
+def is_rating_possible():
+ ao = bpy.context.active_object
+ ui = bpy.context.scene.blenderkitUI
+ if bpy.context.scene.get('assets rated') is not None and ui.down_up == 'SEARCH':
+ if bpy.context.mode in ('SCULPT', 'PAINT_TEXTURE'):
+ b = utils.get_active_brush()
+ ad = b.get('asset_data')
+ if ad is not None:
+ rated = bpy.context.scene['assets rated'].get(ad['asset_base_id'])
+ return True, rated, b, ad
+ if ao is not None:
+ # TODO ADD BRUSHES HERE
+ ad = ao.get('asset_data')
+ if ad is not None:
+ rated = bpy.context.scene['assets rated'].get(ad['asset_base_id'])
+ # originally hidden for allready rated assets
+ return True, rated, ao, ad
+
+ # check also materials
+ m = ao.active_material
+ if m is not None:
+ ad = m.get('asset_data')
+ if ad is not None:
+ rated = bpy.context.scene['assets rated'].get(ad['asset_base_id'])
+ return True, rated, m, ad
+
+ # if t>2 and t<2.5:
+ # ui_props.rating_on = False
+
+ return False, False, None, None
+
+
+def interact_rating(r, mx, my, event):
+ ui = bpy.context.scene.blenderkitUI
+ rating_possible, rated, asset, asset_data = is_rating_possible()
+
+ if rating_possible:
+ bkit_ratings = asset.bkit_ratings
+
+ t = time.time() - ui.last_rating_time
+ # if t>2:
+ # if rated:
+ # ui_props.rating_button_on = True
+ # ui_props.rating_menu_on = False
+ if ui.rating_button_on and event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
+ if mouse_in_area(mx, my,
+ ui.rating_x,
+ ui.rating_y - ui.rating_button_width,
+ ui.rating_button_width * 2,
+ ui.rating_button_width):
+ ui.rating_menu_on = True
+ ui.rating_button_on = False
+ return True
+ if ui.rating_menu_on:
+ if mouse_in_area(mx, my,
+ ui.rating_x,
+ ui.rating_y - ui.rating_ui_height,
+ ui.rating_ui_width,
+ ui.rating_ui_height + 25):
+ rmx = mx - (ui.rating_x)
+ rmy = my - (ui.rating_y - ui.rating_ui_height)
+
+ # quality
+ upload_rating = False
+ if (ui.quality_stars_x < rmx and rmx < ui.quality_stars_x + 10 * ui.star_size and \
+ ui.quality_stars_y < rmy and rmy < ui.quality_stars_y + ui.star_size and event.type == 'LEFTMOUSE' and event.value == 'PRESS') or \
+ ui.dragging_rating_quality:
+
+ if event.type == 'LEFTMOUSE':
+ if event.value == 'PRESS':
+ ui.dragging_rating = True
+ ui.dragging_rating_quality = True
+ elif event.value == 'RELEASE':
+ ui.dragging_rating = False
+ ui.dragging_rating_quality = False
+
+ if ui.dragging_rating_quality:
+ q = math.ceil((rmx - ui.quality_stars_x) / (float(ui.star_size)))
+ bkit_ratings.rating_quality = q
+
+ # work hours
+ if (
+ ui.workhours_bar_x < rmx and rmx < ui.workhours_bar_x + ui.workhours_bar_x_max + ui.workhours_bar_slider_size and \
+ ui.workhours_bar_y < rmy and rmy < ui.workhours_bar_y + ui.workhours_bar_slider_size and event.type == 'LEFTMOUSE' and event.value == 'PRESS') \
+ or (ui.dragging_rating_work_hours):
+ if event.value == 'PRESS':
+ ui.dragging_rating = True
+ ui.dragging_rating_work_hours = True
+ elif event.value == 'RELEASE':
+ ui.dragging_rating = False
+ ui.dragging_rating_work_hours = False
+ if ui.dragging_rating_work_hours:
+ xv = rmx - ui.workhours_bar_x - ui.workhours_bar_slider_size / 2
+ ratio = xv / ui.workhours_bar_x_max
+ if asset_data['asset_type'] == 'model':
+ wh_log2 = ratio * 9 - 1
+ wh = 2 ** wh_log2
+ else:
+ wh = 5 * ratio
+ bkit_ratings.rating_work_hours = wh
+
+ if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
+ if bkit_ratings.rating_quality > 0.1 or bkit_ratings.rating_work_hours > 0.1:
+ ratings.upload_rating(asset)
+ ui.last_rating_time = time.time()
+ return True
+ else:
+ ui.rating_button_on = True
+ ui.rating_menu_on = False
+ return False
+
+
+def mouse_in_area(mx, my, x, y, w, h):
+ if x < mx < x + w and y < my < y + h:
+ return True
+ else:
+ return False
+
+
+def mouse_in_asset_bar(mx, my):
+ ui_props = bpy.context.scene.blenderkitUI
+ if ui_props.bar_y - ui_props.bar_height < my < ui_props.bar_y \
+ and mx > ui_props.bar_x and mx < ui_props.bar_x + ui_props.bar_width:
+ return True
+ else:
+ return False
+
+
+def mouse_in_region(r, mx, my):
+ if 0 < my < r.height and 0 < mx < r.width:
+ return True
+ else:
+ return False
+
+
+def update_ui_size(area, region):
+ ui = bpy.context.scene.blenderkitUI
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ ui_scale = bpy.context.preferences.view.ui_scale
+
+ ui.margin = ui.bl_rna.properties['margin'].default * ui_scale
+ ui.thumb_size = ui.bl_rna.properties['thumb_size'].default * ui_scale
+
+ reg_multiplier = 1
+ if not bpy.context.preferences.system.use_region_overlap:
+ reg_multiplier = 0
+
+ for r in area.regions:
+ if r.type == 'TOOLS':
+ ui.bar_x = r.width * reg_multiplier + ui.margin + ui.bar_x_offset * ui_scale
+ elif r.type == 'UI':
+ ui.bar_end = r.width * reg_multiplier + 100 * ui_scale
+
+ ui.bar_width = region.width - ui.bar_x - ui.bar_end
+ ui.wcount = math.floor(
+ (ui.bar_width - 2 * ui.drawoffset) / (ui.thumb_size + ui.margin))
+
+ search_results = bpy.context.scene.get('search results')
+ if search_results != None:
+ ui.hcount = min(user_preferences.max_assetbar_rows, math.ceil(len(search_results) / ui.wcount))
+ else:
+ ui.hcount = 1
+ ui.bar_height = (ui.thumb_size + ui.margin) * ui.hcount + ui.margin
+ ui.bar_y = region.height - ui.bar_y_offset * ui_scale
+ if ui.down_up == 'UPLOAD':
+ ui.reports_y = ui.bar_y + 800
+ ui.reports_x = ui.bar_x
+ else:
+ ui.reports_y = ui.bar_y + ui.bar_height
+ ui.reports_x = ui.bar_x
+
+ ui.rating_x = ui.bar_x
+ ui.rating_y = ui.bar_y - ui.bar_height
+
+
+class AssetBarOperator(bpy.types.Operator):
+ '''runs search and displays the asset bar at the same time'''
+ bl_idname = "view3d.blenderkit_asset_bar"
+ bl_label = "BlenderKit Asset Bar UI"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ do_search: BoolProperty(name="Run Search", description='', default=True, options={'SKIP_SAVE'})
+ keep_running: BoolProperty(name="Keep Running", description='', default=True, options={'SKIP_SAVE'})
+ free_only: BoolProperty(name="Free Only", description='', default=False, options={'SKIP_SAVE'})
+
+ category: StringProperty(
+ name="Category",
+ description="search only subtree of this category",
+ default="", options={'SKIP_SAVE'})
+
+ def search_more(self):
+ sro = bpy.context.scene.get('search results orig', {})
+ if sro.get('next') != None:
+ search.search(get_next=True)
+
+ def exit_modal(self):
+ try:
+ bpy.types.SpaceView3D.draw_handler_remove(self._handle_2d, 'WINDOW')
+ bpy.types.SpaceView3D.draw_handler_remove(self._handle_3d, 'WINDOW')
+ except:
+ pass;
+ ui_props = bpy.context.scene.blenderkitUI
+
+ ui_props.dragging = False
+ ui_props.tooltip = ''
+ ui_props.active_index = -3
+ ui_props.draw_drag_image = False
+ ui_props.draw_snapped_bounds = False
+ ui_props.has_hit = False
+ ui_props.assetbar_on = False
+
+ def modal(self, context, event):
+ # This is for case of closing the area or changing type:
+ ui_props = context.scene.blenderkitUI
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+
+ areas = []
+
+ for w in context.window_manager.windows:
+ areas.extend(w.screen.areas)
+ if bpy.context.scene != self.scene:
+ self.exit_modal()
+ ui_props.assetbar_on = False
+ return {'CANCELLED'}
+
+ if self.area not in areas or self.area.type != 'VIEW_3D':
+ print('search areas')
+ # stopping here model by now - because of:
+ # switching layouts or maximizing area now fails to assign new area throwing the bug
+ # internal error: modal gizmo-map handler has invalid area
+ self.exit_modal()
+ ui_props.assetbar_on = False
+ return {'CANCELLED'}
+
+ newarea = None
+ for a in context.window.screen.areas:
+ if a.type == 'VIEW_3D':
+ self.area = a
+ for r in a.regions:
+ if r.type == 'WINDOW':
+ self.region = r
+ newarea = a
+ break;
+ # context.area = a
+
+ # we check again and quit if things weren't fixed this way.
+ if newarea == None:
+ self.exit_modal()
+ ui_props.assetbar_on = False
+ return {'CANCELLED'}
+
+ update_ui_size(self.area, self.region)
+
+ search.timer_update()
+ download.timer_update()
+ bg_blender.bg_update()
+
+ if context.region != self.region:
+ return {'PASS_THROUGH'}
+
+ # this was here to check if sculpt stroke is running, but obviously that didn't help,
+ # since the RELEASE event is cought by operator and thus there is no way to detect a stroke has ended...
+ if bpy.context.mode in ('SCULPT', 'PAINT_TEXTURE'):
+ if event.type == 'MOUSEMOVE': # ASSUME THAT SCULPT OPERATOR ACTUALLY STEALS THESE EVENTS,
+ # SO WHEN THERE ARE SOME WE CAN APPEND BRUSH...
+ bpy.context.window_manager['appendable'] = True
+ if event.type == 'LEFTMOUSE':
+ if event.value == 'PRESS':
+ bpy.context.window_manager['appendable'] = False
+
+ self.area.tag_redraw()
+ s = context.scene
+
+ if ui_props.turn_off:
+ ui_props.assetbar_on = False
+ ui_props.turn_off = False
+ self.exit_modal()
+ ui_props.draw_tooltip = False
+ return {'CANCELLED'}
+
+ if ui_props.down_up == 'UPLOAD':
+
+ ui_props.mouse_x = 0
+ ui_props.mouse_y = self.region.height
+
+ mx = event.mouse_x
+ my = event.mouse_y
+
+ ui_props.draw_tooltip = True
+
+ # only generate tooltip once in a while
+ if (
+ event.type == 'LEFTMOUSE' or event.type == 'RIGHTMOUSE') and event.value == 'RELEASE' or event.type == 'ENTER' or ui_props.tooltip == '':
+ ao = bpy.context.active_object
+ if ui_props.asset_type == 'MODEL' and ao != None \
+ or ui_props.asset_type == 'MATERIAL' and ao != None and ao.active_material != None \
+ or ui_props.asset_type == 'BRUSH':
+ export_data, upload_data, eval_path_computing, eval_path_state, eval_path, props = upload.get_upload_data(
+ self,
+ context,
+ ui_props.asset_type)
+ ui_props.tooltip = search.generate_tooltip(upload_data)
+
+ return {'PASS_THROUGH'}
+
+ # TODO add one more condition here to take less performance.
+ r = self.region
+ s = bpy.context.scene
+ sr = s.get('search results')
+
+ # If there aren't any results, we need no interaction(yet)
+ if sr is None:
+ return {'PASS_THROUGH'}
+ if len(sr) - ui_props.scrolloffset < (ui_props.wcount * ui_props.hcount) + 10:
+ self.search_more()
+ if event.type == 'WHEELUPMOUSE' or event.type == 'WHEELDOWNMOUSE' or event.type == 'TRACKPADPAN':
+ # scrolling
+ mx = event.mouse_region_x
+ my = event.mouse_region_y
+
+ if ui_props.dragging and not mouse_in_asset_bar(mx, my): # and my < r.height - ui_props.bar_height \
+ # and mx > 0 and mx < r.width and my > 0:
+ sprops = bpy.context.scene.blenderkit_models
+ if event.type == 'WHEELUPMOUSE':
+ sprops.offset_rotation_amount += sprops.offset_rotation_step
+ elif event.type == 'WHEELDOWNMOUSE':
+ sprops.offset_rotation_amount -= sprops.offset_rotation_step
+
+ #### TODO - this snapping code below is 3x in this file.... refactor it.
+ ui_props.has_hit, ui_props.snapped_location, ui_props.snapped_normal, ui_props.snapped_rotation, face_index, object, matrix = mouse_raycast(
+ context, mx, my)
+
+ # MODELS can be dragged on scene floor
+ if not ui_props.has_hit and ui_props.asset_type == 'MODEL':
+ ui_props.has_hit, ui_props.snapped_location, ui_props.snapped_normal, ui_props.snapped_rotation, face_index, object, matrix = floor_raycast(
+ context,
+ mx, my)
+
+ return {'RUNNING_MODAL'}
+
+ if not mouse_in_asset_bar(mx, my):
+ return {'PASS_THROUGH'}
+
+ # note - TRACKPADPAN is unsupported in blender by now.
+ # if event.type == 'TRACKPADPAN' :
+ # print(dir(event))
+ # print(event.value, event.oskey, event.)
+ if (event.type == 'WHEELDOWNMOUSE') and len(sr) - ui_props.scrolloffset > ui_props.wcount:
+ ui_props.scrolloffset += 1
+
+ if event.type == 'WHEELUPMOUSE' and ui_props.scrolloffset > 0:
+ ui_props.scrolloffset -= 1
+ return {'RUNNING_MODAL'}
+ if event.type == 'MOUSEMOVE': # Apply
+
+ r = self.region
+ mx = event.mouse_region_x
+ my = event.mouse_region_y
+
+ ui_props.mouse_x = mx
+ ui_props.mouse_y = my
+
+ if ui_props.dragging_rating or ui_props.rating_menu_on:
+ res = interact_rating(r, mx, my, event)
+ if res == True:
+ return {'RUNNING_MODAL'}
+
+ if ui_props.drag_init:
+ ui_props.drag_length += 1
+ if ui_props.drag_length > 0:
+ ui_props.dragging = True
+ ui_props.drag_init = False
+
+ if not (ui_props.dragging and mouse_in_region(r, mx, my)) and not mouse_in_asset_bar(mx, my):
+ ui_props.active_index = -3
+ ui_props.draw_tooltip = False
+ bpy.context.window.cursor_set("DEFAULT")
+ return {'PASS_THROUGH'}
+
+ sr = bpy.context.scene['search results']
+
+ if not ui_props.dragging:
+ bpy.context.window.cursor_set("DEFAULT")
+
+ if sr != None and ui_props.wcount * ui_props.hcount > len(sr) and ui_props.scrolloffset > 0:
+ ui_props.scrolloffset = 0
+
+ asset_search_index = get_asset_under_mouse(mx, my)
+ ui_props.active_index = asset_search_index
+ if asset_search_index > -1:
+
+ asset_data = sr[asset_search_index]
+ ui_props.draw_tooltip = True
+
+ ui_props.tooltip = asset_data['tooltip']
+ else:
+ ui_props.draw_tooltip = False
+
+ if mx > ui_props.bar_x + ui_props.bar_width - 50 and len(sr) - ui_props.scrolloffset > (
+ ui_props.wcount * ui_props.hcount):
+ ui_props.active_index = -1
+ return {'RUNNING_MODAL'}
+ if mx < ui_props.bar_x + 50 and ui_props.scrolloffset > 0:
+ ui_props.active_index = -2
+ return {'RUNNING_MODAL'}
+
+ else:
+ result = False
+ if ui_props.dragging and not mouse_in_asset_bar(mx, my) and mouse_in_region(r, mx, my):
+ ui_props.has_hit, ui_props.snapped_location, ui_props.snapped_normal, ui_props.snapped_rotation, face_index, object, matrix = mouse_raycast(
+ context, mx, my)
+ # MODELS can be dragged on scene floor
+ if not ui_props.has_hit and ui_props.asset_type == 'MODEL':
+ ui_props.has_hit, ui_props.snapped_location, ui_props.snapped_normal, ui_props.snapped_rotation, face_index, object, matrix = floor_raycast(
+ context,
+ mx, my)
+ if ui_props.has_hit and ui_props.asset_type == 'MODEL':
+ # this condition is here to fix a bug for a scene submitted by a user, so this situation shouldn't
+ # happen anymore, but there might exists scenes which have this problem for some reason.
+ if ui_props.active_index<len(sr) and ui_props.active_index>-1:
+ ui_props.draw_snapped_bounds = True
+ active_mod = sr[ui_props.active_index]
+ ui_props.snapped_bbox_min = Vector(active_mod['bbox_min'])
+ ui_props.snapped_bbox_max = Vector(active_mod['bbox_max'])
+
+ else:
+ ui_props.draw_snapped_bounds = False
+ ui_props.draw_drag_image = True
+ return {'RUNNING_MODAL'}
+
+ if event.type == 'LEFTMOUSE':
+
+ r = self.region
+ mx = event.mouse_x - r.x
+ my = event.mouse_y - r.y
+
+ ui_props = context.scene.blenderkitUI
+ if event.value == 'PRESS' and ui_props.active_index > -1:
+ if ui_props.asset_type == 'MODEL' or ui_props.asset_type == 'MATERIAL':
+ ui_props.drag_init = True
+ bpy.context.window.cursor_set("NONE")
+ ui_props.draw_tooltip = False
+ ui_props.drag_length = 0
+
+ if ui_props.rating_on:
+ res = interact_rating(r, mx, my, event)
+ if res:
+ return {'RUNNING_MODAL'}
+
+ if not ui_props.dragging and not mouse_in_asset_bar(mx, my):
+ return {'PASS_THROUGH'}
+
+ # this can happen by switching result asset types - length of search result changes
+ if ui_props.scrolloffset > 0 and (ui_props.wcount * ui_props.hcount) > len(sr) - ui_props.scrolloffset:
+ ui_props.scrolloffset = len(sr) - (ui_props.wcount * ui_props.hcount)
+
+ if event.value == 'RELEASE': # Confirm
+ ui_props.drag_init = False
+
+ # scroll by a whole page
+ if mx > ui_props.bar_x + ui_props.bar_width - 50 and len(
+ sr) - ui_props.scrolloffset > ui_props.wcount * ui_props.hcount:
+ ui_props.scrolloffset = min(
+ ui_props.scrolloffset + (ui_props.wcount * ui_props.hcount),
+ len(sr) - ui_props.wcount * ui_props.hcount)
+ return {'RUNNING_MODAL'}
+ if mx < ui_props.bar_x + 50 and ui_props.scrolloffset > 0:
+ ui_props.scrolloffset = max(0, ui_props.scrolloffset - ui_props.wcount * ui_props.hcount)
+ return {'RUNNING_MODAL'}
+
+ # Drag-drop interaction
+ if ui_props.dragging and mouse_in_region(r, mx, my):
+ asset_search_index = ui_props.active_index
+ # raycast here
+ ui_props.active_index = -3
+
+ if ui_props.asset_type == 'MODEL':
+
+ ui_props.has_hit, ui_props.snapped_location, ui_props.snapped_normal, ui_props.snapped_rotation, face_index, object, matrix = mouse_raycast(
+ context, mx, my)
+
+ # MODELS can be dragged on scene floor
+ if not ui_props.has_hit and ui_props.asset_type == 'MODEL':
+ ui_props.has_hit, ui_props.snapped_location, ui_props.snapped_normal, ui_props.snapped_rotation, face_index, object, matrix = floor_raycast(
+ context,
+ mx, my)
+
+ if not ui_props.has_hit:
+ return {'RUNNING_MODAL'}
+
+ target_object = ''
+ if object is not None:
+ target_object = object.name
+ target_slot = ''
+
+ if ui_props.asset_type == 'MATERIAL':
+ ui_props.has_hit, ui_props.snapped_location, ui_props.snapped_normal, ui_props.snapped_rotation, face_index, object, matrix = mouse_raycast(
+ context, mx, my)
+
+ if not ui_props.has_hit:
+ # this is last attempt to get object under mouse - for curves and other objects than mesh.
+ ui_props.dragging = False
+ sel = utils.selection_get()
+ bpy.ops.view3d.select(location=(event.mouse_region_x, event.mouse_region_y))
+ sel1 = utils.selection_get()
+ if sel[0] != sel1[0] and sel1[0].type != 'MESH':
+ object = sel1[0]
+ target_slot = sel1[0].active_material_index
+ ui_props.has_hit = True
+ utils.selection_set(sel)
+
+ if not ui_props.has_hit:
+ print('select fun')
+ return {'RUNNING_MODAL'}
+
+ else:
+ # first, test if object can have material applied.
+ if object is not None and not object.is_library_indirect:
+ target_object = object.name
+ # create final mesh to extract correct material slot
+ temp_mesh = object.to_mesh(depsgraph=bpy.context.depsgraph, apply_modifiers=True,
+ calc_undeformed=False)
+ target_slot = temp_mesh.polygons[face_index].material_index
+ else:
+ self.report({'WARNING'}, "Invalid or library object as input:")
+ target_object = ''
+ target_slot = ''
+
+ # Click interaction
+ else:
+ asset_search_index = get_asset_under_mouse(mx, my)
+
+ if ui_props.asset_type in ('MATERIAL',
+ 'MODEL'): # this was meant for particles, commenting for now or ui_props.asset_type == 'MODEL':
+ ao = bpy.context.active_object
+ if ao != None and not ao.is_library_indirect:
+ target_object = bpy.context.active_object.name
+ target_slot = bpy.context.active_object.active_material_index
+ else:
+ target_object = ''
+ target_slot = ''
+ # FIRST START SEARCH
+
+ if asset_search_index == -3:
+ return {'RUNNING_MODAL'}
+ if asset_search_index > -3:
+ if ui_props.asset_type == 'MATERIAL':
+ if target_object != '':
+ # position is for downloader:
+ loc = ui_props.snapped_location
+ rotation = (0, 0, 0)
+
+ asset_data = sr[asset_search_index]
+ utils.automap(target_object, target_slot=target_slot,
+ tex_size=asset_data.get('texture_size_meters', 1.0))
+ bpy.ops.scene.blenderkit_download(True,
+ asset_type=ui_props.asset_type,
+ asset_index=asset_search_index,
+ model_location=loc,
+ model_rotation=rotation,
+ target_object=target_object,
+ material_target_slot=target_slot)
+
+
+ elif ui_props.asset_type == 'MODEL':
+ if ui_props.has_hit and ui_props.dragging:
+ loc = ui_props.snapped_location
+ rotation = ui_props.snapped_rotation
+ else:
+ loc = s.cursor.location
+ rotation = s.cursor.rotation_euler
+
+ bpy.ops.scene.blenderkit_download(True,
+ asset_type=ui_props.asset_type,
+ asset_index=asset_search_index,
+ model_location=loc,
+ model_rotation=rotation,
+ target_object=target_object)
+
+ else:
+ bpy.ops.scene.blenderkit_download(asset_type=ui_props.asset_type,
+ asset_index=asset_search_index)
+
+ ui_props.dragging = False
+ return {'RUNNING_MODAL'}
+ else:
+ return {'RUNNING_MODAL'}
+
+ if event.type == 'X' and ui_props.active_index != -3:
+ sr = bpy.context.scene['search results']
+ asset_data = sr[ui_props.active_index]
+ print(asset_data['name'])
+ print('delete')
+ paths.delete_asset_debug(asset_data)
+ asset_data['downloaded'] = 0
+ return {'RUNNING_MODAL'}
+ return {'PASS_THROUGH'}
+
+ def invoke(self, context, event):
+ # FIRST START SEARCH
+ ui_props = context.scene.blenderkitUI
+
+ if self.do_search:
+ # we erase search keywords for cateogry search now, since these combinations usually return nothing now.
+ # when the db gets bigger, this can be deleted.
+ if self.category != '':
+ sprops = utils.get_search_props()
+ sprops.search_keywords = ''
+ search.search(category=self.category, free_only=self.free_only)
+
+ if ui_props.assetbar_on:
+ # we don't want to run the assetbar many times, that's why it has a switch on/off behaviour,
+ # unless being called with 'keep_running' prop.
+ if not self.keep_running:
+ # this sends message to the originally running operator, so it quits, and then it ends this one too.
+ # If it initiated a search, the search will finish in a thread. The switch off procedure is run
+ # by the 'original' operator, since if we get here, it means
+ # same operator is allready running.
+ ui_props.turn_off = True
+ # if there was an error, we need to turn off these props so we can restart after 2 clicks
+ ui_props.assetbar_on = False
+
+ else:
+ pass
+ return {'FINISHED'}
+
+ ui_props.dragging = False #only for cases where assetbar ended with an error.
+ ui_props.assetbar_on = True
+ ui_props.turn_off = False
+
+ sr = bpy.context.scene.get('search results')
+ if sr is None:
+ bpy.context.scene['search results'] = []
+
+ if context.area.type == 'VIEW_3D':
+ # the arguments we pass the the callback
+ args = (self, context)
+ self.area = context.area
+ self.scene = bpy.context.scene
+
+ for r in self.area.regions:
+ if r.type == 'WINDOW':
+ self.region = r
+
+ update_ui_size(self.area, self.region)
+
+ self._handle_2d = bpy.types.SpaceView3D.draw_handler_add(draw_callback_2d, args, 'WINDOW', 'POST_PIXEL')
+ self._handle_3d = bpy.types.SpaceView3D.draw_handler_add(draw_callback_3d, args, 'WINDOW', 'POST_VIEW')
+
+ context.window_manager.modal_handler_add(self)
+ ui_props.assetbar_on = True
+ return {'RUNNING_MODAL'}
+ else:
+
+ self.report({'WARNING'}, "View3D not found, cannot run operator")
+ return {'CANCELLED'}
+
+ def execute(self, context):
+ return {'RUNNING_MODAL'}
+
+
+classess = (
+ AssetBarOperator,
+
+)
+
+# store keymaps here to access after registration
+addon_keymaps = []
+
+
+def register_ui():
+ for c in classess:
+ bpy.utils.register_class(c)
+
+ args = (None, bpy.context)
+ bpy.types.SpaceView3D.draw_handler_add(draw_callback_2d_progress, args, 'WINDOW', 'POST_PIXEL')
+ bpy.types.SpaceView3D.draw_handler_add(draw_callback_3d_progress, args, 'WINDOW', 'POST_VIEW')
+
+ wm = bpy.context.window_manager
+
+ # spaces solved by registering shortcut to Window. Couldn't register object mode before somehow.
+ if not wm.keyconfigs.addon:
+ return
+ km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type='EMPTY')
+ kmi = km.keymap_items.new(AssetBarOperator.bl_idname, 'SEMI_COLON', 'PRESS', ctrl=False, shift=False)
+ kmi.properties.keep_running = False
+ kmi.properties.do_search = False
+
+ addon_keymaps.append(km)
+
+
+def unregister_ui():
+ for c in classess:
+ bpy.utils.unregister_class(c)
+
+ args = (None, bpy.context)
+
+ try:
+ bpy.types.SpaceView3D.draw_handler_remove(draw_callback_2d_progress, args, 'WINDOW', 'POST_PIXEL')
+ bpy.types.SpaceView3D.draw_handler_remove(draw_callback_3d_progress, args, 'WINDOW', 'POST_VIEW')
+ except:
+ print('handlers allready removed')
+ wm = bpy.context.window_manager
+ if not wm.keyconfigs.addon:
+ return
+
+ for km in addon_keymaps:
+ wm.keyconfigs.addon.keymaps.remove(km)
+ del addon_keymaps[:]
diff --git a/blenderkit/ui_bgl.py b/blenderkit/ui_bgl.py
new file mode 100644
index 00000000..2165cdc5
--- /dev/null
+++ b/blenderkit/ui_bgl.py
@@ -0,0 +1,127 @@
+# ##### 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 #####
+
+import bgl, blf
+
+import bpy, blf
+import gpu
+from gpu_extras.batch import batch_for_shader
+
+
+def draw_rect(x, y, width, height, color):
+ xmax = x + width
+ ymax = y + height
+ points = [[x, y], # [x, y]
+ [x, ymax], # [x, y]
+ [xmax, ymax], # [x, y]
+ [xmax, y], # [x, y]
+ ]
+ indices = ((0, 1, 2), (2, 3, 0))
+
+ shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
+ batch = batch_for_shader(shader, 'TRIS', {"pos": points}, indices=indices)
+
+ shader.bind()
+ shader.uniform_float("color", color)
+ bgl.glEnable(bgl.GL_BLEND)
+ batch.draw(shader)
+
+
+def draw_line2d(x1, y1, x2, y2, width, color):
+ coords = (
+ (x1, y1), (x2, y2))
+
+ indices = (
+ (0, 1),)
+ bgl.glEnable(bgl.GL_BLEND)
+
+ shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
+ batch = batch_for_shader(shader, 'LINES', {"pos": coords}, indices=indices)
+ shader.bind()
+ shader.uniform_float("color", color)
+ batch.draw(shader)
+
+
+def draw_lines(vertices, indices, color):
+ bgl.glEnable(bgl.GL_BLEND)
+
+ shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
+ batch = batch_for_shader(shader, 'LINES', {"pos": vertices}, indices=indices)
+ shader.bind()
+ shader.uniform_float("color", color)
+ batch.draw(shader)
+
+
+def draw_rect_3d(coords, color):
+ indices = [(0, 1, 2), (2, 3, 0)]
+ shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
+ batch = batch_for_shader(shader, 'TRIS', {"pos": coords}, indices=indices)
+ shader.uniform_float("color", color)
+ batch.draw(shader)
+
+
+def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1)):
+ # draw_rect(x,y, width, height, (.5,0,0,.5))
+
+ coords = [
+ (x, y), (x + width, y),
+ (x, y + height), (x + width, y + height)]
+
+ uvs = [(crop[0], crop[1]),
+ (crop[2], crop[1]),
+ (crop[0], crop[3]),
+ (crop[2], crop[3]),
+ ]
+
+ indices = [(0, 1, 2), (2, 1, 3)]
+
+ shader = gpu.shader.from_builtin('2D_IMAGE')
+ batch = batch_for_shader(shader, 'TRIS',
+ {"pos": coords,
+ "texCoord": uvs},
+ indices=indices)
+
+ # send image to gpu if it isn't there already
+ if image.gl_load():
+ raise Exception()
+
+ # texture identifier on gpu
+ texture_id = image.bindcode
+
+ # in case someone disabled it before
+ bgl.glEnable(bgl.GL_BLEND)
+
+ # bind texture to image unit 0
+ bgl.glActiveTexture(bgl.GL_TEXTURE0)
+ bgl.glBindTexture(bgl.GL_TEXTURE_2D, texture_id)
+
+ shader.bind()
+ # tell shader to use the image that is bound to image unit 0
+ shader.uniform_int("image", 0)
+ batch.draw(shader)
+
+ bgl.glDisable(bgl.GL_TEXTURE_2D)
+
+
+def draw_text(text, x, y, size, color=(1, 1, 1, 0.5)):
+ font_id = 0
+ # bgl.glColor4f(*color)
+ blf.color(font_id, color[0], color[1], color[2], color[3])
+ blf.position(font_id, x, y, 0)
+ blf.size(font_id, size, 72)
+ blf.draw(font_id, text)
diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py
new file mode 100644
index 00000000..9444f619
--- /dev/null
+++ b/blenderkit/ui_panels.py
@@ -0,0 +1,769 @@
+# ##### 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 #####
+
+if "bpy" in locals():
+ import imp
+
+ imp.reload(paths)
+ imp.reload(ratings)
+ imp.reload(utils)
+ imp.reload(search)
+ imp.reload(upload)
+else:
+ from blenderkit import paths, ratings, utils, download, categories, ui
+
+from bpy.types import (
+ Panel
+)
+
+import bpy
+
+
+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 = 3
+ 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:]
+ li+=1
+ if li > maxlines:
+ break;
+ if li>maxlines:
+ break;
+ layout.label(text=l, icon=icon)
+ icon = 'NONE'
+
+
+# this was moved to separate interface:
+
+def draw_ratings(layout, context):
+ # layout.operator("wm.url_open", text="Read rating instructions", icon='QUESTION').url = 'https://support.google.com/?hl=en'
+ asset = utils.get_active_asset()
+ bkit_ratings = asset.bkit_ratings
+
+ ratings.draw_rating(layout, bkit_ratings, 'rating_quality', 'Quality')
+ layout.separator()
+ layout.prop(bkit_ratings, 'rating_work_hours')
+ w = context.region.width
+
+ layout.label(text='problems')
+ layout.prop(bkit_ratings, 'rating_problems', text='')
+ layout.label(text='compliments')
+ layout.prop(bkit_ratings, 'rating_compliments', text='')
+
+ row = layout.row()
+ op = row.operator("object.blenderkit_rating_upload", text="Send rating", icon='URL')
+ return op
+
+
+def draw_upload_common(layout, props, asset_type, context):
+ op = layout.operator("wm.url_open", text="Read upload instructions",
+ icon='QUESTION')
+ if asset_type == 'MODEL':
+ op.url = paths.BLENDERKIT_MODEL_UPLOAD_INSTRUCTIONS_URL
+ if asset_type == 'MATERIAL':
+ op.url = paths.BLENDERKIT_MATERIAL_UPLOAD_INSTRUCTIONS_URL
+
+ row = layout.row(align=True)
+ if props.upload_state != '':
+ label_multiline(layout, text=props.upload_state, width=context.region.width)
+ if props.uploading:
+ op = layout.operator('object.kill_bg_process', text="", icon='CANCEL')
+ op.process_source = asset_type
+ op.process_type = 'UPLOAD'
+ layout = layout.column()
+ layout.enabled = False
+
+ # if props.upload_state.find('Error') > -1:
+ # layout.label(text = props.upload_state)
+
+ if props.asset_base_id == '':
+ optext = 'Upload %s' % asset_type.lower()
+ else:
+ optext = 'Reupload %s (with thumbnail)' % asset_type.lower()
+
+ op = layout.operator("object.blenderkit_upload", text=optext, icon='EXPORT')
+ op.asset_type = asset_type
+ op.as_new = False
+
+ if props.asset_base_id != '':
+ op = layout.operator("object.blenderkit_upload", text='Reupload only metadata', icon='EXPORT')
+ op.asset_type = asset_type
+ op.metadata_only = True
+
+ op = layout.operator("object.blenderkit_upload", text='Upload as new asset', icon='EXPORT')
+ op.asset_type = asset_type
+ op.as_new = True
+ # layout.label(text = 'asset id, overwrite only for reuploading')
+ if props.asset_base_id != '':
+ row = layout.row()
+
+ row.prop(props, 'asset_base_id', icon='FILE_TICK')
+ layout.operator("object.blenderkit_mark_for_validation", icon='EXPORT')
+
+ layout.prop(props, 'category')
+ if asset_type == 'MODEL' and props.subcategory != '': # by now block this for other asset types.
+ layout.prop(props, 'subcategory')
+
+ layout.prop(props, 'license')
+
+
+def poll_local_panels():
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ return user_preferences.panel_behaviour == 'BOTH' or user_preferences.panel_behaviour == 'LOCAL'
+
+
+def prop_needed(layout, props, name, value, is_not_filled=''):
+ row = layout.row()
+ if value == is_not_filled:
+ # row.label(text='', icon = 'ERROR')
+ icon = 'ERROR'
+ row.prop(props, name, icon=icon)
+ else:
+ # row.label(text='', icon = 'FILE_TICK')
+ icon = None
+ row.prop(props, name)
+
+
+def draw_panel_model_upload(self, context):
+ ob = bpy.context.active_object
+ while ob.parent is not None:
+ ob = ob.parent
+ props = ob.blenderkit
+
+ layout = self.layout
+
+ draw_upload_common(layout, props, 'MODEL', context)
+
+ prop_needed(layout, props, 'name', props.name)
+
+ col = layout.column()
+ if props.is_generating_thumbnail:
+ col.enabled = False
+ prop_needed(col, props, 'thumbnail', props.has_thumbnail, False)
+ if bpy.context.scene.render.engine in ('CYCLES', 'BLENDER_EEVEE'):
+ col.operator("object.blenderkit_generate_thumbnail", text='Generate thumbnail', icon='IMAGE')
+
+ # row = layout.row(align=True)
+ if props.is_generating_thumbnail:
+ row = layout.row(align=True)
+ row.label(text=props.thumbnail_generating_state)
+ op = row.operator('object.kill_bg_process', text="", icon='CANCEL')
+ op.process_source = 'MODEL'
+ op.process_type = 'THUMBNAILER'
+ elif props.thumbnail_generating_state != '':
+ label_multiline(layout, text=props.thumbnail_generating_state)
+
+ layout.prop(props, 'description')
+ prop_needed(layout, props, 'tags', props.tags)
+ # prop_needed(layout, props, 'style', props.style)
+ # prop_needed(layout, props, 'production_level', props.production_level)
+ layout.prop(props, 'style')
+ layout.prop(props, 'production_level')
+
+ layout.prop(props, 'condition')
+ layout.prop(props, 'is_free')
+ layout.prop(props, 'pbr')
+ layout.label(text='design props:')
+ layout.prop(props, 'manufacturer')
+ layout.prop(props, 'designer')
+ layout.prop(props, 'design_collection')
+ layout.prop(props, 'design_variant')
+ layout.prop(props, 'use_design_year')
+ if props.use_design_year:
+ layout.prop(props, 'design_year')
+
+ row = layout.row()
+ if props.work_hours == 0:
+ row.label(text='', icon='ERROR')
+ row.prop(props, 'work_hours')
+
+ layout.prop(props, 'adult')
+
+
+def draw_panel_scene_upload(self, context):
+ s = bpy.context.scene
+ props = s.blenderkit
+
+ layout = self.layout
+ if bpy.app.debug_value != -1:
+ layout.label(text='Scene upload not Implemented')
+ return
+ draw_upload_common(layout, props, 'SCENE', context)
+
+ # layout = layout.column()
+
+ # row = layout.row()
+
+ # if props.dimensions[0] + props.dimensions[1] == 0 and props.face_count == 0:
+ # icon = 'ERROR'
+ # layout.operator("object.blenderkit_auto_tags", text='Auto fill tags', icon=icon)
+ # else:
+ # layout.operator("object.blenderkit_auto_tags", text='Auto fill tags')
+
+ prop_needed(layout, props, 'name', props.name)
+
+ col = layout.column()
+ # if props.is_generating_thumbnail:
+ # col.enabled = False
+ prop_needed(col, props, 'thumbnail', props.has_thumbnail, False)
+ # if bpy.context.scene.render.engine == 'CYCLES':
+ # col.operator("object.blenderkit_generate_thumbnail", text='Generate thumbnail', icon='IMAGE_COL')
+
+ # row = layout.row(align=True)
+ # if props.is_generating_thumbnail:
+ # row = layout.row(align=True)
+ # row.label(text = props.thumbnail_generating_state)
+ # op = row.operator('object.kill_bg_process', text="", icon='CANCEL')
+ # op.process_source = 'MODEL'
+ # op.process_type = 'THUMBNAILER'
+ # elif props.thumbnail_generating_state != '':
+ # label_multiline(layout, text = props.thumbnail_generating_state)
+
+ layout.prop(props, 'description')
+ prop_needed(layout, props, 'tags', props.tags)
+ layout.prop(props, 'style')
+ layout.prop(props, 'production_level')
+ layout.prop(props, 'use_design_year')
+ if props.use_design_year:
+ layout.prop(props, 'design_year')
+ layout.prop(props, 'condition')
+ row = layout.row()
+ if props.work_hours == 0:
+ row.label(text='', icon='ERROR')
+ row.prop(props, 'work_hours')
+ layout.prop(props, 'adult')
+
+
+def draw_panel_model_search(self, context):
+ s = context.scene
+ props = s.blenderkit_models
+ layout = self.layout
+ layout.prop(props, "search_keywords", text="", icon='VIEWZOOM')
+
+ icon = 'NONE'
+ if props.report == 'Available only in higher plans.':
+ icon = 'ERROR'
+ label_multiline(layout, text=props.report, icon=icon)
+ if props.report == 'Available only in higher plans.':
+ layout.operator("wm.url_open", text="Check plans", icon='URL').url = paths.BLENDERKIT_PLANS
+
+ layout.prop(props, "search_style")
+ # if props.search_style == 'OTHER':
+ # layout.prop(props, "search_style_other")
+ # layout.prop(props, "search_engine")
+ # col = layout.column()
+ # layout.prop(props, 'append_link', expand=True, icon_only=False)
+ # layout.prop(props, 'import_as', expand=True, icon_only=False)
+
+ # layout.prop(props, "search_advanced")
+ if props.search_advanced:
+ layout.separator()
+
+ # layout.label(text = "common searches keywords:")
+ # layout.prop(props, "search_global_keywords", text = "")
+ # layout.prop(props, "search_modifier_keywords")
+ if props.search_engine == 'OTHER':
+ layout.prop(props, "search_engine_keyword")
+
+ # AGE
+ layout.prop(props, "search_condition") # , text ='condition of object new/old e.t.c.')
+
+ # DESIGN YEAR
+ layout.prop(props, "search_design_year", text='designed in ( min - max )')
+ row = layout.row(align=True)
+ if not props.search_design_year_min:
+ row.active = False
+ row.prop(props, "search_design_year_min", text='min')
+ row.prop(props, "search_design_year_max", text='max')
+
+ # POLYCOUNT
+ layout.prop(props, "search_polycount", text='polycount in ( min - max )')
+ row = layout.row(align=True)
+ if not props.search_polycount:
+ row.active = False
+ row.prop(props, "search_polycount_min", text='min')
+ row.prop(props, "search_polycount_max", text='max')
+
+ # TEXTURE RESOLUTION
+ layout.prop(props, "search_texture_resolution", text='texture resolution ( min - max )')
+ row = layout.row(align=True)
+ if not props.search_texture_resolution:
+ row.active = False
+ row.prop(props, "search_texture_resolution_min", text='min')
+ row.prop(props, "search_texture_resolution_max", text='max')
+
+ # ADULT
+ layout.prop(props, "search_adult") # , text ='condition of object new/old e.t.c.')
+
+ draw_panel_categories(self, context)
+
+ layout.separator()
+ layout.label(text='how to import assets:')
+ col = layout.column()
+ col.prop(props, 'append_method', expand=True, icon_only=False)
+ layout.prop(props, 'randomize_rotation')
+ if props.randomize_rotation:
+ layout.prop(props, 'randomize_rotation_amount')
+
+
+def draw_panel_scene_search(self, context):
+ s = context.scene
+ props = s.blenderkit_scene
+ layout = self.layout
+ # layout.label(text = "common search properties:")
+ layout.prop(props, "search_keywords", text="", icon='VIEWZOOM')
+
+ label_multiline(layout, text=props.report)
+
+ layout.prop(props, "search_style")
+ if props.search_style == 'OTHER':
+ layout.prop(props, "search_style_other")
+ layout.prop(props, "search_engine")
+ layout.separator()
+ draw_panel_categories(self, context)
+
+
+class VIEW3D_PT_blenderkit_model_properties(Panel):
+ bl_category = "BlenderKit"
+ bl_idname = "VIEW3D_PT_blenderkit_model_properties"
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_label = "Model tweaking"
+ bl_context = "objectmode"
+
+ @classmethod
+ def poll(cls, context):
+ p = bpy.context.active_object is not None and bpy.context.active_object.get('asset_data') is not None
+ return p
+
+ def draw(self, context):
+ # draw asset properties here
+ layout = self.layout
+
+ o = bpy.context.active_object
+ ad = o['asset_data']
+ layout.label(text=str(ad['name']))
+ # proxies just don't make it in 2.79... they should stay hidden and used only by pros ...
+ # if 'rig' in ad['tags']:
+ # # layout.label(text = 'can make proxy')
+ # layout.operator('object.blenderkit_make_proxy', text = 'Make Armature proxy')
+
+ layout.operator('object.blenderkit_bring_to_scene', text='Bring to scene')
+ # layout.operator('object.blenderkit_color_corrector')
+
+
+def draw_panel_model_rating(self, context):
+ o = bpy.context.active_object
+ op = draw_ratings(self.layout, context) # , props)
+ op.asset_type = 'MODEL'
+
+
+def draw_panel_material_upload(self, context):
+ o = bpy.context.active_object
+ mat = bpy.context.active_object.active_material
+
+ props = mat.blenderkit
+ layout = self.layout
+
+ draw_upload_common(layout, props, 'MATERIAL', context)
+
+ prop_needed(layout, props, 'name', props.name)
+ layout.prop(props, 'description')
+ layout.prop(props, 'style')
+ if props.style == 'OTHER':
+ layout.prop(props, 'style_other')
+ # layout.prop(props, 'engine')
+ # if props.engine == 'OTHER':
+ # layout.prop(props, 'engine_other')
+ prop_needed(layout, props, 'tags', props.tags)
+ # layout.prop(props,'shaders')#TODO autofill on upload
+ # row = layout.row()
+ layout.prop(props, 'pbr')
+ layout.prop(props, 'uv')
+ layout.prop(props, 'animated')
+ layout.prop(props, 'texture_size_meters')
+
+ # THUMBNAIL
+ row = layout.row()
+ if props.is_generating_thumbnail:
+ row.enabled = False
+ prop_needed(row, props, 'thumbnail', props.has_thumbnail, False)
+
+ if props.is_generating_thumbnail:
+ row = layout.row(align=True)
+ row.label(text=props.thumbnail_generating_state, icon='RENDER_STILL')
+ op = row.operator('object.kill_bg_process', text="", icon='CANCEL')
+ op.process_source = 'MATERIAL'
+ op.process_type = 'THUMBNAILER'
+ elif props.thumbnail_generating_state != '':
+ label_multiline(layout, text=props.thumbnail_generating_state)
+
+ if bpy.context.scene.render.engine in ('CYCLES', 'BLENDER_EEVEE'):
+ layout.operator("object.blenderkit_material_thumbnail", text='Render thumbnail with Cycles', icon='EXPORT')
+
+ # tname = "." + bpy.context.active_object.active_material.name + "_thumbnail"
+ # if props.has_thumbnail and bpy.data.textures.get(tname) is not None:
+ # row = layout.row()
+ # # row.scale_y = 1.5
+ # row.template_preview(bpy.data.textures[tname], preview_id='test')
+
+
+def draw_panel_material_search(self, context):
+ wm = context.scene
+ props = wm.blenderkit_mat
+
+ layout = self.layout
+ layout.prop(props, "search_keywords", text="", icon='VIEWZOOM')
+
+ label_multiline(layout, text=props.report)
+
+ layout.prop(props, 'search_style')
+ if props.search_style == 'OTHER':
+ layout.prop(props, 'search_style_other')
+ layout.prop(props, 'search_engine')
+ if props.search_engine == 'OTHER':
+ layout.prop(props, 'search_engine_other')
+
+ layout.prop(props, 'automap')
+
+ draw_panel_categories(self, context)
+
+
+def draw_panel_material_ratings(self, context):
+ op = draw_ratings(self.layout, context) # , props)
+ op.asset_type = 'MATERIAL'
+
+
+def draw_panel_brush_upload(self, context):
+ brush = utils.get_active_brush()
+ if brush is not None:
+ props = brush.blenderkit
+
+ layout = self.layout
+
+ draw_upload_common(layout, props, 'BRUSH', context)
+
+ layout.prop(props, 'name')
+ layout.prop(props, 'description')
+ layout.prop(props, 'tags')
+
+
+def draw_panel_brush_search(self, context):
+ wm = context.scene
+ props = wm.blenderkit_brush
+
+ layout = self.layout
+ layout.prop(props, "search_keywords", text="", icon='VIEWZOOM')
+ label_multiline(layout, text=props.report)
+ draw_panel_categories(self, context)
+
+
+def draw_panel_brush_ratings(self, context):
+ # props = utils.get_brush_props(context)
+ op = draw_ratings(self.layout, context) # , props)
+
+ op.asset_type = 'BRUSH'
+
+
+class VIEW3D_PT_blenderkit_unified(Panel):
+ bl_category = "BlenderKit"
+ bl_idname = "VIEW3D_PT_blenderkit_unified"
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_label = "BlenderKit"
+
+ @classmethod
+ def poll(cls, context):
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ return user_preferences.panel_behaviour == 'BOTH' or user_preferences.panel_behaviour == 'UNIFIED'
+
+ def draw(self, context):
+ s = context.scene
+ ui_props = s.blenderkitUI
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ wm = bpy.context.window_manager
+ layout = self.layout
+
+ row = layout.row()
+
+ #
+ row.prop(ui_props, 'down_up', expand=True, icon_only=True)
+ # row.label('')
+ row = row.split().row()
+ row.prop(ui_props, 'asset_type', expand=True, icon_only=True)
+
+ w = context.region.width
+
+ if user_preferences.api_key == '':
+ op = layout.operator("wm.url_open", text="Register online",
+ icon='QUESTION')
+ op.url = paths.BLENDERKIT_SIGNUP_URL
+ layout.label(text='Paste your API Key:')
+ layout.prop(user_preferences, 'api_key', text='')
+ layout.separator()
+ elif bpy.data.filepath == '':
+
+ label_multiline(layout, text="It's better to save the file first.", width=w)
+ layout.separator()
+ if wm.get('bkit_update'):
+ label_multiline(layout, text="New version available!", icon='INFO', width=w)
+ layout.operator("wm.url_open", text="Get new version",
+ icon='URL').url = paths.BLENDERKIT_ADDON_FILE_URL
+ layout.separator()
+ layout.separator()
+ layout.separator()
+ if ui_props.down_up == 'SEARCH':
+ # global assetbar_on
+ if not ui_props.assetbar_on:
+ icon = 'EXPORT'
+ text = 'Show AssetBar - ;'
+ row = layout.row()
+ sr = bpy.context.scene.get('search results')
+ if sr != None:
+ icon = 'RESTRICT_VIEW_OFF'
+ row.scale_y = 1
+ text = 'Show Assetbar to see %i results - ;' % len(sr)
+ op = row.operator('view3d.blenderkit_asset_bar', text=text, icon=icon)
+
+ else:
+
+ op = layout.operator('view3d.blenderkit_asset_bar', text='Hide AssetBar - ;', icon='EXPORT')
+ op.keep_running = False
+ op.do_search = False
+
+ if ui_props.asset_type == 'MODEL':
+ # noinspection PyCallByClass
+ draw_panel_model_search(self, context)
+ if ui_props.asset_type == 'SCENE':
+ # noinspection PyCallByClass
+ draw_panel_scene_search(self, context)
+
+ elif ui_props.asset_type == 'MATERIAL':
+ draw_panel_material_search(self, context)
+ elif ui_props.asset_type == 'BRUSH':
+ if context.sculpt_object or context.image_paint_object:
+ # noinspection PyCallByClass
+ draw_panel_brush_search(self, context)
+ else:
+ label_multiline(layout, text='switch to paint or sculpt mode.', width=context.region.width)
+ return
+
+ # blocking this now. It became terribly slow.
+ layout.operator("wm.url_open", text="See my uploads",
+ icon='URL').url = paths.BLENDERKIT_USER_ASSETS
+ elif ui_props.down_up == 'UPLOAD':
+ if not ui_props.assetbar_on:
+ text = 'Show asset preview - ;'
+ else:
+ text = 'Hide asset preview - ;'
+ op = layout.operator('view3d.blenderkit_asset_bar', text=text, icon='EXPORT')
+ op.keep_running = False
+ op.do_search = False
+ e = s.render.engine
+ if e not in ('CYCLES', 'BLENDER_EEVEE'):
+ rtext = 'Only Cycles and EEVEE render engines are currently supported. ' \
+ 'Please use Cycles for all assets you upload to BlenderKit.'
+ label_multiline(layout, rtext, icon='ERROR', width=w)
+ return;
+
+ if ui_props.asset_type == 'MODEL':
+ label_multiline(layout, "Uploaded models won't be available in b2.79", icon='ERROR')
+ if bpy.context.active_object is not None:
+ draw_panel_model_upload(self, context)
+ else:
+ layout.label(text='selet object to upload')
+ elif ui_props.asset_type == 'SCENE':
+ draw_panel_scene_upload(self, context)
+
+ elif ui_props.asset_type == 'MATERIAL':
+ label_multiline(layout, "Uploaded materials won't be available in b2.79", icon='ERROR')
+
+ if bpy.context.active_object is not None and bpy.context.active_object.active_material is not None:
+ draw_panel_material_upload(self, context)
+ else:
+ label_multiline(layout, text='select object with material to upload materials', width=w)
+
+ elif ui_props.asset_type == 'BRUSH':
+ if context.sculpt_object or context.image_paint_object:
+ draw_panel_brush_upload(self, context)
+ else:
+ layout.label(text='switch to paint or sculpt mode.')
+
+ elif ui_props.down_up == 'RATING': # the poll functions didn't work here, don't know why.
+
+ if ui_props.asset_type == 'MODEL':
+ # TODO improve poll here to parenting structures
+ if bpy.context.active_object is not None and bpy.context.active_object.get('asset_data') != None:
+ ad = bpy.context.active_object.get('asset_data')
+ layout.label(text=ad['name'])
+ draw_panel_model_rating(self, context)
+ if ui_props.asset_type == 'MATERIAL':
+ if bpy.context.active_object is not None and \
+ bpy.context.active_object.active_material is not None and \
+ bpy.context.active_object.active_material.blenderkit.asset_base_id != '':
+ layout.label(text=bpy.context.active_object.active_material.blenderkit.name + ' :')
+ # noinspection PyCallByClass
+ draw_panel_material_ratings(self, context)
+ if ui_props.asset_type == 'BRUSH':
+ if context.sculpt_object or context.image_paint_object:
+ props = utils.get_brush_props(context)
+ if props.asset_base_id != '':
+ layout.label(text=props.name + ' :')
+ # noinspection PyCallByClass
+ draw_panel_brush_ratings(self, context)
+ if ui_props.asset_type == 'TEXTURE':
+ layout.label(text='not yet implemented')
+
+
+class SetCategoryOperator(bpy.types.Operator):
+ """Visit subcategory"""
+ bl_idname = "view3d.blenderkit_set_category"
+ bl_label = "BlenderKit Set Active Category"
+
+ category: bpy.props.StringProperty(
+ name="Category",
+ description="set this category active",
+ default="")
+
+ asset_type: bpy.props.StringProperty(
+ name="Asset Type",
+ description="asset type",
+ default="")
+
+ @classmethod
+ def poll(cls, context):
+ return True
+
+ def execute(self, context):
+ acat = bpy.context.window_manager['active_category'][self.asset_type]
+ if self.category == '':
+ acat.remove(acat[-1])
+ else:
+ acat.append(self.category)
+ # we have to write back to wm. Thought this should happen with original list.
+ bpy.context.window_manager['active_category'][self.asset_type] = acat
+ return {'FINISHED'}
+
+
+def draw_panel_categories(self, context):
+ s = context.scene
+ ui_props = s.blenderkitUI
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ layout = self.layout
+ # row = layout.row()
+ # row.prop(ui_props, 'asset_type', expand=True, icon_only=True)
+ layout.separator()
+ layout.label(text='Categories')
+ wm = bpy.context.window_manager
+
+ col = layout.column(align=True)
+ if wm.get('active_category') is not None:
+ acat = wm['active_category'][ui_props.asset_type]
+ if len(acat) > 1:
+ # we are in subcategory, so draw the parent button
+ op = col.operator('view3d.blenderkit_set_category', text='...', icon='FILE_PARENT')
+ op.asset_type = ui_props.asset_type
+ op.category = ''
+ cats = categories.get_category(wm['bkit_categories'], cat_path=acat)
+ # draw freebies only in models parent category
+ if ui_props.asset_type == 'MODEL' and len(acat) == 1:
+ op = col.operator('view3d.blenderkit_asset_bar', text='freebies')
+ op.free_only = True
+
+ for c in cats['children']:
+ if c['assetCount'] > 0:
+ row = col.row(align=True)
+ if len(c['children']) > 0 and c['assetCount'] > 15:
+ row = row.split(factor=.8, align=True)
+ # row = split.split()
+ ctext = '%s (%i)' % (c['name'], c['assetCount'])
+ op = row.operator('view3d.blenderkit_asset_bar', text=ctext)
+ op.do_search = True
+ op.keep_running = True
+ op.category = c['slug']
+ # TODO enable subcategories, now not working due to some bug on server probably
+ if len(c['children']) > 0 and c['assetCount'] > 15:
+ # row = row.split()
+ op = row.operator('view3d.blenderkit_set_category', text='>>')
+ op.asset_type = ui_props.asset_type
+ op.category = c['slug']
+ # for c1 in c['children']:
+ # if c1['assetCount']>0:
+ # row = col.row()
+ # split = row.split(percentage=.2)
+ # row = split.split()
+ # row = split.split()
+ # ctext = '%s (%i)' % (c1['name'], c1['assetCount'])
+ # op = row.operator('view3d.blenderkit_search', text=ctext)
+ # op.category = c1['slug']
+
+
+class VIEW3D_PT_blenderkit_downloads(Panel):
+ bl_category = "BlenderKit"
+ bl_idname = "VIEW3D_PT_blenderkit_downloads"
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_label = "Downloads"
+
+ @classmethod
+ def poll(cls, context):
+ return len(download.download_threads) > 0
+
+ def draw(self, context):
+ layout = self.layout
+ for threaddata in download.download_threads:
+ tcom = threaddata[2]
+ asset_data = threaddata[1]
+ row = layout.row()
+ row.label(text=asset_data['name'])
+ row.label(text=str(int(tcom.progress)) + ' %')
+
+
+classess = (
+ SetCategoryOperator,
+
+ VIEW3D_PT_blenderkit_unified,
+ VIEW3D_PT_blenderkit_model_properties,
+ VIEW3D_PT_blenderkit_downloads,
+
+)
+
+
+def register_ui_panels():
+ for c in classess:
+ bpy.utils.register_class(c)
+
+
+def unregister_ui_panels():
+ for c in classess:
+ bpy.utils.unregister_class(c)
diff --git a/blenderkit/upload.py b/blenderkit/upload.py
new file mode 100644
index 00000000..3be1650a
--- /dev/null
+++ b/blenderkit/upload.py
@@ -0,0 +1,765 @@
+# ##### 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 #####
+
+if "bpy" in locals():
+ import imp
+
+ imp.reload(asset_inspector)
+ imp.reload(paths)
+ imp.reload(utils)
+ imp.reload(bg_blender)
+ imp.reload(autothumb)
+else:
+ from blenderkit import asset_inspector, paths, utils, bg_blender, autothumb, version_checker
+
+import tempfile, os, subprocess, json, re
+
+import bpy
+import requests
+
+BLENDERKIT_EXPORT_DATA_FILE = "data.json"
+
+from bpy.props import ( # TODO only keep the ones actually used when cleaning
+ EnumProperty,
+ BoolProperty,
+)
+from bpy.types import (
+ Operator,
+ Panel,
+ AddonPreferences,
+ PropertyGroup,
+ UIList
+)
+
+
+def comma2array(text):
+ commasep = text.split(',')
+ ar = []
+ for i, s in enumerate(commasep):
+ s = s.strip()
+ if s != '':
+ ar.append(s)
+ return ar
+
+
+def get_app_version():
+ ver = bpy.app.version
+ return '%i.%i.%i' % (ver[0], ver[1], ver[2])
+
+
+def add_version(data):
+ app_version = get_app_version()
+ addon_version = version_checker.get_addon_version()
+ data["sourceAppName"] = "blender"
+ data["sourceAppVersion"] = app_version
+ data["addonVersion"] = addon_version
+
+
+def params_to_dict(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 write_to_report(props, text):
+ props.report = props.report + text + '\n'
+
+
+def get_missing_data_model(props):
+ props.report = ''
+ autothumb.update_upload_model_preview(None, None)
+
+ if props.name == '':
+ write_to_report(props, 'Set model name')
+ if props.tags == '':
+ write_to_report(props, 'Write at least 3 tags')
+ if not props.has_thumbnail:
+ write_to_report(props, 'Add thumbnail:')
+
+ props.report += props.thumbnail_generating_state + '\n'
+ if props.work_hours == 0.0:
+ write_to_report(props, 'Fill in work hours')
+ if props.engine == 'NONE':
+ write_to_report(props, 'Set at least one rendering/output engine')
+ if not any(props.dimensions):
+ write_to_report(props, 'Run autotags operator or fill in dimensions manually')
+
+
+def get_missing_data_scene(props):
+ props.report = ''
+ autothumb.update_upload_model_preview(None, None)
+
+ if props.name == '':
+ write_to_report(props, 'Set scene name')
+ if props.tags == '':
+ write_to_report(props, 'Write at least 3 tags')
+ if not props.has_thumbnail:
+ write_to_report(props, 'Add thumbnail:')
+
+ props.report += props.thumbnail_generating_state + '\n'
+ if props.work_hours == 0.0:
+ write_to_report(props, 'Fill in work hours')
+ if props.engine == 'NONE':
+ write_to_report(props, 'Set at least one rendering/output engine')
+
+
+def get_missing_data_material(props):
+ props.report = ''
+ autothumb.update_upload_material_preview(None, None)
+ if props.name == '':
+ write_to_report(props, 'Set material name')
+ if props.tags == '':
+ write_to_report(props, 'Write at least 3 tags')
+ if not props.has_thumbnail:
+ write_to_report(props, 'Add thumbnail:')
+ props.report += props.thumbnail_generating_state
+ if props.engine == 'NONE':
+ write_to_report(props, 'Set rendering/output engine')
+
+
+def get_missing_data_brush(props):
+ autothumb.update_upload_brush_preview(None, None)
+ props.report = ''
+ if props.name == '':
+ write_to_report(props, 'Set brush name')
+ if props.tags == '':
+ write_to_report(props, 'Write at least 3 tags')
+ if not props.has_thumbnail:
+ write_to_report(props, 'Add thumbnail:')
+ props.report += props.thumbnail_generating_state
+
+
+def sub_to_camel(content):
+ replaced = re.sub(r"_.",
+ lambda m: m.group(0)[1].upper(), content)
+ return (replaced)
+
+
+def camel_to_sub(content):
+ replaced = re.sub(r"[A-Z]", lambda m: '_' + m.group(0).lower(), content)
+ return replaced
+
+
+def get_upload_data(self, context, asset_type):
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ api_key = user_preferences.api_key
+
+ export_data = {
+ "type": asset_type,
+ }
+ upload_params = {}
+ if asset_type == 'MODEL':
+ # Prepare to save the file
+ mainmodel = utils.get_active_model()
+
+ props = mainmodel.blenderkit
+
+ obs = utils.get_hierarchy(mainmodel)
+ obnames = []
+ for ob in obs:
+ obnames.append(ob.name)
+ export_data["models"] = obnames
+ export_data["thumbnail_path"] = bpy.path.abspath(props.thumbnail)
+
+ eval_path_computing = "bpy.data.objects['%s'].blenderkit.uploading" % mainmodel.name
+ eval_path_state = "bpy.data.objects['%s'].blenderkit.upload_state" % mainmodel.name
+ eval_path = "bpy.data.objects['%s']" % mainmodel.name
+
+ engines = [props.engine.lower()]
+ if props.engine1 != 'NONE':
+ engines.append(props.engine1.lower())
+ if props.engine2 != 'NONE':
+ engines.append(props.engine2.lower())
+ if props.engine3 != 'NONE':
+ engines.append(props.engine3.lower())
+ if props.engine == 'OTHER':
+ engines.append(props.engine_other.lower())
+
+ style = props.style.lower()
+ if style == 'OTHER':
+ style = props.style_other.lower()
+
+ pl_dict = {'FINISHED': 'finished', 'TEMPLATE': 'template'}
+
+ upload_data = {
+ "assetType": 'model',
+
+ }
+ upload_params = {
+ "productionLevel": props.production_level.lower(),
+ "model_style": style,
+ "engines": engines,
+ "modifiers": comma2array(props.modifiers),
+ "materials": comma2array(props.materials),
+ "shaders": comma2array(props.shaders),
+ "uv": props.uv,
+ "dimensionX": round(props.dimensions[0], 4),
+ "dimensionY": round(props.dimensions[1], 4),
+ "dimensionZ": round(props.dimensions[2], 4),
+
+ "boundBoxMinX": round(props.bbox_min[0], 4),
+ "boundBoxMinY": round(props.bbox_min[1], 4),
+ "boundBoxMinZ": round(props.bbox_min[2], 4),
+
+ "boundBoxMaxX": round(props.bbox_max[0], 4),
+ "boundBoxMaxY": round(props.bbox_max[1], 4),
+ "boundBoxMaxZ": round(props.bbox_max[2], 4),
+
+ "animated": props.animated,
+ "rig": props.rig,
+ "simulation": props.simulation,
+ "purePbr": props.pbr,
+ "faceCount": props.face_count,
+ "faceCountRender": props.face_count_render,
+ "manifold": props.manifold,
+ "objectCount": props.object_count,
+
+ # "scene": props.is_scene,
+ }
+ if props.use_design_year:
+ upload_params["designYear"] = props.design_year
+ if props.condition != 'UNSPECIFIED':
+ upload_params["condition"] = props.condition.lower()
+ if props.pbr:
+ pt = props.pbr_type
+ pt = pt.lower()
+ upload_params["pbrType"] = pt
+
+ if props.texture_resolution_max > 0:
+ upload_params["textureResolutionMax"] = props.texture_resolution_max
+ upload_params["textureResolutionMin"] = props.texture_resolution_min
+ if props.mesh_poly_type != 'OTHER':
+ upload_params["meshPolyType"] = props.mesh_poly_type.lower() # .replace('_',' ')
+
+ optional_params = ['manufacturer', 'designer', 'design_collection', 'design_variant']
+ for p in optional_params:
+ if eval('props.%s' % p) != '':
+ upload_params[sub_to_camel(p)] = eval('props.%s' % p)
+
+ if asset_type == 'SCENE':
+ # Prepare to save the file
+ s = bpy.context.scene
+
+ props = s.blenderkit
+
+ export_data["scene"] = s.name
+ export_data["thumbnail_path"] = bpy.path.abspath(props.thumbnail)
+
+ eval_path_computing = "bpy.data.scenes['%s'].blenderkit.uploading" % s.name
+ eval_path_state = "bpy.data.scenes['%s'].blenderkit.upload_state" % s.name
+ eval_path = "bpy.data.scenes['%s']" % s.name
+
+ engines = [props.engine.lower()]
+ if props.engine1 != 'NONE':
+ engines.append(props.engine1.lower())
+ if props.engine2 != 'NONE':
+ engines.append(props.engine2.lower())
+ if props.engine3 != 'NONE':
+ engines.append(props.engine3.lower())
+ if props.engine == 'OTHER':
+ engines.append(props.engine_other.lower())
+
+ style = props.style.lower()
+ if style == 'OTHER':
+ style = props.style_other.lower()
+
+ pl_dict = {'FINISHED': 'finished', 'TEMPLATE': 'template'}
+
+ upload_data = {
+ "assetType": 'scene',
+
+ }
+ upload_params = {
+ "productionLevel": props.production_level.lower(),
+ "model_style": style,
+ "engines": engines,
+ "modifiers": comma2array(props.modifiers),
+ "materials": comma2array(props.materials),
+ "shaders": comma2array(props.shaders),
+ "uv": props.uv,
+
+ "animated": props.animated,
+ # "simulation": props.simulation,
+ "purePbr": props.pbr,
+ "faceCount": 1, # props.face_count,
+ "faceCountRender": 1, # props.face_count_render,
+ "objectCount": 1, # props.object_count,
+
+ # "scene": props.is_scene,
+ }
+ if props.use_design_year:
+ upload_params["designYear"] = props.design_year
+ if props.condition != 'UNSPECIFIED':
+ upload_params["condition"] = props.condition.lower()
+ if props.pbr:
+ pt = props.pbr_type
+ pt = pt.lower()
+ upload_params["pbrType"] = pt
+
+ if props.texture_resolution_max > 0:
+ upload_params["textureResolutionMax"] = props.texture_resolution_max
+ upload_params["textureResolutionMin"] = props.texture_resolution_min
+ if props.mesh_poly_type != 'OTHER':
+ upload_params["meshPolyType"] = props.mesh_poly_type.lower() # .replace('_',' ')
+
+ elif asset_type == 'MATERIAL':
+ mat = bpy.context.active_object.active_material
+ props = mat.blenderkit
+
+ # props.name = mat.name
+
+ export_data["material"] = str(mat.name)
+ export_data["thumbnail_path"] = bpy.path.abspath(props.thumbnail)
+ # mat analytics happen here, since they don't take up any time...
+ asset_inspector.check_material(props, mat)
+
+ eval_path_computing = "bpy.data.materials['%s'].blenderkit.uploading" % mat.name
+ eval_path_state = "bpy.data.materials['%s'].blenderkit.upload_state" % mat.name
+ eval_path = "bpy.data.materials['%s']" % mat.name
+
+ engine = props.engine
+ if engine == 'OTHER':
+ engine = props.engine_other
+ engine = engine.lower()
+ style = props.style.lower()
+ if style == 'OTHER':
+ style = props.style_other.lower()
+
+ upload_data = {
+ "assetType": 'material',
+
+ }
+
+ upload_params = {
+ "material_style": style,
+ "engine": engine,
+ "shaders": comma2array(props.shaders),
+ "uv": props.uv,
+ "animated": props.animated,
+ "purePbr": props.pbr,
+ "textureSizeMeters": props.texture_size_meters,
+
+ }
+
+ if props.pbr:
+ upload_params["pbrType"] = props.pbr_type.lower()
+
+ if props.texture_resolution_max > 0:
+ upload_params["textureResolutionMax"] = props.texture_resolution_max
+ upload_params["textureResolutionMin"] = props.texture_resolution_min
+
+ elif asset_type == 'BRUSH':
+ brush = utils.get_active_brush()
+
+ props = brush.blenderkit
+ # props.name = brush.name
+
+ export_data["brush"] = str(brush.name)
+ export_data["thumbnail_path"] = bpy.path.abspath(brush.icon_filepath)
+
+ eval_path_computing = "bpy.data.brushes['%s'].blenderkit.uploading" % brush.name
+ eval_path_state = "bpy.data.brushes['%s'].blenderkit.upload_state" % brush.name
+ eval_path = "bpy.data.brushes['%s']" % brush.name
+
+ # mat analytics happen here, since they don't take up any time...
+
+ brush_type = ''
+ if bpy.context.sculpt_object is not None:
+ brush_type = 'sculpt'
+
+ elif bpy.context.image_paint_object: # could be just else, but for future p
+ brush_type = 'texture_paint'
+
+ upload_params = {
+ "mode": brush_type,
+ }
+
+ upload_data = {
+ "assetType": 'brush',
+ }
+
+ elif asset_type == 'TEXTURE':
+ style = props.style
+ if style == 'OTHER':
+ style = props.style_other
+
+ upload_data = {
+ "assetType": 'texture',
+
+ }
+ upload_params = {
+ "style": style,
+ "animated": props.animated,
+ "purePbr": props.pbr,
+ "resolution": props.resolution,
+ }
+ if props.pbr:
+ pt = props.pbr_type
+ pt = pt.lower()
+ upload_data["pbrType"] = pt
+
+ add_version(upload_data)
+
+ upload_data["name"] = props.name
+ upload_data["description"] = props.description
+ upload_data["tags"] = comma2array(props.tags)
+ if props.category == '':
+ upload_data["category"] = asset_type.lower()
+ else:
+ upload_data["category"] = props.category
+ if props.subcategory != '':
+ upload_data["category"] = props.subcategory
+ upload_data["license"] = props.license
+ upload_data["isFree"] = props.is_free
+ upload_data["token"] = user_preferences.api_key
+
+ if props.asset_base_id != '':
+ upload_data['assetBaseId'] = props.asset_base_id
+ upload_data['id'] = props.id
+
+ upload_data['parameters'] = upload_params
+
+ return export_data, upload_data, eval_path_computing, eval_path_state, eval_path, props
+
+
+def mark_for_validation(self, context, asset_type):
+ props = utils.get_upload_props()
+ props.upload_state = 'marking for validation'
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+
+ upload_data = {
+ "verificationStatus": "ready"
+ }
+
+ url = paths.get_bkit_url() + 'assets/'
+ headers = {"accept": "application/json", "Authorization": "Bearer %s" % user_preferences.api_key}
+ url += props.id + '/'
+
+ try:
+ r = requests.patch(url, json=upload_data, headers=headers, verify=True) # files = files,
+ props.upload_state = 'marked for validation'
+ except requests.exceptions.RequestException as e:
+ props.upload_state = str(e)
+ props.uploading = False
+ return {'CANCELLED'}
+ return {'FINISHED'}
+
+
+def get_upload_location(props):
+ scene = bpy.context.scene
+ ui_props = scene.blenderkitUI
+ if ui_props.asset_type == 'MODEL':
+ if bpy.context.active_object is not None:
+ ob = utils.get_active_model()
+ return ob.location
+ if ui_props.asset_type == 'SCENE':
+ return None
+ elif ui_props.asset_type == 'MATERIAL':
+ if bpy.context.active_object is not None and bpy.context.active_object.active_material is not None:
+ return bpy.context.active_object.location
+ elif ui_props.asset_type == 'TEXTURE':
+ return None
+ elif ui_props.asset_type == 'BRUSH':
+ return None
+ return None
+
+
+def start_upload(self, context, asset_type, as_new, metadata_only):
+ props = utils.get_upload_props()
+ location = get_upload_location(props)
+ props.upload_state = 'preparing upload'
+ # do this for fixing long tags in some upload cases
+ props.tags = props.tags[:]
+
+ props.name = props.name.strip()
+ # TODO move this to separate function
+ # check for missing metadata
+ if asset_type == 'MODEL':
+ get_missing_data_model(props)
+ if asset_type == 'SCENE':
+ get_missing_data_scene(props)
+ elif asset_type == 'MATERIAL':
+ get_missing_data_material(props)
+ elif asset_type == 'BRUSH':
+ get_missing_data_brush(props)
+
+ if props.report != '':
+ self.report({'ERROR_INVALID_INPUT'}, props.report)
+ return {'CANCELLED'}
+ if as_new:
+ props.asset_base_id = ''
+ props.id = ''
+ export_data, upload_data, eval_path_computing, eval_path_state, eval_path, props = get_upload_data(self, context,
+ asset_type)
+ # utils.pprint(upload_data)
+ upload_data['parameters'] = params_to_dict(
+ upload_data['parameters']) # weird array conversion only for upload, not for tooltips.
+
+ binary_path = bpy.app.binary_path
+ script_path = os.path.dirname(os.path.realpath(__file__))
+ basename, ext = os.path.splitext(bpy.data.filepath)
+ # if not basename:
+ # basename = os.path.join(basename, "temp")
+ if not ext:
+ ext = ".blend"
+ tempdir = tempfile.mkdtemp()
+ source_filepath = os.path.join(tempdir, "export_blenderkit" + ext)
+ clean_file_path = paths.get_clean_filepath()
+ data = {
+ 'clean_file_path': clean_file_path,
+ 'source_filepath': source_filepath,
+ 'temp_dir': tempdir,
+ 'export_data': export_data,
+ 'upload_data': upload_data,
+ 'debug_value': bpy.app.debug_value,
+ }
+ datafile = os.path.join(tempdir, BLENDERKIT_EXPORT_DATA_FILE)
+
+ # check if thumbnail exists:
+
+ if not os.path.exists(export_data["thumbnail_path"]):
+ props.upload_state = 'Thumbnail not found'
+ props.uploading = False
+ return {'CANCELLED'}
+
+ # first upload metadata to server, so it can be saved inside the current file
+ url = paths.get_bkit_url() + 'assets/'
+ headers = {"accept": "application/json", "Authorization": "Bearer %s" % upload_data['token']}
+ # upload_data['license'] = 'ovejajojo'
+ json_metadata = upload_data # json.dumps(upload_data, ensure_ascii=False).encode('utf8')
+ global reports
+ if props.asset_base_id == '':
+ try:
+ r = requests.post(url, json=json_metadata, headers=headers, verify=True) # files = files,
+ props.upload_state = 'uploaded metadata'
+ except requests.exceptions.RequestException as e:
+ print(e)
+ props.upload_state = str(e)
+ props.uploading = False
+ return {'CANCELLED'}
+
+ else:
+ url += props.id + '/'
+ try:
+ if not metadata_only:
+ json_metadata["verificationStatus"] = "uploading"
+ r = requests.put(url, json=json_metadata, headers=headers, verify=True) # files = files,
+ props.upload_state = 'uploaded metadata'
+ # parse the reqest
+ # print('uploaded metadata')
+ # print(r.text)
+ except requests.exceptions.RequestException as e:
+ print(e)
+ props.upload_state = str(e)
+ props.uploading = False
+ return {'CANCELLED'}
+
+ # props.upload_state = 'step 1'
+ if metadata_only:
+ props.uploading = False
+
+ return {'FINISHED'}
+ try:
+ rj = r.json()
+ if props.asset_base_id == '':
+ props.asset_base_id = rj['assetBaseId']
+ props.id = rj['id']
+
+ upload_data['assetBaseId'] = props.asset_base_id
+ upload_data['id'] = props.id
+
+ bpy.ops.wm.save_mainfile()
+ # fa
+ props.uploading = True
+ # save a copy of actual scene but don't interfere with the users models
+ bpy.ops.wm.save_as_mainfile(filepath=source_filepath, compress=False, copy=True)
+
+ with open(datafile, 'w') as s:
+ json.dump(data, s)
+
+ proc = subprocess.Popen([
+ binary_path,
+ "--background",
+ "-noaudio",
+ clean_file_path,
+ "--python", os.path.join(script_path, "upload_bg.py"),
+ "--", datafile # ,filepath, tempdir
+ ], bufsize=5000, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
+
+ bg_blender.add_bg_process(eval_path_computing=eval_path_computing, eval_path_state=eval_path_state,
+ eval_path=eval_path, process_type='UPLOAD', process=proc, location=location)
+
+ except Exception as e:
+ props.upload_state = str(e)
+ props.uploading = False
+ return {'CANCELLED'}
+
+ return {'FINISHED'}
+
+
+asset_types = (
+ ('MODEL', 'Model', 'set of objects'),
+ ('SCENE', 'Scene', 'scene'),
+ ('MATERIAL', 'Material', 'any .blend Material'),
+ ('TEXTURE', 'Texture', 'a texture, or texture set'),
+ ('BRUSH', 'Brush', 'brush, can be any type of blender brush'),
+ ('ADDON', 'Addon', 'addnon'),
+)
+
+
+class ModelUploadOperator(Operator):
+ """Tooltip"""
+ bl_idname = "object.blenderkit_upload"
+ bl_description = "Upload or re-upload asset + thumbnail + metadata"
+
+ bl_label = "BlenderKit asset upload"
+
+ # type of upload - model, material, textures, e.t.c.
+ asset_type: EnumProperty(
+ name="Type",
+ items=asset_types,
+ description="Type of upload",
+ default="MODEL",
+ )
+
+ as_new: BoolProperty(
+ name="upload as new",
+ description="delets asset id and uploads as new file",
+ default=False,
+ options={'SKIP_SAVE'}
+ )
+
+ metadata_only: BoolProperty(
+ name="upload metadata",
+ description="update only metadata",
+ default=False,
+ options={'SKIP_SAVE'}
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return bpy.context.active_object is not None
+
+ def execute(self, context):
+ bpy.ops.object.blenderkit_auto_tags()
+ props = utils.get_upload_props()
+
+ # in case of name change, we have to reupload everything, since the name is stored in blender file,
+ # and is used for linking to scene
+ metadata_only = self.metadata_only
+ if props.name_changed:
+ print('has to reupload whole data, name has changed.')
+ self.metadata_only = False
+ props.name_changed = False
+
+ result = start_upload(self, context, self.asset_type, self.as_new, self.metadata_only)
+
+ return result
+
+ def draw(self, context):
+ layout = self.layout
+ layout.label(text="Really upload as new? ")
+ layout.label(text="Do this only when you create a new asset from an old one.")
+ layout.label(text="For updates of thumbnail or model use reupload.")
+
+ def invoke(self, context, event):
+ if self.as_new:
+ return context.window_manager.invoke_props_dialog(self)
+ else:
+ return self.execute(context)
+
+
+class ModelMarkForValidation(Operator):
+ """Tooltip"""
+ bl_idname = "object.blenderkit_mark_for_validation"
+ bl_description = "After this, model can be validated by our validators and \n then be part of the public database"
+
+ bl_label = "Mark for validation"
+
+ # type of upload - model, material, textures, e.t.c.
+ asset_type: EnumProperty(
+ name="type",
+ items=asset_types,
+ description="Type of upload",
+ default="MODEL",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ props = utils.get_upload_props()
+ return bpy.context.active_object is not None and props.asset_base_id != ''
+
+ def execute(self, context):
+ result = mark_for_validation(self, context, self.asset_type)
+
+ return result
+
+
+# TODO this is for upldating by creators, if they edit metadata on server or want to upload from older file version.
+# class GetMetadataFromServer(Operator):
+# """Tooltip"""
+# bl_idname = "object.blenderkit_get_from_server"
+# bl_description ="After this, model can be validated by our validators and \n then be part of the public database"
+#
+# bl_label = "Mark for validation"
+#
+# # type of upload - model, material, textures, e.t.c.
+# asset_type : EnumProperty(
+# name="type",
+# items=asset_types,
+# description="Type of upload",
+# default="MODEL",
+# )
+#
+# @classmethod
+# def poll(cls, context):
+# props = utils.get_upload_props()
+# return bpy.context.active_object is not None and props.asset_base_id != ''
+#
+# def execute(self, context):
+# result = mark_for_validation(self, context, self.asset_type)
+#
+# return result
+
+
+def register_upload():
+ bpy.utils.register_class(ModelUploadOperator)
+ bpy.utils.register_class(ModelMarkForValidation)
+
+
+def unregister_upload():
+ bpy.utils.unregister_class(ModelUploadOperator)
+ bpy.utils.unregister_class(ModelMarkForValidation)
diff --git a/blenderkit/upload_bg.py b/blenderkit/upload_bg.py
new file mode 100644
index 00000000..60ee4690
--- /dev/null
+++ b/blenderkit/upload_bg.py
@@ -0,0 +1,199 @@
+# ##### 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 #####
+
+import sys, json, os, time
+import requests
+import logging
+
+import bpy
+import addon_utils
+from blenderkit import paths, append_link, bg_blender
+
+BLENDERKIT_EXPORT_DATA = sys.argv[-1]
+
+
+def start_logging():
+ logging.basicConfig()
+ logging.getLogger().setLevel(logging.DEBUG)
+ requests_log = logging.getLogger("requests.packages.urllib3")
+ requests_log.setLevel(logging.DEBUG)
+ requests_log.propagate = True
+
+
+def print_gap():
+ print('\n\n\n\n')
+
+
+class upload_in_chunks(object):
+ def __init__(self, filename, chunksize=1 << 13, report_name='file'):
+ self.filename = filename
+ self.chunksize = chunksize
+ self.totalsize = os.path.getsize(filename)
+ self.readsofar = 0
+ self.report_name = report_name
+
+ def __iter__(self):
+ with open(self.filename, 'rb') as file:
+ while True:
+ data = file.read(self.chunksize)
+ if not data:
+ sys.stderr.write("\n")
+ break
+ self.readsofar += len(data)
+ percent = self.readsofar * 1e2 / self.totalsize
+ bg_blender.progress('uploading %s' % self.report_name, percent)
+ # sys.stderr.write("\r{percent:3.0f}%".format(percent=percent))
+ yield data
+
+ def __len__(self):
+ return self.totalsize
+
+
+def upload_files(filepath, upload_data, files):
+ headers = {"accept": "application/json", "Authorization": "Bearer %s" % upload_data['token']}
+ version_id = upload_data['id']
+ for f in files:
+ bg_blender.progress('uploading %s' % f['type'])
+ upload_info = {
+ 'assetId': version_id,
+ 'fileType': f['type'],
+ 'fileIndex': f['index'],
+ 'originalFilename': os.path.basename(f['file_path'])
+ }
+ upload_create_url = paths.get_bkit_url() + 'uploads/'
+ upload = requests.post(upload_create_url, json=upload_info, headers=headers, verify=True)
+ upload = upload.json()
+
+ upheaders = {
+ "accept": "application/json",
+ "Authorization": "Bearer %s" % upload_data['token'],
+ "Content-Type": "multipart/form-data",
+ "Content-Disposition": 'form-data; name="file"; filename=%s' % f['file_path']
+
+ }
+ chunk_size = 1024 * 256
+
+ # file gets uploaded here:
+ uploaded = False
+ # s3 upload is now the only option
+ for a in range(0, 20):
+ if not uploaded:
+ try:
+ upload_response = requests.put(upload['s3UploadUrl'],
+ data=upload_in_chunks(f['file_path'], chunk_size, f['type']),
+ stream=True, verify=True)
+ print('upload response')
+ print(upload_response.text)
+ uploaded = True
+ except Exception as e:
+ bg_blender.progress('Upload %s failed, retrying' % f['type'])
+ time.sleep(1)
+
+ # confirm single file upload to bkit server
+ upload_done_url = paths.get_bkit_url() + 'uploads_s3/' + upload['id'] + '/upload-file/'
+ upload_response = requests.post(upload_done_url, headers=headers, verify=True)
+
+ bg_blender.progress('finished uploading')
+
+ return {'FINISHED'}
+
+
+if __name__ == "__main__":
+
+ bpy.data.scenes.new('upload')
+ for s in bpy.data.scenes:
+ if s.name != 'upload':
+ bpy.data.scenes.remove(s)
+ try:
+ # bg_blender.progress('preparing scene')
+ bg_blender.progress('preparing scene - link objects')
+ with open(BLENDERKIT_EXPORT_DATA, 'r') as s:
+ data = json.load(s)
+
+ bpy.app.debug_value = data.get('debug_value', 0)
+ export_data = data['export_data']
+ upload_data = data['upload_data']
+
+ if export_data['type'] == 'MODEL':
+ obnames = export_data['models']
+ main_source, allobs = append_link.append_objects(file_name=data['source_filepath'],
+ obnames=obnames,
+ rotation=(0, 0, 0))
+ g = bpy.data.collections.new(upload_data['name'])
+ for o in allobs:
+ g.objects.link(o)
+ bpy.context.scene.collection.children.link(g)
+ if export_data['type'] == 'SCENE':
+ sname = export_data['scene']
+ main_source = append_link.append_scene(file_name=data['source_filepath'],
+ scenename=sname)
+ bpy.data.scenes.remove(bpy.data.scenes['upload'])
+ main_source.name = sname
+ elif export_data['type'] == 'MATERIAL':
+ matname = export_data['material']
+ main_source = append_link.append_material(file_name=data['source_filepath'], matname=matname)
+
+ elif export_data['type'] == 'BRUSH':
+ brushname = export_data['brush']
+ main_source = append_link.append_brush(file_name=data['source_filepath'], brushname=brushname)
+
+ bpy.ops.file.pack_all()
+
+ # TODO fetch asset_id here
+ asset_id = main_source.blenderkit.asset_base_id
+ main_source.blenderkit.uploading = False
+
+ fpath = os.path.join(data['temp_dir'], asset_id + '.blend')
+
+ bpy.ops.wm.save_as_mainfile(filepath=fpath, compress=True, copy=False)
+ os.remove(data['source_filepath'])
+
+ bg_blender.progress('preparing scene - open files')
+
+ files = [{
+ "type": "thumbnail",
+ "index": 0,
+ "file_path": export_data["thumbnail_path"]
+ }, {
+ "type": "blend",
+ "index": 0,
+ "file_path": fpath
+ }]
+
+ bg_blender.progress('uploading')
+
+ upload_files(fpath, upload_data, files)
+
+ # mark on server as uploaded
+ confirm_data = {
+ "verificationStatus": "uploaded"
+ }
+
+ url = paths.get_bkit_url() + 'assets/'
+ headers = {"accept": "application/json", "Authorization": "Bearer %s" % upload_data['token']}
+ url += upload_data["id"] + '/'
+
+ r = requests.patch(url, json=confirm_data, headers=headers, verify=True) # files = files,
+
+ bg_blender.progress('upload finished successfully')
+
+
+ except Exception as e:
+ print(e)
+ bg_blender.progress(e)
+ sys.exit(1)
diff --git a/blenderkit/utils.py b/blenderkit/utils.py
new file mode 100644
index 00000000..39c55f33
--- /dev/null
+++ b/blenderkit/utils.py
@@ -0,0 +1,415 @@
+# ##### 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 #####
+
+if "bpy" in locals():
+ import imp
+
+ imp.reload(paths)
+else:
+ from blenderkit import paths, categories
+import bpy
+from mathutils import Vector
+import json
+import os
+import requests
+
+
+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.active_object
+ selobs = bpy.context.selected_objects
+ 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 hasattr(bpy.context, 'active_object'):
+ ob = bpy.context.active_object
+ while ob.parent is not None:
+ ob = ob.parent
+ return ob
+ return None
+
+
+def get_selected_models():
+ 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:
+ done[ob] = True
+ ob = ob.parent
+
+ if ob not in parents and ob not in done:
+ if ob.blenderkit.name != '':
+ parents.append(ob)
+ done[ob] = True
+ return parents
+
+
+def get_search_props():
+ scene = bpy.context.scene
+ 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 == '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.active_object is not None:
+ ob = get_active_model()
+ return ob
+ if ui_props.asset_type == 'SCENE':
+ return bpy.context.scene
+
+ elif ui_props.asset_type == 'MATERIAL':
+ if bpy.context.active_object 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.active_object is not None:
+ ob = get_active_model()
+ return ob.blenderkit
+ if ui_props.asset_type == 'SCENE':
+ s = bpy.context.scene
+ return s.blenderkit
+ elif ui_props.asset_type == 'MATERIAL':
+ if bpy.context.active_object 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(2)
+ else:
+ return '.bkit_preview_full_' + str(index).zfill(2)
+
+
+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') as s:
+ prefs = json.load(s)
+ user_preferences.api_key = prefs['API_key']
+ user_preferences.global_dir = prefs['global_dir']
+
+
+def save_prefs(self, context):
+ user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
+ if user_preferences.api_key != '':
+ prefs = {
+ 'API_key': user_preferences.api_key,
+ 'global_dir': user_preferences.global_dir,
+ }
+ # user_preferences.api_key = user_preferences.api_key.strip()
+ fpath = paths.BLENDERKIT_SETTINGS_FILENAME
+ f = open(fpath, 'w')
+ with open(fpath, 'w') as s:
+ json.dump(prefs, s)
+
+
+def load_categories():
+ categories.copy_categories()
+ tempdir = paths.get_temp_dir()
+ categories_filepath = os.path.join(tempdir, 'categories.json')
+
+ wm = bpy.context.window_manager
+ with open(categories_filepath, 'r') as catfile:
+ wm['bkit_categories'] = json.load(catfile)
+
+ wm['active_category'] = {
+ 'MODEL': ['model'],
+ 'SCENE': ['scene'],
+ 'MATERIAL': ['material'],
+ 'BRUSH': ['brush'],
+ }
+
+
+def get_hidden_image(tpath, bdata_name, force_reload=False):
+ hidden_name = '.%s' % bdata_name
+ img = bpy.data.images.get(hidden_name)
+
+ if tpath.startswith('//'):
+ tpath = bpy.path.abspath(tpath)
+
+ gap = '\n\n\n'
+ en = '\n'
+ if img == None or (img.filepath != tpath):
+ if tpath.startswith('//'):
+ tpath = bpy.path.abspath(tpath)
+ if not os.path.exists(tpath) or 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()
+ elif force_reload:
+ if img.packed_file is not None:
+ img.unpack(method='USE_ORIGINAL')
+ img.reload()
+ 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)
+ img.name = name
+ img.name = name
+
+ return img
+
+
+def get_brush_props(context):
+ brush = get_active_brush()
+ if brush is not None:
+ return brush.blenderkit
+ return None
+
+
+def pprint(data):
+ print(json.dumps(data, indent=4, sort_keys=True))
+
+
+def get_hierarchy(ob):
+ obs = []
+ doobs = [ob]
+ while len(doobs) > 0:
+ o = doobs.pop()
+ doobs.extend(o.children)
+ obs.append(o)
+ return 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.
+ mesh = ob.to_mesh(depsgraph=bpy.context.depsgraph, apply_modifiers=True, calc_undeformed=False)
+
+ # to_mesh(context.depsgraph, apply_modifiers=self.applyModifiers, calc_undeformed=False)
+ obcount += 1
+ 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 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':
+ mesh = ob.to_mesh(depsgraph=bpy.context.depsgraph, apply_modifiers=True, calc_undeformed=False)
+ obcount += 1
+ 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 = requests.post(url, json=json, verify=True, headers=headers)
+
+
+# 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):
+ from blenderkit import bg_blender as bg
+ 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':
+ 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
+
+ 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
+ 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 cube :)
+ bpy.ops.object.editmode_toggle()
+ tob.data.uv_layers.active = tob.data.uv_layers['automap']
+ # tob.data.uv_textures["automap"].active_render = True
+
+ bpy.context.view_layer.objects.active = actob
diff --git a/blenderkit/version_checker.py b/blenderkit/version_checker.py
new file mode 100644
index 00000000..ad5e23a5
--- /dev/null
+++ b/blenderkit/version_checker.py
@@ -0,0 +1,73 @@
+# ##### 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 #####
+
+import requests, os, json, threading
+from blenderkit import paths
+
+
+def get_addon_version():
+ import blenderkit
+ ver = blenderkit.bl_info['version']
+ return '%i.%i.%i' % (ver[0], ver[1], ver[2])
+
+
+def check_version(url, api_key, module):
+ headers = {
+ "accept": "application/json",
+ "Authorization": "Bearer %s" % api_key}
+
+ print('checking online version of module %s' % str(module.bl_info['name']))
+ try:
+ r = requests.get(url, headers=headers)
+ data = r.json()
+ ver_online = {
+ 'addonVersion2.8': data['addonVersion']
+ }
+ tempdir = paths.get_temp_dir()
+
+ ver_filepath = os.path.join(tempdir, 'addon_version.json')
+ with open(ver_filepath, 'w') as s:
+ json.dump(ver_online, s, indent=4)
+ except:
+ print("couldn't check online for version updates")
+
+
+def compare_versions(module):
+ try:
+ ver_local = module.bl_info['version']
+ ver_local_float = ver_local[0] + .01 * ver_local[1] + .0001 * ver_local[2]
+
+ tempdir = paths.get_temp_dir()
+ ver_filepath = os.path.join(tempdir, 'addon_version.json')
+ with open(ver_filepath, 'r') as s:
+ data = json.load(s)
+
+ ver_online = data['addonVersion2.8'].split('.')
+ ver_online_float = int(ver_online[0]) + .01 * int(ver_online[1]) + .0001 * int(ver_online[2])
+
+ print('versions: installed-%s, online-%s' % (str(ver_local_float), str(ver_online_float)))
+ if ver_online_float > ver_local_float:
+ return True
+ except:
+ print("couldn't compare addon versions")
+ return False
+
+
+def check_version_thread(url, API_key, module):
+ thread = threading.Thread(target=check_version, args=([url, API_key, module]), daemon=True)
+ thread.start()