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 /blenderkit
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.
Diffstat (limited to 'blenderkit')
-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()