diff options
72 files changed, 0 insertions, 26440 deletions
diff --git a/blenderkit/README.md b/blenderkit/README.md deleted file mode 100644 index e2e77067..00000000 --- a/blenderkit/README.md +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 173b6c85..00000000 --- a/blenderkit/__init__.py +++ /dev/null @@ -1,1977 +0,0 @@ -# ##### 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": "BlenderKit Online Asset Library", - "author": "Vilem Duha, Petr Dlouhy", - "version": (3, 0, 0), - "blender": (2, 93, 0), - "location": "View3D > Properties > BlenderKit", - "description": "Online BlenderKit library (materials, models, brushes and more). Connects to the internet.", - "warning": "", - "doc_url": "{BLENDER_MANUAL_URL}/addons/3d_view/blenderkit.html", - "category": "3D View", -} - -if "bpy" in locals(): - from importlib import reload - - # alphabetically sorted all add-on modules since reload only happens from __init__. - # modules with _bg are used for background computations in separate blender instance and that's why they don't need reload. - - append_link = reload(append_link) - asset_bar_op = reload(asset_bar_op) - asset_inspector = reload(asset_inspector) - autothumb = reload(autothumb) - bg_blender = reload(bg_blender) - bkit_oauth = reload(bkit_oauth) - categories = reload(categories) - colors = reload(colors) - download = reload(download) - icons = reload(icons) - image_utils = reload(image_utils) - oauth = reload(oauth) - overrides = reload(overrides) - paths = reload(paths) - ratings = reload(ratings) - ratings_utils = reload(ratings_utils) - comments_utils = reload(comments_utils) - resolutions = reload(resolutions) - search = reload(search) - tasks_queue = reload(tasks_queue) - ui = reload(ui) - ui_bgl = reload(ui_bgl) - ui_panels = reload(ui_panels) - upload = reload(upload) - upload_bg = reload(upload_bg) - utils = reload(utils) - reports = reload(reports) - - bl_ui_widget = reload(bl_ui_widget) - bl_ui_label = reload(bl_ui_label) - bl_ui_button = reload(bl_ui_button) - bl_ui_image = reload(bl_ui_image) - # bl_ui_checkbox = reload(bl_ui_checkbox) - # bl_ui_slider = reload(bl_ui_slider) - # bl_ui_up_down = reload(bl_ui_up_down) - bl_ui_drag_panel = reload(bl_ui_drag_panel) - bl_ui_draw_op = reload(bl_ui_draw_op) - # bl_ui_textbox = reload(bl_ui_textbox) - -else: - from blenderkit import append_link - from blenderkit import asset_bar_op - from blenderkit import asset_inspector - from blenderkit import autothumb - from blenderkit import bg_blender - from blenderkit import bkit_oauth - from blenderkit import categories - from blenderkit import colors - from blenderkit import download - from blenderkit import icons - from blenderkit import image_utils - from blenderkit import oauth - from blenderkit import overrides - from blenderkit import paths - from blenderkit import ratings - from blenderkit import ratings_utils - from blenderkit import comments_utils - from blenderkit import resolutions - from blenderkit import search - from blenderkit import tasks_queue - from blenderkit import ui - from blenderkit import ui_bgl - from blenderkit import ui_panels - from blenderkit import upload - from blenderkit import upload_bg - from blenderkit import utils - from blenderkit import reports - - from blenderkit.bl_ui_widgets import bl_ui_widget - from blenderkit.bl_ui_widgets import bl_ui_label - from blenderkit.bl_ui_widgets import bl_ui_button - from blenderkit.bl_ui_widgets import bl_ui_image - # from blenderkit.bl_ui_widgets import bl_ui_checkbox - # from blenderkit.bl_ui_widgets import bl_ui_slider - # from blenderkit.bl_ui_widgets import bl_ui_up_down - from blenderkit.bl_ui_widgets import bl_ui_draw_op - from blenderkit.bl_ui_widgets import bl_ui_drag_panel - # from blenderkit.bl_ui_widgets import bl_ui_textbox - -import os -import math -import time -import logging -import bpy -import pathlib - -log = logging.getLogger(__name__) - -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): - ui_props = bpy.context.window_manager.blenderkitUI - ui_props.assetbar_on = False - ui_props.turn_off = False - preferences = bpy.context.preferences.addons['blenderkit'].preferences - preferences.login_attempt = False - - -@bpy.app.handlers.persistent -def check_timers_timer(): - ''' checks if all timers are registered regularly. Prevents possible bugs from stopping the addon.''' - if not bpy.app.background: - if not bpy.app.timers.is_registered(search.search_timer): - bpy.app.timers.register(search.search_timer) - if not bpy.app.timers.is_registered(download.download_timer): - bpy.app.timers.register(download.download_timer) - if not (bpy.app.timers.is_registered(tasks_queue.queue_worker)): - bpy.app.timers.register(tasks_queue.queue_worker) - if not bpy.app.timers.is_registered(bg_blender.bg_update): - bpy.app.timers.register(bg_blender.bg_update) - return 5.0 - - -conditions = ( - ('UNSPECIFIED', 'Unspecified', ""), - ('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'), - ('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 styles'), -) -search_model_styles = ( - ('REALISTIC', 'Realistic', "Photo realistic model"), - ('PAINTERLY', 'Painterly', 'Hand painted with visible strokes'), - ('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'), - ('OTHER', 'Other', 'Other style'), -) -search_material_styles = ( - ('REALISTIC', 'Realistic', "Photo realistic model"), - ('NPR', 'Non photorealistic', 'Hand painted with visible strokes'), - ('ANY', 'Any', 'Any'), -) -engines = ( - ('CYCLES', 'Cycles', 'Blender Cycles'), - ('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', ''), -) - - - - - - - -def udate_down_up(self, context): - """Perform a search if results are empty.""" - s = context.scene - wm = bpy.context.window_manager - props = bpy.context.window_manager.blenderkitUI - if wm.get('search results') == None and props.down_up == 'SEARCH': - search.search() - - -def switch_search_results(self, context): - s = bpy.context.scene - wm = bpy.context.window_manager - props = bpy.context.window_manager.blenderkitUI - if props.asset_type == 'MODEL': - wm['search results'] = wm.get('bkit model search') - wm['search results orig'] = wm.get('bkit model search orig') - elif props.asset_type == 'SCENE': - wm['search results'] = wm.get('bkit scene search') - wm['search results orig'] = wm.get('bkit scene search orig') - elif props.asset_type == 'HDR': - wm['search results'] = wm.get('bkit hdr search') - wm['search results orig'] = wm.get('bkit hdr search orig') - elif props.asset_type == 'MATERIAL': - wm['search results'] = wm.get('bkit material search') - wm['search results orig'] = wm.get('bkit material search orig') - elif props.asset_type == 'TEXTURE': - wm['search results'] = wm.get('bkit texture search') - wm['search results orig'] = wm.get('bkit texture search orig') - elif props.asset_type == 'BRUSH': - wm['search results'] = wm.get('bkit brush search') - wm['search results orig'] = wm.get('bkit brush search orig') - if not (context.sculpt_object or context.image_paint_object): - reports.add_report( - 'Switch to paint or sculpt mode to search in BlenderKit brushes.') - # if wm['search results'] == None: - # wm['search results'] = [] - # if wm['search results orig'] == None: - # wm['search results orig'] = {'count': 0, 'results': []} - - search.load_previews() - if wm['search results'] == None and props.down_up == 'SEARCH': - search.search() - - -def asset_type_callback(self, context): - ''' - Returns - items for Enum property, depending on the down_up property - BlenderKit is either in search or in upload mode. - - ''' - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - - if self.down_up == 'SEARCH': - items = ( - ('MODEL', 'Models', 'Find models', 'OBJECT_DATAMODE', 0), - ('MATERIAL', 'Materials', 'Find materials', 'MATERIAL', 2), - # ('TEXTURE', 'Texture', 'Browse textures', 'TEXTURE', 3), - ('SCENE', 'Scenes', 'Find scenes', 'SCENE_DATA', 3), - ('HDR', 'HDRs', 'Find HDRs', 'WORLD', 4), - ('BRUSH', 'Brushes', 'Find brushes', 'BRUSH_DATA', 5) - ) - else: - items = ( - ('MODEL', 'Model', 'Upload a model', 'OBJECT_DATAMODE', 0), - # ('SCENE', 'SCENE', 'Browse scenes', 'SCENE_DATA', 1), - ('MATERIAL', 'Material', 'Upload a material', 'MATERIAL', 2), - # ('TEXTURE', 'Texture', 'Browse textures', 'TEXTURE', 3), - ('SCENE', 'Scene', 'Upload a scene', 'SCENE_DATA', 3), - ('HDR', 'HDR', 'Upload a HDR', 'WORLD', 4), - ('BRUSH', 'Brush', 'Upload a brush', 'BRUSH_DATA', 5) - ) - - return items - - -def run_drag_drop_update(self, context): - if self.drag_init_button: - ui_props = bpy.context.window_manager.blenderkitUI - # ctx = utils.get_fake_context(bpy.context) - - bpy.ops.view3d.close_popup_button('INVOKE_DEFAULT') - bpy.ops.view3d.asset_drag_drop('INVOKE_DEFAULT', asset_search_index=ui_props.active_index + ui_props.scroll_offset) - - self.drag_init_button = False - - -class BlenderKitUIProps(PropertyGroup): - - down_up: EnumProperty( - name="Download vs Upload", - items=( - ('SEARCH', 'Search', 'Activate searching', 'VIEWZOOM', 0), - ('UPLOAD', 'Upload', 'Activate uploading', 'COPYDOWN', 1), - # ('RATING', 'Rating', 'Activate rating', 'SOLO_ON', 2) - ), - description="BlenderKit", - default="SEARCH", - update=udate_down_up - ) - asset_type: EnumProperty( - name=" ", - items=asset_type_callback, - description="", - default=None, - update=switch_search_results - ) - - asset_type_fold: BoolProperty(name="Expand asset types", default=False) - # 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="Highlight 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=40, 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) - scroll_offset: 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_init_button: BoolProperty(name="Drag Initialisation from button", - default=False, - description="Click or drag into scene for download", - update = run_drag_drop_update) - 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) - - hdr_upload_image: PointerProperty(name='Upload HDR', - type=bpy.types.Image, - description='Pick an image to upload') - - # StringProperty( - # name="Upload HDR", - # description="Active HDR image to upload", - # default="") - - -def search_procedural_update(self, context): - if self.search_procedural in ('PROCEDURAL', 'BOTH'): - self.search_texture_resolution = False - search.search_update(self, context) - - -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) - own_only: BoolProperty(name="My Assets Only", description="Search only for your assets", - default=False, update=search.search_update) - use_filters: BoolProperty(name="Filters are on", description="some filters are used", - 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="") - - # TEXTURE RESOLUTION - search_texture_resolution: BoolProperty(name="Texture Resolution", - description="Limit texture resolutions", - default=False, - update=search.search_update, - ) - search_texture_resolution_min: IntProperty(name="Min Texture Resolution", - description="Minimum texture resolution", - default=256, - min=0, - max=32768, - update=search.search_update, - ) - - search_texture_resolution_max: IntProperty(name="Max Texture Resolution", - description="Maximum texture resolution", - default=4096, - min=0, - max=32768, - update=search.search_update, - ) - - # file_size - search_file_size: BoolProperty(name="File Size", - description="Limit file sizes", - default=False, - update=search.search_update, - ) - search_file_size_min: IntProperty(name="Min File Size", - description="Minimum file size", - default=0, - min=0, - max=2000, - update=search.search_update, - ) - - search_file_size_max: IntProperty(name="Max File Size", - description="Maximum file size", - default=500, - min=0, - max=2000, - update=search.search_update, - ) - - search_procedural: EnumProperty( - items=( - ('BOTH', 'Both', ''), - ('PROCEDURAL', 'Procedural', ''), - ('TEXTURE_BASED', 'Texture based', ''), - - ), - default='BOTH', - description='Search only procedural/texture based assets', - update=search_procedural_update - ) - - search_verification_status: EnumProperty( - name="Verification status", - description="Search by verification status", - items= - ( - ('ALL', 'All', 'All'), - ('UPLOADING', 'Uploading', 'Uploading'), - ('UPLOADED', 'Uploaded', 'Uploaded'), - ('READY', 'Ready for V.', 'Ready for validation (deprecated since 2.8)'), - ('VALIDATED', 'Validated', 'Validated'), - ('ON_HOLD', 'On Hold', 'On Hold'), - ('REJECTED', 'Rejected', 'Rejected'), - ('DELETED', 'Deleted', 'Deleted'), - ), - default='ALL', - update=search.search_update, - ) - - # resolution download/import settings - resolution: EnumProperty( - name="Max resolution", - description="Cap texture sizes in the file to this resolution", - items= - ( - # ('256', '256x256', ''), - ('512', '512x512', ''), - ('1024', '1024x1024', ''), - ('2048', '2048x2048', ''), - ('4096', '4096x4096', ''), - ('8192', '8192x8192', ''), - ('ORIGINAL', 'ORIGINAL FILE', ''), - - ), - default='1024', - ) - free_only: BoolProperty(name="Free first", description="Show free models first", - default=False, update=search.search_update) - - unpack_files: BoolProperty(name="Unpack Files", - description="Unpack files after download", - default=True - ) - - unrated_only: BoolProperty(name="Unrated only", description="Show only unrated models", - default=False, update=search.search_update) - quality_limit: IntProperty(name="Quality limit", - description = 'Only show assets with a higher quality', - default=0, min=0, max=10, update=search.search_update) - - - -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.''' - utils.name_update(self) - - -def update_free(self, context): - if self.is_free == 'FULL': - self.is_free = 'FREE' - ui_panels.ui_message(title="All BlenderKit materials are free", - message="Any material uploaded to BlenderKit is free." \ - " However, it can still earn money for the author," \ - " based on our fair share system. " \ - "Part of subscription is sent to artists based on usage by paying users.\n") - -# common_upload_props = [ -# { -# 'identifier':'id', -# 'name':"Asset Version Id", -# 'type':'StringProperty', -# 'description':'Unique name of the asset version(hidden)', -# 'default':'' -# } -# { -# 'identifier':'id', -# 'name':"Asset Version Id", -# 'type':'StringProperty', -# 'description':'Unique name of the asset version(hidden)', -# 'default':'' -# } -# ] - - - - -class BlenderKitCommonUploadProps(object): - # for p in common_upload_props: - # exec(f"{p['identifier']}: {p['type']}(name='{p['name']}',description='{p['description']}',default='{p['default']}')") - - 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=utils.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 principled shader is used", - default=False) - - pbr_type: EnumProperty( - name="PBR Type", - items=pbr_types, - description="PBR type", - default="METALLIC", - ) - license: EnumProperty( - items=upload.licenses, - default='royalty_free', - description='License. Please read our help for choosing the right licenses', - ) - - is_private: EnumProperty( - name="Thumbnail Style", - items=( - ('PRIVATE', 'Private', ""), - ('PUBLIC', 'Public', "") - ), - description="Public assets go into the validation process. \n" - "Validated assets are visible to all users.\n" - "Private assets are limited by your plan quota\n" - "State", - default="PUBLIC", - ) - - is_procedural: BoolProperty(name="Procedural", - description="Asset is procedural - has no texture", - default=True - ) - node_count: IntProperty(name="Node count", description="Total nodes in the asset", default=0) - texture_count: IntProperty(name="Texture count", description="Total texture count in asset", default=0) - total_megapixels: IntProperty(name="Megapixels", description="Total megapixels of texture", default=0) - - # is_private: BoolProperty(name="Asset is Private", - # description="If not marked private, your asset will go into the validation process automatically\n" - # "Private assets are limited by quota", - # default=False) - - is_free: EnumProperty( - name="Thumbnail Style", - items=( - ('FULL', 'Full', "Your asset will be only available for subscribers"), - ('FREE', 'Free', "You consent you want to release this asset as free for everyone") - ), - description="Assets can be in Free or in Full plan. Also free assets generate credits", - default="FULL", - ) - - 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 or png, 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=categories.get_category_enums, - update=categories.update_category_enums - ) - subcategory: EnumProperty( - name="Subcategory", - description="Subcategory to put into", - items=categories.get_subcategory_enums, - update=categories.update_subcategory_enums - ) - subcategory1: EnumProperty( - name="Subcategory lvl2", - description="Subcategory to put into", - items=categories.get_subcategory1_enums - ) - - -class BlenderKitRatingProps(PropertyGroup): - rating_quality: IntProperty(name="Quality", - description="quality of the material", - default=0, - min=-1, max=10, - update=ratings_utils.update_ratings_quality) - - # the following enum is only to ease interaction - enums support 'drag over' and enable to draw the stars easily. - rating_quality_ui: EnumProperty(name='rating_quality_ui', - items=ratings_utils.stars_enum_callback, - description='Rating stars 0 - 10', - default=None, - update=ratings_utils.update_quality_ui, - ) - - rating_work_hours: FloatProperty(name="Work Hours", - description="How many hours did this work take?", - default=0.00, - min=0.0, max=150, update=ratings_utils.update_ratings_work_hours - ) - - # 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", - update=search.search_update, - ) - search_style_other: StringProperty( - name="Style Other", - description="Style not in the list", - default="", - update=search.search_update, - ) - search_engine: EnumProperty( - name='Engine', - items=engines, - default='NONE', - description='Output engine', - update=search.search_update, - ) - search_engine_other: StringProperty( - name="Engine", - description="engine not specified by addon", - default="", - update=search.search_update, - ) - append_method: EnumProperty( - name="Import Method", - items=( - ('LINK', 'Link', "Link Material - will be in external file and can't be directly edited"), - ('APPEND', 'Append', 'Append if you need to edit the material'), - ), - description="Appended materials are editable in your scene. Linked assets are saved in original files, " - "aren't editable directly, but also don't increase your file size", - default="APPEND" - ) - 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="", - ) - - is_free: EnumProperty( - name="Thumbnail Style", - items=( - ('FULL', 'Full', "Your asset will be only available for subscribers."), - ('FREE', 'Free', "You consent you want to release this asset as free for everyone.") - ), - description="Assets can be in Free or in Full plan. Also free assets generate credits. \n" - "All BlenderKit materials are free", - default="FREE", - update=update_free - ) - - - - 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="Size of texture in real world units", - 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 (for Glass only)", - description="For refractive materials, you might need a background.\n" - "Don't use for other types of materials.\n" - "Transparent background is preferred", - default=False) - thumbnail_background_lightness: FloatProperty(name="Thumbnail Background Lightness", - description="Set to make your material stand out with enough contrast", - default=.9, - min=0.00001, max=1) - thumbnail_samples: IntProperty(name="Cycles Samples", - description="Cycles samples", default=100, - 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_resolution: EnumProperty( - name="Resolution", - items=autothumb.thumbnail_resolutions, - description="Thumbnail resolution", - default="1024", - ) - - thumbnail_generator_type: EnumProperty( - name="Thumbnail Style", - items=( - ('BALL', 'Ball', ""), - ('BALL_COMPLEX', 'Ball complex', 'Complex ball to highlight edgewear or material thickness'), - ('FLUID', 'Fluid', 'Fluid'), - ('CLOTH', 'Cloth', 'Cloth'), - ('HAIR', 'Hair', 'Hair ') - ), - description="Style of asset", - default="BALL", - ) - - thumbnail: StringProperty( - name="Thumbnail", - description="Thumbnail path - 512x512 .jpg image, rendered with cycles.\n" - "Only standard BlenderKit previews will be accepted.\n" - "Only exception are special effects like fire or similar", - 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 BlenderKitHDRUploadProps(PropertyGroup, BlenderKitCommonUploadProps): - texture_resolution_max: IntProperty(name="Texture Resolution Max", description="texture resolution maximum", - default=0) - evs_cap: IntProperty(name="EV cap", description="EVs dynamic range", - default=0) - true_hdr: BoolProperty(name="Real HDR", description="Image has High dynamic range", default=False) - - -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' - 'Templates should be tools to finish certain tasks, like a thumbnailer scene, \n ' - 'finished mesh topology as start for modelling or others', - ) - - 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 piece or product. Not you", - default="", - ) - - designer: StringProperty( - name="Designer", - description="Author of the original design piece depicted. Usually not you", - default="", - ) - - design_collection: StringProperty( - name="Design Collection", - description="Fill if this piece is part of a real world design collection", - default="", - ) - - design_variant: StringProperty( - name="Variant", - description="Colour or material variant of the product", - default="", - ) - - thumbnail: StringProperty( - name="Thumbnail", - description="Thumbnail path - 512x512 .jpg\n" - "Rendered with cycles", - - 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=1.0, - min=0.01, max=10) - - thumbnail_angle: EnumProperty( - name='Thumbnail Angle', - items=autothumb.thumbnail_angles, - default='DEFAULT', - description='thumbnailer angle', - ) - - thumbnail_snap_to: EnumProperty( - name='Model Snaps To:', - items=autothumb.thumbnail_snap, - default='GROUND', - description='typical placing of the interior. Leave on ground for most objects that respect gravity :)', - ) - - thumbnail_resolution: EnumProperty( - name="Resolution", - items=autothumb.thumbnail_resolutions, - description="Thumbnail resolution", - default="1024", - ) - - thumbnail_samples: IntProperty(name="Cycles Samples", - description="cycles samples setting", default=100, - 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="Thumbnail path - 512x512 .jpg\n" - "Rendered with cycles", - 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 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='', - ) - - 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 - ) - - # 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. \n" - "Useful for search of historical or future objects", - default=False, - update=search.search_update, - ) - - search_design_year_min: IntProperty(name="Minimum Design Year", - description="Minimum design year", - default=1950, min=-100000000, max=1000000000, - update=search.search_update, - ) - - search_design_year_max: IntProperty(name="Maximum Design Year", - description="Maximum design year", - default=2017, - min=0, - max=10000000, - update=search.search_update, - ) - - # POLYCOUNT - search_polycount: BoolProperty(name="Use Polycount", - description="Limit polycount", - default=False, - update=search.search_update, ) - - search_polycount_min: IntProperty(name="Min Polycount", - description="Minimum poly count", - default=0, - min=0, - max=100000000, - update=search.search_update, ) - - search_polycount_max: IntProperty(name="Max Polycount", - description="Maximum poly count", - default=100000000, - min=0, - max=100000000, - update=search.search_update, - ) - - append_method: EnumProperty( - name="Import Method", - items=( - ('LINK_COLLECTION', 'Link', 'Link Collection'), - ('APPEND_OBJECTS', 'Append', 'Append as Objects'), - ), - description="Appended objects are editable in your scene. Linked assets are saved in original files, " - "aren't editable but also don't increase your file size", - default="APPEND_OBJECTS" - ) - 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') - - perpendicular_snap: BoolProperty(name='Perpendicular snap', - description="Limit snapping that is close to perpendicular angles to be perpendicular", - default=True) - - perpendicular_snap_threshold: FloatProperty(name="Threshold", - description="Limit perpendicular snap to be below these values", - default=.25, - min=0, - max=.5, - ) - - -class BlenderKitHDRSearchProps(PropertyGroup, BlenderKitCommonSearchProps): - search_keywords: StringProperty( - name="Search", - description="Search for these keywords", - default="", - update=search.search_update - ) - - true_hdr: BoolProperty( - name='Real HDRs only', - description='Search only for real HDRs, this means images that have a range higher than 0-1 in their pixels', - default=True, - update=search.search_update - ) - - -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="Restrict search for style", - 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 - ) - append_link: EnumProperty( - name="Append or link", - items=( - ('LINK', 'Link', ''), - ('APPEND', 'Append', ''), - ), - description="choose if the scene will be linked or appended", - default="APPEND" - ) - switch_after_append: BoolProperty( - name='Switch to scene after download', - default=True - ) - - -def fix_subdir(self, context): - '''Fixes project subdicrectory settings if people input invalid path.''' - - # pp = pathlib.PurePath(self.project_subdir) - pp = self.project_subdir[:] - pp = pp.replace('\\', '') - pp = pp.replace('/', '') - pp = pp.replace(':', '') - pp = '//' + pp - if self.project_subdir != pp: - self.project_subdir = pp - - ui_panels.ui_message(title="Fixed to relative path", - message="This path should be always realative.\n" \ - " It's a directory BlenderKit creates where your .blend is \n " \ - "and uses it for storing assets.") - - -class BlenderKitAddonPreferences(AddonPreferences): - # this must match the addon name, use '__package__' - # when defining this in a submodule of a python package. - bl_idname = __name__ - - default_global_dict = paths.default_global_dict() - - enable_oauth = True - - 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 - ) - - api_key_refresh: StringProperty( - name="BlenderKit refresh API Key", - description="API key used to refresh the token regularly", - default="", - subtype="PASSWORD", - ) - - api_key_timeout: IntProperty( - name='api key timeout', - description='time where the api key will need to be refreshed', - default=0, - ) - - api_key_life: IntProperty( - name='api key life time', - description='maximum lifetime of the api key, in seconds', - default=0, - ) - - refresh_in_progress: BoolProperty( - name="Api key refresh in progress", - description="Api key is currently being refreshed. Don't refresh it again", - default=False - ) - - login_attempt: BoolProperty( - name="Login/Signup attempt", - description="When this is on, BlenderKit is trying to connect and login", - default=False - ) - - show_on_start: BoolProperty( - name="Show assetbar when starting blender", - description="Show assetbar when starting blender", - default=False - ) - - tips_on_start: BoolProperty( - name="Show tips when starting blender", - description="Show tips when starting blender", - default=True - ) - - search_in_header: BoolProperty( - name="Show BlenderKit search in 3D view header", - description="Show BlenderKit search in 3D view header", - default=True - ) - - 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", - update=fix_subdir - ) - - 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 (For assets upload)", - description="By default this is off so you can continue your work without any lag", - default=False - ) - - 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=1, - max=20) - - thumb_size: IntProperty(name="Assetbar thumbnail Size", default=96, min=-1, max=256) - - #counts usages so it can encourage user after some time to do things. - asset_counter: IntProperty(name="Usage Counter", - description="Counts usages so it asks for registration only after reaching a limit", - default=0, - min=0, - max=20000) - - notifications_counter: IntProperty( - name='Notifications Counter', - description='count users notifications', - default=0, - ) - # this is now made obsolete by the new popup upon registration -ensures the user knows about the first search. - # first_run: BoolProperty( - # name="First run", - # description="Detects if addon was already registered/run.", - # default=True, - # update=utils.save_prefs - # ) - - use_timers: BoolProperty( - name="Use timers", - description="Use timers for BlenderKit. Usefull for debugging since timers seem to be unstable", - default=True, - update=utils.save_prefs - ) - - # single_timer: BoolProperty( - # name="Use timers", - # description="Use timers for BlenderKit. Usefull for debugging since timers seem to be unstable", - # default=True, - # update=utils.save_prefs - # ) - - experimental_features: BoolProperty( - name="Enable experimental features", - description="Enable all experimental features of BlenderKit. Use at your own risk", - default=False, - update=utils.save_prefs - ) - - categories_fix: BoolProperty( - name="Enable category fixing mode", - description="Enable category fixing mode", - default=False, - update=utils.save_prefs - ) - - # 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 - layout.prop(self, "show_on_start") - - if self.api_key.strip() == '': - if self.enable_oauth: - ui_panels.draw_login_buttons(layout) - else: - op = layout.operator("wm.url_open", text="Register online and get your API Key", - icon='QUESTION') - op.url = paths.BLENDERKIT_SIGNUP_URL - else: - if self.enable_oauth: - layout.operator("wm.blenderkit_logout", text="Logout", - icon='URL') - - # if not self.enable_oauth: - layout.prop(self, "api_key", text='Your API Key') - # layout.label(text='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, "allow_proximity") - # layout.prop(self, "panel_behaviour") - layout.prop(self, "thumb_size") - layout.prop(self, "max_assetbar_rows") - layout.prop(self, "tips_on_start") - layout.prop(self, "search_in_header") - layout.prop(self, "thumbnail_use_gpu") - - if bpy.context.preferences.view.show_developer_ui: - layout.prop(self, "use_timers") - layout.prop(self, "experimental_features") - layout.prop(self, "categories_fix") - - -# # @bpy.app.handlers.persistent -# def blenderkit_timer(): -# -# -# if not user_preferences.use_timers: -# search.search_timer() -# download.download_timer() -# tasks_queue.queue_worker() -# bg_blender.bg_update() -# registration -classes = ( - - BlenderKitAddonPreferences, - BlenderKitUIProps, - - BlenderKitModelSearchProps, - BlenderKitModelUploadProps, - - BlenderKitSceneSearchProps, - BlenderKitSceneUploadProps, - - BlenderKitHDRSearchProps, - BlenderKitHDRUploadProps, - - BlenderKitMaterialUploadProps, - BlenderKitMaterialSearchProps, - - BlenderKitTextureUploadProps, - - BlenderKitBrushSearchProps, - BlenderKitBrushUploadProps, - - BlenderKitRatingProps, -) - - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - - bpy.types.WindowManager.blenderkitUI = PointerProperty( - type=BlenderKitUIProps) - - # MODELS - bpy.types.WindowManager.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.WindowManager.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) - - # HDRs - bpy.types.WindowManager.blenderkit_HDR = PointerProperty( - type=BlenderKitHDRSearchProps) - bpy.types.Image.blenderkit = PointerProperty( # for uploads, not now... - type=BlenderKitHDRUploadProps) - bpy.types.Image.bkit_ratings = PointerProperty( # for uploads, not now... - type=BlenderKitRatingProps) - - # MATERIALS - bpy.types.WindowManager.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.WindowManager.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() - icons.register_icons() - ui_panels.register_ui_panels() - bg_blender.register() - utils.load_prefs() - overrides.register_overrides() - bkit_oauth.register() - tasks_queue.register() - asset_bar_op.register() - - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.use_timers and not bpy.app.background: - bpy.app.timers.register(check_timers_timer, persistent=True) - - bpy.app.handlers.load_post.append(scene_load) - # detect if the user just enabled the addon in preferences, thus enable to run - for w in bpy.context.window_manager.windows: - for a in w.screen.areas: - if a.type == 'PREFERENCES': - tasks_queue.add_task((bpy.ops.wm.blenderkit_welcome, ('INVOKE_DEFAULT',)), fake_context=True, - fake_context_area='PREFERENCES') - #save preferences after manually enabling the addon - tasks_queue.add_task((bpy.ops.wm.save_userpref, ()), fake_context=False,) - - -def unregister(): - if bpy.app.timers.is_registered(check_timers_timer): - bpy.app.timers.unregister(check_timers_timer) - ui_panels.unregister_ui_panels() - ui.unregister_ui() - - icons.unregister_icons() - search.unregister_search() - asset_inspector.unregister_asset_inspector() - download.unregister_download() - upload.unregister_upload() - ratings.unregister_ratings() - autothumb.unregister_thumbnailer() - bg_blender.unregister() - overrides.unregister_overrides() - bkit_oauth.unregister() - tasks_queue.unregister() - asset_bar_op.unregister() - - del bpy.types.WindowManager.blenderkit_models - del bpy.types.WindowManager.blenderkit_scene - del bpy.types.WindowManager.blenderkit_HDR - del bpy.types.WindowManager.blenderkit_brush - del bpy.types.WindowManager.blenderkit_mat - - del bpy.types.Scene.blenderkit - del bpy.types.Object.blenderkit - del bpy.types.Image.blenderkit - del bpy.types.Material.blenderkit - del bpy.types.Brush.blenderkit - - for cls in classes: - bpy.utils.unregister_class(cls) - - bpy.app.handlers.load_post.remove(scene_load) diff --git a/blenderkit/append_link.py b/blenderkit/append_link.py deleted file mode 100644 index 6fe710be..00000000 --- a/blenderkit/append_link.py +++ /dev/null @@ -1,403 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import utils, ui - -import bpy -import uuid - - -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 already in the scene, so we know same name != same material - - mats_before = bpy.data.materials[:] - try: - with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to): - found = False - for m in data_from.materials: - if m == matname or matname is None: - data_to.materials = [m] - # print(m, type(m)) - matname = m - found = True - break; - - #not found yet? probably some name inconsistency then. - if not found and len(data_from.materials)>0: - data_to.materials = [data_from.materials[0]] - matname = data_from.materials[0] - print(f"the material wasn't found under the exact name, appended another one: {matname}") - # print('in the appended file the name is ', matname) - - except Exception as e: - print(e) - print('failed to open the asset file') - # we have to find the new material , due to possible name changes - mat = None - for m in bpy.data.materials: - if m not in mats_before: - mat = m - break; - #still not found? - if mat is None: - mat = bpy.data.materials.get(matname) - - 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()) - - #reset ui_props of the scene to defaults: - ui_props = bpy.context.window_manager.blenderkitUI - ui_props.down_up = 'SEARCH' - - return scene - - -def get_node_sure(node_tree, ntype=''): - ''' - Gets a node of certain type, but creates a new one if not pre - ''' - node = None - for n in node_tree.nodes: - if ntype == n.bl_rna.identifier: - node = n - return node - if not node: - node = node_tree.nodes.new(type=ntype) - - return node - -def hdr_swap(name, hdr): - ''' - Try to replace the hdr in current world setup. If this fails, create a new world. - :param name: Name of the resulting world (renamse the current one if swap is successfull) - :param hdr: Image type - :return: None - ''' - w = bpy.context.scene.world - if w: - w.use_nodes = True - w.name = name - nt = w.node_tree - for n in nt.nodes: - if 'ShaderNodeTexEnvironment' == n.bl_rna.identifier: - env_node = n - env_node.image = hdr - return - new_hdr_world(name,hdr) - - -def new_hdr_world(name, hdr): - ''' - creates a new world, links in the hdr with mapping node, and links the world to scene - :param name: Name of the world datablock - :param hdr: Image type - :return: None - ''' - w = bpy.data.worlds.new(name=name) - w.use_nodes = True - bpy.context.scene.world = w - - nt = w.node_tree - env_node = nt.nodes.new(type='ShaderNodeTexEnvironment') - env_node.image = hdr - background = get_node_sure(nt, 'ShaderNodeBackground') - tex_coord = get_node_sure(nt, 'ShaderNodeTexCoord') - mapping = get_node_sure(nt, 'ShaderNodeMapping') - - nt.links.new(env_node.outputs['Color'], background.inputs['Color']) - nt.links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector']) - nt.links.new(mapping.outputs['Vector'], env_node.inputs['Vector']) - env_node.location.x = -400 - mapping.location.x = -600 - tex_coord.location.x = -800 - - -def load_HDR(file_name, name): - '''Load a HDR into file and link it to scene world. ''' - already_linked = False - for i in bpy.data.images: - if i.filepath == file_name: - hdr = i - already_linked = True - break; - - if not already_linked: - hdr = bpy.data.images.load(file_name) - - hdr_swap(name, hdr) - return hdr - - -def link_collection(file_name, obnames=[], location=(0, 0, 0), link=False, parent = None, **kwargs): - '''link an instanced group - model type asset''' - sel = utils.selection_get() - - with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to): - scols = [] - for col in data_from.collections: - if col == kwargs['name']: - data_to.collections = [col] - - rotation = (0, 0, 0) - if kwargs.get('rotation') is not None: - rotation = kwargs['rotation'] - - bpy.ops.object.empty_add(type='PLAIN_AXES', location=location, rotation=rotation) - main_object = bpy.context.view_layer.objects.active - main_object.instance_type = 'COLLECTION' - - if parent is not None: - main_object.parent = bpy.data.objects.get(parent) - - main_object.matrix_world.translation = location - - for col in bpy.data.collections: - if col.library is not None: - fp = bpy.path.abspath(col.library.filepath) - fp1 = bpy.path.abspath(file_name) - if fp == fp1: - main_object.instance_collection = col - break; - - #sometimes, the lib might already be without the actual link. - if not main_object.instance_collection and kwargs['name']: - col = bpy.data.collections.get(kwargs['name']) - if col: - main_object.instance_collection = col - - main_object.name = main_object.instance_collection.name - - # bpy.ops.wm.link(directory=file_name + "/Collection/", filename=kwargs['name'], link=link, instance_collections=True, - # autoselect=True) - # main_object = bpy.context.view_layer.objects.active - # 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 - - bbox_threshold = 25000 - display_threshold = 200000 - total_max_threshold = 2000000 - # emitting too many parent particles just kills blender now. - - #this part tuned child count, we'll leave children to artists only. - # 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)) - - #1st level of optimizaton - switch t bounding boxes. - if total_count>bbox_threshold: - target_object.display_type = 'BOUNDS' - # 2nd level of optimization - reduce percentage of displayed particles. - ps.display_percentage = min(ps.display_percentage, max(1, int(100 * display_threshold / total_count))) - #here we can also tune down number of children displayed. - #set the count - ps.count = count - #add the modifier - bpy.ops.object.particle_system_add() - # 3rd level - hide particle system from viewport - is done on the modifier.. - if total_count > total_max_threshold: - target_object.modifiers[-1].show_viewport = False - - 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''' - #simplified version of append - if kwargs.get('name'): - # by now used for appending into scene - scene = bpy.context.scene - sel = utils.selection_get() - bpy.ops.object.select_all(action='DESELECT') - - path = file_name + "\\Collection\\" - collection_name = kwargs.get('name') - fc = utils.get_fake_context(bpy.context, area_type='VIEW_3D') - bpy.ops.wm.append(fc, filename=collection_name, directory=path) - - return_obs = [] - to_hidden_collection = [] - collection = None - for ob in bpy.context.scene.objects: - if ob.select_get(): - return_obs.append(ob) - if not ob.parent: - main_object = ob - ob.location = location - # check for object that should be hidden - if ob.users_collection[0].name == collection_name: - collection = ob.users_collection[0] - collection['is_blenderkit_asset'] = True - - else: - to_hidden_collection.append(ob) - - if kwargs.get('rotation'): - main_object.rotation_euler = kwargs['rotation'] - - if kwargs.get('parent') is not None: - main_object.parent = bpy.data.objects[kwargs['parent']] - main_object.matrix_world.translation = location - - - #move objects that should be hidden to a sub collection - if len(to_hidden_collection)>0 and collection is not None: - hidden_collection_name = collection_name+'_hidden' - h_col = bpy.data.collections.new(name = hidden_collection_name) - collection.children.link(h_col) - for ob in to_hidden_collection: - ob.users_collection[0].objects.unlink(ob) - h_col.objects.link(ob) - utils.exclude_collection(hidden_collection_name) - - bpy.ops.object.select_all(action='DESELECT') - utils.selection_set(sel) - #let collection also store info that it was created by BlenderKit, for purging reasons - - return main_object, return_obs - - #this is used for uploads: - with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to): - sobs = [] - # for col in data_from.collections: - # if col == kwargs.get('name'): - 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'] - - if kwargs.get('parent') is not None: - main_object.parent = bpy.data.objects[kwargs['parent']] - main_object.matrix_world.translation = location - - bpy.ops.object.select_all(action='DESELECT') - - utils.selection_set(sel) - - - return main_object, return_obs diff --git a/blenderkit/asset_bar_op.py b/blenderkit/asset_bar_op.py deleted file mode 100644 index 4ad15285..00000000 --- a/blenderkit/asset_bar_op.py +++ /dev/null @@ -1,1004 +0,0 @@ -import bpy - -from bpy.types import Operator - -from blenderkit.bl_ui_widgets.bl_ui_label import * -from blenderkit.bl_ui_widgets.bl_ui_button import * -from blenderkit.bl_ui_widgets.bl_ui_image import * -# from blenderkit.bl_ui_widgets.bl_ui_checkbox import * -# from blenderkit.bl_ui_widgets.bl_ui_slider import * -# from blenderkit.bl_ui_widgets.bl_ui_up_down import * -from blenderkit.bl_ui_widgets.bl_ui_drag_panel import * -from blenderkit.bl_ui_widgets.bl_ui_draw_op import * -# from blenderkit.bl_ui_widgets.bl_ui_textbox import * -import random -import math -import time - -import blenderkit -from blenderkit import ui, paths, utils, search, comments_utils - -from bpy.props import ( - IntProperty, - BoolProperty, - StringProperty -) - -active_area_pointer = 0 - -def get_area_height(self): - if type(self.context) != dict: - if self.context is None: - self.context = bpy.context - self.context = self.context.copy() - # print(self.context) - if self.context.get('area') is not None: - return self.context['area'].height - # else: - # maxw, maxa, region = utils.get_largest_area() - # if maxa: - # self.context['area'] = maxa - # self.context['window'] = maxw - # self.context['region'] = region - # self.update(self.x,self.y) - # - # return self.context['area'].height - # print('no area found') - return 100 - - -BL_UI_Widget.get_area_height = get_area_height - - -def modal_inside(self, context, event): - ui_props = bpy.context.window_manager.blenderkitUI - if ui_props.turn_off: - ui_props.turn_off = False - self.finish() - - if self._finished: - return {'FINISHED'} - - if context.area: - context.area.tag_redraw() - else: - self.finish() - return {'FINISHED'} - - self.update_timer += 1 - - if self.update_timer > self.update_timer_limit: - self.update_timer = 0 - # print('timer', time.time()) - self.update_images() - - # progress bar - sr = bpy.context.window_manager.get('search results') - ui_scale = bpy.context.preferences.view.ui_scale - for asset_button in self.asset_buttons: - if sr is not None and len(sr) > asset_button.asset_index: - asset_data = sr[asset_button.asset_index] - - if asset_data['downloaded'] > 0: - asset_button.progress_bar.width = int(self.button_size * ui_scale * asset_data['downloaded'] / 100) - asset_button.progress_bar.visible = True - else: - asset_button.progress_bar.visible = False - - if self.handle_widget_events(event): - return {'RUNNING_MODAL'} - - if event.type in {"ESC"}: - self.finish() - - self.mouse_x = event.mouse_region_x - self.mouse_y = event.mouse_region_y - if event.type == 'WHEELUPMOUSE' and self.panel.is_in_rect(self.mouse_x, self.mouse_y): - self.scroll_offset -= 2 - self.scroll_update() - return {'RUNNING_MODAL'} - - elif event.type == 'WHEELDOWNMOUSE' and self.panel.is_in_rect(self.mouse_x, self.mouse_y): - self.scroll_offset += 2 - self.scroll_update() - return {'RUNNING_MODAL'} - - if self.check_ui_resized(context) or self.check_new_search_results(context): - # print(self.check_ui_resized(context), print(self.check_new_search_results(context))) - self.update_ui_size(context) - self.update_layout(context, event) - - # 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 - return {"PASS_THROUGH"} - - -def asset_bar_modal(self, context, event): - return modal_inside(self, context, event) - - -def asset_bar_invoke(self, context, event): - if not self.on_invoke(context, event): - return {"CANCELLED"} - - args = (self, context) - - self.register_handlers(args, context) - - self.update_timer_limit = 30 - self.update_timer = 0 - # print('adding timer') - # self._timer = context.window_manager.event_timer_add(10.0, window=context.window) - global active_area_pointer - context.window_manager.modal_handler_add(self) - self.active_window_pointer = context.window.as_pointer() - self.active_area_pointer = context.area.as_pointer() - active_area_pointer = context.area.as_pointer() - self.active_region_pointer = context.region.as_pointer() - - return {"RUNNING_MODAL"} - - -BL_UI_OT_draw_operator.modal = asset_bar_modal -BL_UI_OT_draw_operator.invoke = asset_bar_invoke - - -def set_mouse_down_right(self, mouse_down_right_func): - self.mouse_down_right_func = mouse_down_right_func - - -def mouse_down_right(self, x, y): - if self.is_in_rect(x, y): - self.__state = 1 - try: - self.mouse_down_right_func(self) - except Exception as e: - print(e) - - return True - - return False - - -BL_UI_Button.mouse_down_right = mouse_down_right -BL_UI_Button.set_mouse_down_right = set_mouse_down_right - -asset_bar_operator = None - - -# BL_UI_Button.handle_event = handle_event - -def get_tooltip_data(asset_data): - gimg = None - tooltip_data = asset_data.get('tooltip_data') - if tooltip_data is None: - author_text = '' - - if bpy.context.window_manager.get('bkit authors') is not None: - a = bpy.context.window_manager['bkit authors'].get(asset_data['author']['id']) - if a is not None and a != '': - if a.get('gravatarImg') is not None: - gimg = utils.get_hidden_image(a['gravatarImg'], a['gravatarHash']).name - - if len(a['firstName']) > 0 or len(a['lastName']) > 0: - author_text = f"by {a['firstName']} {a['lastName']}" - - aname = asset_data['displayName'] - aname = aname[0].upper() + aname[1:] - if len(aname) > 36: - aname = f"{aname[:33]}..." - - rc = asset_data.get('ratingsCount') - show_rating_threshold = 0 - rcount = 0 - quality = '-' - if rc: - rcount = min(rc.get('quality', 0), rc.get('workingHours', 0)) - if rcount > show_rating_threshold: - quality = str(round(asset_data['ratingsAverage'].get('quality'))) - tooltip_data = { - 'aname': aname, - 'author_text': author_text, - 'quality': quality, - 'gimg': gimg - } - asset_data['tooltip_data'] = tooltip_data - gimg = tooltip_data['gimg'] - if gimg is not None: - gimg = bpy.data.images[gimg] - - -class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator): - bl_idname = "view3d.blenderkit_asset_bar_widget" - bl_label = "BlenderKit asset bar refresh" - bl_description = "BlenderKit asset bar refresh" - bl_options = {'REGISTER'} - - 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 first", description='', default=False, options={'SKIP_SAVE'}) - - category: StringProperty( - name="Category", - description="search only subtree of this category", - default="", options={'SKIP_SAVE'}) - - tooltip: bpy.props.StringProperty(default='Runs search and displays the asset bar at the same time') - - @classmethod - def description(cls, context, properties): - return properties.tooltip - - def new_text(self, text, x, y, width=100, height=15, text_size=None, halign='LEFT'): - label = BL_UI_Label(x, y, width, height) - label.text = text - if text_size is None: - text_size = 14 - label.text_size = text_size - label.text_color = self.text_color - label._halign = halign - return label - - def init_tooltip(self): - self.tooltip_widgets = [] - self.tooltip_height = self.tooltip_size - self.tooltip_width = self.tooltip_size - ui_props = bpy.context.window_manager.blenderkitUI - if ui_props.asset_type == 'HDR': - self.tooltip_width = self.tooltip_size * 2 - # total_size = tooltip# + 2 * self.margin - self.tooltip_panel = BL_UI_Drag_Panel(0, 0, self.tooltip_width, self.tooltip_height) - self.tooltip_panel.bg_color = (0.0, 0.0, 0.0, 0.5) - self.tooltip_panel.visible = False - - tooltip_image = BL_UI_Image(0, 0, 1, 1) - img_path = paths.get_addon_thumbnail_path('thumbnail_notready.jpg') - tooltip_image.set_image(img_path) - tooltip_image.set_image_size((self.tooltip_width, self.tooltip_height)) - tooltip_image.set_image_position((0, 0)) - self.tooltip_image = tooltip_image - self.tooltip_widgets.append(tooltip_image) - - bottom_panel_fraction = 0.15 - labels_start = self.tooltip_height * (1 - bottom_panel_fraction) - - dark_panel = BL_UI_Widget(0, labels_start, self.tooltip_width, self.tooltip_height * bottom_panel_fraction) - dark_panel.bg_color = (0.0, 0.0, 0.0, 0.7) - self.tooltip_dark_panel = dark_panel - self.tooltip_widgets.append(dark_panel) - - name_label = self.new_text('', self.margin, labels_start + self.margin, - text_size=self.asset_name_text_size) - self.asset_name = name_label - self.tooltip_widgets.append(name_label) - - self.gravatar_size = int(self.tooltip_height * bottom_panel_fraction - self.margin) - - authors_name = self.new_text('author', self.tooltip_width - self.gravatar_size - self.margin, - self.tooltip_height - self.author_text_size - self.margin, labels_start, - text_size=self.author_text_size, halign='RIGHT') - self.authors_name = authors_name - self.tooltip_widgets.append(authors_name) - - gravatar_image = BL_UI_Image(self.tooltip_width - self.gravatar_size, self.tooltip_height - self.gravatar_size, - 1, 1) - img_path = paths.get_addon_thumbnail_path('thumbnail_notready.jpg') - gravatar_image.set_image(img_path) - gravatar_image.set_image_size((self.gravatar_size - 1 * self.margin, self.gravatar_size - 1 * self.margin)) - gravatar_image.set_image_position((0, 0)) - self.gravatar_image = gravatar_image - self.tooltip_widgets.append(gravatar_image) - - quality_star = BL_UI_Image(self.margin, self.tooltip_height - self.margin - self.asset_name_text_size, - 1, 1) - img_path = paths.get_addon_thumbnail_path('star_grey.png') - quality_star.set_image(img_path) - quality_star.set_image_size((self.asset_name_text_size, self.asset_name_text_size)) - quality_star.set_image_position((0, 0)) - # self.quality_star = quality_star - self.tooltip_widgets.append(quality_star) - label = self.new_text('', 2*self.margin+self.asset_name_text_size, - self.tooltip_height - int(self.asset_name_text_size+ self.margin * .5), - text_size=self.asset_name_text_size) - self.tooltip_widgets.append(label) - self.quality_label = label - - # label = self.new_text('Right click for menu.', self.margin, - # self.tooltip_height - int(self.author_text_size) - self.margin, - # text_size=int(self.author_text_size*.7)) - # self.tooltip_widgets.append(label) - - def hide_tooltip(self): - self.tooltip_panel.visible = False - for w in self.tooltip_widgets: - w.visible = False - - def show_tooltip(self): - self.tooltip_panel.visible = True - for w in self.tooltip_widgets: - w.visible = True - - def show_notifications(self, widget): - bpy.ops.wm.show_notifications() - if comments_utils.check_notifications_read(): - widget.visible = False - - def check_new_search_results(self, context): - sr = bpy.context.window_manager.get('search results') - if not hasattr(self, 'search_results_count'): - if not sr: - self.search_results_count = 0 - return True - - self.search_results_count = len(sr) - - if sr is not None and len(sr) != self.search_results_count: - self.search_results_count = len(sr) - return True - return False - - def get_region_size(self, context): - # just check the size of region.. - - region = context.region - area = context.area - ui_width = 0 - tools_width = 0 - for r in area.regions: - if r.type == 'UI': - ui_width = r.width - if r.type == 'TOOLS': - tools_width = r.width - total_width = region.width - tools_width - ui_width - return total_width, region.height - - def check_ui_resized(self, context): - # TODO this should only check if region was resized, not really care about the UI elements size. - region_width, region_height = self.get_region_size(context) - - if not hasattr(self, 'total_width'): - self.total_width = region_width - self.region_height = region_height - - if region_height != self.region_height or region_width != self.total_width: - self.region_height = region_height - self.total_width = region_width - return True - return False - - def update_ui_size(self, context): - - if bpy.app.background or not context.area: - return - - region = context.region - area = context.area - - ui_props = bpy.context.window_manager.blenderkitUI - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - ui_scale = bpy.context.preferences.view.ui_scale - - self.margin = ui_props.bl_rna.properties['margin'].default * ui_scale - self.margin = int(9 * ui_scale) - self.button_margin = int(0 * ui_scale) - self.asset_name_text_size = int(20 * ui_scale) - self.author_text_size = int(self.asset_name_text_size * .7 * ui_scale) - self.assetbar_margin = int(2 * ui_scale) - self.tooltip_size = int(512 * ui_scale) - - if ui_props.asset_type == 'HDR': - self.tooltip_width = self.tooltip_size * 2 - else: - self.tooltip_width = self.tooltip_size - - self.thumb_size = user_preferences.thumb_size * ui_scale - self.button_size = 2 * self.button_margin + self.thumb_size - self.other_button_size = 30 * ui_scale - self.icon_size = 24 * ui_scale - self.validation_icon_margin = 3 * ui_scale - reg_multiplier = 1 - if not bpy.context.preferences.system.use_region_overlap: - reg_multiplier = 0 - - ui_width = 0 - tools_width = 0 - reg_multiplier = 1 - if not bpy.context.preferences.system.use_region_overlap: - reg_multiplier = 0 - for r in area.regions: - if r.type == 'UI': - ui_width = r.width * reg_multiplier - if r.type == 'TOOLS': - tools_width = r.width * reg_multiplier - self.bar_x = tools_width + self.margin + ui_props.bar_x_offset * ui_scale - self.bar_end = ui_width + 180 * ui_scale + self.other_button_size - self.bar_width = region.width - self.bar_x - self.bar_end - - self.wcount = math.floor((self.bar_width) / (self.button_size)) - - self.max_hcount = math.floor(context.window.width / self.button_size) - self.max_wcount = user_preferences.max_assetbar_rows - - search_results = bpy.context.window_manager.get('search results') - # we need to init all possible thumb previews in advance/ - # self.hcount = user_preferences.max_assetbar_rows - if search_results is not None and self.wcount > 0: - self.hcount = min(user_preferences.max_assetbar_rows, math.ceil(len(search_results) / self.wcount)) - self.hcount = max(self.hcount, 1) - else: - self.hcount = 1 - - self.bar_height = (self.button_size) * self.hcount + 2 * self.assetbar_margin - # self.bar_y = region.height - ui_props.bar_y_offset * ui_scale - self.bar_y = ui_props.bar_y_offset * ui_scale - if ui_props.down_up == 'UPLOAD': - self.reports_y = region.height - self.bar_y - 600 - ui_props.reports_y = region.height - self.bar_y - 600 - self.reports_x = self.bar_x - ui_props.reports_x = self.bar_x - else:#ui.bar_y - ui.bar_height - 100 - - self.reports_y = region.height - self.bar_y - self.bar_height - 50 - ui_props.reports_y =region.height - self.bar_y - self.bar_height- 50 - self.reports_x = self.bar_x - ui_props.reports_x = self.bar_x - # print(self.bar_y, self.bar_height, region.height) - - def update_layout(self, context, event): - # restarting asset_bar completely since the widgets are too hard to get working with updates. - self.scroll_update() - - self.position_and_hide_buttons() - - self.button_close.set_location(self.bar_width - self.other_button_size, -self.other_button_size) - if hasattr(self, 'button_notifications'): - self.button_notifications.set_location(self.bar_width - self.other_button_size * 2, -self.other_button_size) - self.button_scroll_up.set_location(self.bar_width, 0) - self.panel.width = self.bar_width - self.panel.height = self.bar_height - - self.panel.set_location(self.bar_x, self.panel.y) - - # update Tooltip size - if self.tooltip_dark_panel.width != self.tooltip_width: - self.tooltip_dark_panel.width = self.tooltip_width - self.tooltip_panel.width = self.tooltip_width - self.tooltip_image.width = self.tooltip_width - self.tooltip_image.set_image_size((self.tooltip_width, self.tooltip_height)) - self.gravatar_image.set_location(self.tooltip_width - self.gravatar_size, - self.tooltip_height - self.gravatar_size) - self.authors_name.set_location(self.tooltip_width - self.gravatar_size - self.margin, - self.tooltip_height - self.author_text_size - self.margin) - - # to hide arrows accordingly - - - def asset_button_init(self, asset_x, asset_y, button_idx): - ui_scale = bpy.context.preferences.view.ui_scale - - button_bg_color = (0.2, 0.2, 0.2, .1) - button_hover_color = (0.8, 0.8, 0.8, .2) - - new_button = BL_UI_Button(asset_x, asset_y, self.button_size, self.button_size) - - # asset_data = sr[asset_idx] - # iname = blenderkit.utils.previmg_name(asset_idx) - # img = bpy.data.images.get(iname) - - new_button.bg_color = button_bg_color - new_button.hover_bg_color = button_hover_color - new_button.text = "" # asset_data['name'] - # if img: - # new_button.set_image(img.filepath) - - new_button.set_image_size((self.thumb_size, self.thumb_size)) - new_button.set_image_position((self.button_margin, self.button_margin)) - new_button.button_index = button_idx - new_button.search_index = button_idx - new_button.set_mouse_down(self.drag_drop_asset) - new_button.set_mouse_down_right(self.asset_menu) - new_button.set_mouse_enter(self.enter_button) - new_button.set_mouse_exit(self.exit_button) - new_button.text_input = self.handle_key_input - # add validation icon to button - - validation_icon = BL_UI_Image( - asset_x + self.button_size - self.icon_size - self.button_margin - self.validation_icon_margin, - asset_y + self.button_size - self.icon_size - self.button_margin - self.validation_icon_margin, 0, 0) - - # v_icon = ui.verification_icons[asset_data.get('verificationStatus', 'validated')] - # if v_icon is not None: - # img_fp = paths.get_addon_thumbnail_path(v_icon) - # validation_icon.set_image(img_fp) - validation_icon.set_image_size((self.icon_size, self.icon_size)) - validation_icon.set_image_position((0, 0)) - self.validation_icons.append(validation_icon) - new_button.validation_icon = validation_icon - - progress_bar = BL_UI_Widget(asset_x, asset_y + self.button_size - 3, self.button_size, 3) - progress_bar.bg_color = (0.0, 1.0, 0.0, 0.3) - new_button.progress_bar = progress_bar - self.progress_bars.append(progress_bar) - - if utils.profile_is_validator(): - red_alert = BL_UI_Widget(asset_x, asset_y, self.button_size, self.button_size) - red_alert.bg_color = (1.0, 0.0, 0.0, 0.0) - red_alert.visible = False - new_button.red_alert = red_alert - self.red_alerts.append(red_alert) - # if result['downloaded'] > 0: - # ui_bgl.draw_rect(x, y, int(ui_props.thumb_size * result['downloaded'] / 100.0), 2, green) - - return new_button - - def init_ui(self): - ui_scale = bpy.context.preferences.view.ui_scale - - button_bg_color = (0.2, 0.2, 0.2, .1) - button_hover_color = (0.8, 0.8, 0.8, .2) - - self.buttons = [] - self.asset_buttons = [] - self.validation_icons = [] - self.progress_bars = [] - self.red_alerts = [] - self.widgets_panel = [] - - self.panel = BL_UI_Drag_Panel(0, 0, self.bar_width, self.bar_height) - self.panel.bg_color = (0.0, 0.0, 0.0, 0.5) - - # sr = bpy.context.window_manager.get('search results', []) - # if sr is not None: - # we init max possible buttons. - button_idx = 0 - for x in range(0, self.max_wcount): - for y in range(0, self.max_hcount): - # asset_x = self.assetbar_margin + a * (self.button_size) - # asset_y = self.assetbar_margin + b * (self.button_size) - # button_idx = x + y * self.max_wcount - asset_idx = button_idx + self.scroll_offset - # if asset_idx < len(sr): - new_button = self.asset_button_init(0, 0, button_idx) - new_button.asset_index = asset_idx - self.asset_buttons.append(new_button) - button_idx += 1 - - self.button_close = BL_UI_Button(self.bar_width - self.other_button_size, -self.other_button_size, - self.other_button_size, - self.other_button_size) - self.button_close.bg_color = button_bg_color - self.button_close.hover_bg_color = button_hover_color - self.button_close.text = "" - img_fp = paths.get_addon_thumbnail_path('vs_rejected.png') - self.button_close.set_image(img_fp) - self.button_close.set_mouse_down(self.cancel_press) - - self.widgets_panel.append(self.button_close) - - scroll_width = 30 - self.button_scroll_down = BL_UI_Button(-scroll_width, 0, scroll_width, self.bar_height) - self.button_scroll_down.bg_color = button_bg_color - self.button_scroll_down.hover_bg_color = button_hover_color - self.button_scroll_down.text = "" - self.button_scroll_down.set_image(paths.get_addon_thumbnail_path('arrow_left.png')) - self.button_scroll_down.set_image_size((scroll_width, self.button_size)) - self.button_scroll_down.set_image_position((0, int((self.bar_height - self.button_size) / 2))) - - self.button_scroll_down.set_mouse_down(self.scroll_down) - - self.widgets_panel.append(self.button_scroll_down) - - self.button_scroll_up = BL_UI_Button(self.bar_width, 0, scroll_width, self.bar_height) - self.button_scroll_up.bg_color = button_bg_color - self.button_scroll_up.hover_bg_color = button_hover_color - self.button_scroll_up.text = "" - self.button_scroll_up.set_image(paths.get_addon_thumbnail_path('arrow_right.png')) - self.button_scroll_up.set_image_size((scroll_width, self.button_size)) - self.button_scroll_up.set_image_position((0, int((self.bar_height - self.button_size) / 2))) - - self.button_scroll_up.set_mouse_down(self.scroll_up) - - self.widgets_panel.append(self.button_scroll_up) - - # notifications - if not comments_utils.check_notifications_read(): - self.button_notifications = BL_UI_Button(self.bar_width - self.other_button_size * 2, - -self.other_button_size, self.other_button_size, - self.other_button_size) - self.button_notifications.bg_color = button_bg_color - self.button_notifications.hover_bg_color = button_hover_color - self.button_notifications.text = "" - img_fp = paths.get_addon_thumbnail_path('bell.png') - self.button_notifications.set_image(img_fp) - self.button_notifications.set_mouse_down(self.show_notifications) - self.widgets_panel.append(self.button_notifications) - - self.update_images() - - def position_and_hide_buttons(self): - # position and layout buttons - sr = bpy.context.window_manager.get('search results', []) - if sr is None: - sr = [] - - i = 0 - for y in range(0, self.hcount): - for x in range(0, self.wcount): - asset_x = self.assetbar_margin + x * (self.button_size) - asset_y = self.assetbar_margin + y * (self.button_size) - button_idx = x + y * self.wcount - asset_idx = button_idx + self.scroll_offset - if len(self.asset_buttons) <= button_idx: - break - button = self.asset_buttons[button_idx] - button.set_location(asset_x, asset_y) - button.validation_icon.set_location( - asset_x + self.button_size - self.icon_size - self.button_margin - self.validation_icon_margin, - asset_y + self.button_size - self.icon_size - self.button_margin - self.validation_icon_margin) - button.progress_bar.set_location(asset_x, asset_y + self.button_size - 3) - if asset_idx < len(sr): - button.visible = True - button.validation_icon.visible = True - # button.progress_bar.visible = True - else: - button.visible = False - button.validation_icon.visible = False - button.progress_bar.visible = False - if utils.profile_is_validator(): - button.red_alert.set_location(asset_x, asset_y) - i += 1 - - for a in range(i, len(self.asset_buttons)): - button = self.asset_buttons[a] - button.visible = False - button.validation_icon.visible = False - button.progress_bar.visible = False - - def __init__(self): - super().__init__() - - self.update_ui_size(bpy.context) - - # todo move all this to update UI size - ui_props = bpy.context.window_manager.blenderkitUI - - self.draw_tooltip = False - # let's take saved scroll offset and use it to keep scroll between operator runs - self.scroll_offset = ui_props.scroll_offset - - self.text_color = (0.9, 0.9, 0.9, 1.0) - - self.init_ui() - self.init_tooltip() - self.hide_tooltip() - - def setup_widgets(self, context, event): - widgets_panel = [] - widgets_panel.extend(self.widgets_panel) - widgets_panel.extend(self.buttons) - widgets_panel.extend(self.asset_buttons) - widgets_panel.extend(self.validation_icons) - widgets_panel.extend(self.progress_bars) - widgets_panel.extend(self.red_alerts) - - widgets = [self.panel] - - widgets += widgets_panel - widgets.append(self.tooltip_panel) - widgets += self.tooltip_widgets - - self.init_widgets(context, widgets) - self.panel.add_widgets(widgets_panel) - self.tooltip_panel.add_widgets(self.tooltip_widgets) - - def on_invoke(self, context, event): - - self.context = context - - if self.do_search or context.window_manager.get('search results') is None: - # TODO: move the search behaviour to separate operator, since asset bar can be already woken up from a timer. - - # 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) - - ui_props = context.window_manager.blenderkitUI - if ui_props.assetbar_on: - # TODO solve this otehrwise to enable more asset bars? - - # 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 already 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 False - - ui_props.assetbar_on = True - global asset_bar_operator - - asset_bar_operator = self - - self.active_index = -1 - - self.setup_widgets(context, event) - self.position_and_hide_buttons() - self.hide_tooltip() - - self.panel.set_location(self.bar_x, - self.bar_y) - # to hide arrows accordingly - self.scroll_update() - - self.window = context.window - self.area = context.area - self.scene = bpy.context.scene - # global active_window_pointer, active_area_pointer, active_region_pointer - # ui.active_window_pointer = self.window.as_pointer() - # ui.active_area_pointer = self.area.as_pointer() - # ui.active_region_pointer = self.region.as_pointer() - - return True - - def on_finish(self, context): - # redraw all areas, since otherwise it stays to hang for some more time. - # bpy.types.SpaceView3D.draw_handler_remove(self._handle_2d_tooltip, 'WINDOW') - # to pass the operator to validation icons - global asset_bar_operator - asset_bar_operator = None - - # context.window_manager.event_timer_remove(self._timer) - - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - ui_props.assetbar_on = False - ui_props.scroll_offset = self.scroll_offset - - wm = bpy.data.window_managers[0] - - for w in wm.windows: - for a in w.screen.areas: - a.tag_redraw() - self._finished = True - - # handlers - - def enter_button(self, widget): - # print('enter button', self.active_index, widget.button_index) - # print(widget.button_index+ self.scroll_offset, self.active_index) - search_index = widget.button_index + self.scroll_offset - if search_index < self.search_results_count: - self.show_tooltip() - # print(self.active_index, search_index) - if self.active_index != search_index: - self.active_index = search_index - - scene = bpy.context.scene - wm = bpy.context.window_manager - sr = wm['search results'] - asset_data = sr[search_index] # + self.scroll_offset] - - self.draw_tooltip = True - # self.tooltip = asset_data['tooltip'] - ui_props = bpy.context.window_manager.blenderkitUI - ui_props.active_index = search_index # + self.scroll_offset - - img = ui.get_large_thumbnail_image(asset_data) - if img: - self.tooltip_image.set_image(img.filepath) - - get_tooltip_data(asset_data) - an = asset_data['name'] - max_name_length = 30 - if len(an)>max_name_length+3: - an = an[:30]+'...' - self.asset_name.text = an - self.authors_name.text = asset_data['tooltip_data']['author_text'] - self.quality_label.text = asset_data['tooltip_data']['quality'] - # print(asset_data['tooltip_data']['quality']) - gimg = asset_data['tooltip_data']['gimg'] - if gimg is not None: - gimg = bpy.data.images[gimg] - if gimg: - self.gravatar_image.set_image(gimg.filepath - ) - # print('moving tooltip') - properties_width = 0 - for r in bpy.context.area.regions: - if r.type == 'UI': - properties_width = r.width - tooltip_x = min(int(widget.x_screen), - int(bpy.context.region.width - self.tooltip_panel.width - properties_width)) - tooltip_y = int(widget.y_screen + widget.height) - # self.init_tooltip() - self.tooltip_panel.set_location(tooltip_x, tooltip_y) - self.tooltip_panel.layout_widgets() - # print(tooltip_x, tooltip_y) - # bpy.ops.wm.blenderkit_asset_popup('INVOKE_DEFAULT') - - def exit_button(self, widget): - # print(f'exit {widget.search_index} , {self.active_index}') - # this condition checks if there wasn't another button already entered, which can happen with small button gaps - if self.active_index == widget.button_index + self.scroll_offset: - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - ui_props.draw_tooltip = False - self.draw_tooltip = False - self.hide_tooltip() - # popup asset card on mouse down - # if utils.experimental_enabled(): - # h = widget.get_area_height() - # print(h,h-self.mouse_y,self.panel.y_screen, self.panel.y,widget.y_screen, widget.y) - # if utils.experimental_enabled() and self.mouse_y<widget.y_screen: - # self.active_index = widget.button_index + self.scroll_offset - # bpy.ops.wm.blenderkit_asset_popup('INVOKE_DEFAULT') - - def drag_drop_asset(self, widget): - bpy.ops.view3d.asset_drag_drop('INVOKE_DEFAULT', asset_search_index=widget.search_index + self.scroll_offset) - - def cancel_press(self, widget): - self.finish() - - def asset_menu(self, widget): - self.hide_tooltip() - bpy.ops.wm.blenderkit_asset_popup('INVOKE_DEFAULT') - # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_asset_menu') - - def search_more(self): - sro = bpy.context.window_manager.get('search results orig') - if sro is None: - return - if sro.get('next') is None: - return - search_props = utils.get_search_props() - if search_props.is_searching: - return - - blenderkit.search.search(get_next=True) - - def update_validation_icon(self, asset_button, asset_data): - if utils.profile_is_validator(): - ar = bpy.context.window_manager.get('asset ratings') - rating = ar.get(asset_data['id']) - if rating is not None: - rating = rating.to_dict() - - v_icon = ui.verification_icons[asset_data.get('verificationStatus', 'validated')] - if v_icon is not None: - img_fp = paths.get_addon_thumbnail_path(v_icon) - asset_button.validation_icon.set_image(img_fp) - asset_button.validation_icon.visible = True - elif rating in (None, {}): - v_icon = 'star_grey.png' - img_fp = paths.get_addon_thumbnail_path(v_icon) - asset_button.validation_icon.set_image(img_fp) - asset_button.validation_icon.visible = True - else: - asset_button.validation_icon.visible = False - else: - if asset_data.get('canDownload', True) == 0: - img_fp = paths.get_addon_thumbnail_path('locked.png') - asset_button.validation_icon.set_image(img_fp) - else: - asset_button.validation_icon.visible = False - - def update_images(self): - sr = bpy.context.window_manager.get('search results') - if not sr: - return - for asset_button in self.asset_buttons: - if asset_button.visible: - asset_button.asset_index = asset_button.button_index + self.scroll_offset - # print(asset_button.asset_index, len(sr)) - if asset_button.asset_index < len(sr): - asset_button.visible = True - - asset_data = sr[asset_button.asset_index] - - iname = blenderkit.utils.previmg_name(asset_button.asset_index) - # show indices for debug purposes - # asset_button.text = str(asset_button.asset_index) - img = bpy.data.images.get(iname) - if img is None or len(img.pixels) == 0: - img_filepath = paths.get_addon_thumbnail_path('thumbnail_notready.jpg') - else: - img_filepath = img.filepath - # print(asset_button.button_index, img_filepath) - - asset_button.set_image(img_filepath) - self.update_validation_icon(asset_button, asset_data) - - if utils.profile_is_validator() and asset_data['verificationStatus'] == 'uploaded': - over_limit = utils.is_upload_old(asset_data) - if over_limit: - redness = min(over_limit * .05, 0.7) - asset_button.red_alert.bg_color = (1, 0, 0, redness) - asset_button.red_alert.visible = True - else: - asset_button.red_alert.visible = False - elif utils.profile_is_validator(): - asset_button.red_alert.visible = False - else: - asset_button.visible = False - asset_button.validation_icon.visible = False - if utils.profile_is_validator(): - asset_button.red_alert.visible = False - - def scroll_update(self): - sr = bpy.context.window_manager.get('search results') - sro = bpy.context.window_manager.get('search results orig') - # empty results - if sr is None: - self.button_scroll_down.visible = False - self.button_scroll_up.visible = False - return - - self.scroll_offset = min(self.scroll_offset, len(sr) - (self.wcount * self.hcount)) - self.scroll_offset = max(self.scroll_offset, 0) - self.update_images() - - # print(sro) - if sro['count'] > len(sr) and len(sr) - self.scroll_offset < (self.wcount * self.hcount) + 15: - self.search_more() - - if self.scroll_offset == 0: - self.button_scroll_down.visible = False - else: - self.button_scroll_down.visible = True - - if self.scroll_offset >= sro['count'] - (self.wcount * self.hcount): - self.button_scroll_up.visible = False - else: - self.button_scroll_up.visible = True - - def search_by_author(self, asset_index): - sr = bpy.context.window_manager['search results'] - asset_data = sr[asset_index] - a = asset_data['author']['id'] - if a is not None: - sprops = utils.get_search_props() - sprops.search_keywords = '' - sprops.search_verification_status = 'ALL' - # utils.p('author:', a) - search.search(author_id=a) - return True - - def handle_key_input(self, event): - if event.type == 'A': - self.search_by_author(self.active_index) - return True - return False - - def scroll_up(self, widget): - self.scroll_offset += self.wcount * self.hcount - self.scroll_update() - - def scroll_down(self, widget): - self.scroll_offset -= self.wcount * self.hcount - self.scroll_update() - - -def register(): - bpy.utils.register_class(BlenderKitAssetBarOperator) - - -def unregister(): - bpy.utils.unregister_class(BlenderKitAssetBarOperator) diff --git a/blenderkit/asset_inspector.py b/blenderkit/asset_inspector.py deleted file mode 100644 index cbb9517a..00000000 --- a/blenderkit/asset_inspector.py +++ /dev/null @@ -1,393 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import utils - -import bpy -from object_print3d_utils import operators as ops - -RENDER_OBTYPES = ['MESH', 'CURVE', 'SURFACE', 'METABALL', 'TEXT'] - - -def check_material(props, mat): - e = bpy.context.scene.render.engine - shaders = [] - textures = [] - props.texture_count = 0 - props.node_count = 0 - props.total_megapixels = 0 - props.is_procedural = True - - if e == 'CYCLES': - - if mat.node_tree is not None: - checknodes = mat.node_tree.nodes[:] - while len(checknodes) > 0: - n = checknodes.pop() - props.node_count += 1 - 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': - - if n.image is not None: - mattype = 'image based' - props.is_procedural = False - if n.image not in textures: - textures.append(n.image) - props.texture_count += 1 - props.total_megapixels += (n.image.size[0] * n.image.size[1]) - - 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 = [] - textures = [] - props.uv = False - props.texture_count = 0 - props.total_megapixels = 0 - props.node_count = 0 - 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() - props.node_count +=1 - 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': - - - if n.image is not None and n.image not in textures: - props.is_procedural = False - mattype = 'image based' - - textures.append(n.image) - props.texture_count += 1 - props.total_megapixels += (n.image.size[0] * n.image.size[1]) - - 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. - 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 + ', ') - - -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: - passed = item[0].endswith(' 0') - 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': - ob_eval = None - if ob.type == 'CURVE': - # depsgraph = bpy.context.evaluated_depsgraph_get() - # object_eval = ob.evaluated_get(depsgraph) - mesh = ob.to_mesh() - 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 - - if ob_eval: - ob_eval.to_mesh_clear() - - # 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 - 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') - - # 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.window_manager.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) - elif ui.asset_type == 'HDR': - # reset some properties here, because they might not get re-filled at all when they aren't needed anymore. - - hdr = utils.get_active_asset() - props = hdr.blenderkit - props.texture_resolution_max = max(hdr.size[0],hdr.size[1]) - - -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" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - @classmethod - def poll(cls, context): - return utils.uploadable_asset_poll() - - 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/asset_pack_bg.py b/blenderkit/asset_pack_bg.py deleted file mode 100644 index c59ca08d..00000000 --- a/blenderkit/asset_pack_bg.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys -import json -from blenderkit import resolutions - -BLENDERKIT_EXPORT_DATA = sys.argv[-1] - -if __name__ == "__main__": - resolutions.run_bg(sys.argv[-1]) diff --git a/blenderkit/autothumb.py b/blenderkit/autothumb.py deleted file mode 100644 index 330d31a2..00000000 --- a/blenderkit/autothumb.py +++ /dev/null @@ -1,671 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -from blenderkit import paths, utils, bg_blender, ui_panels, icons, tasks_queue, download - -import tempfile, os, subprocess, json, sys - -import bpy -from bpy.props import ( - FloatProperty, - IntProperty, - EnumProperty, - BoolProperty, - StringProperty, -) - -BLENDERKIT_EXPORT_DATA_FILE = "data.json" - -thumbnail_resolutions = ( - ('256', '256', ''), - ('512', '512', ''), - ('1024', '1024 - minimum for public', ''), - ('2048', '2048', ''), -) - -thumbnail_angles = ( - ('DEFAULT', 'default', ''), - ('FRONT', 'front', ''), - ('SIDE', 'side', ''), - ('TOP', 'top', ''), -) - -thumbnail_snap = ( - ('GROUND', 'ground', ''), - ('WALL', 'wall', ''), - ('CEILING', 'ceiling', ''), - ('FLOAT', 'floating', ''), -) - - -def get_texture_ui(tpath, iname): - tex = bpy.data.textures.get(iname) - - if tpath.startswith('//'): - tpath = bpy.path.abspath(tpath) - - if not tex or not tex.image or not tex.image.filepath == tpath: - tasks_queue.add_task((utils.get_hidden_image, (tpath, iname)), only_last=True) - tasks_queue.add_task((utils.get_hidden_texture, (iname,)), only_last=True) - return None - return tex - - -def check_thumbnail(props, imgpath): - img = utils.get_hidden_image(imgpath, 'upload_preview', force_reload=True) - # print(' check thumbnail ', img) - 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 = '' - - tex = utils.get_hidden_texture(img.name) - # pcoll = icons.icon_collections["previews"] - # pcoll.load(img.name, img.filepath, 'IMAGE') - - return img - 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 - img = 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.view_layer.objects.active 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=None, json_args=None, props=None, wait=False, add_bg_process=True): - # Prepare to save the file - - binary_path = bpy.app.binary_path - script_path = os.path.dirname(os.path.realpath(__file__)) - - ext = '.blend' - - tfpath = paths.get_thumbnailer_filepath() - datafile = os.path.join(json_args['tempdir'], BLENDERKIT_EXPORT_DATA_FILE) - try: - with open(datafile, 'w', encoding='utf-8') as s: - json.dump(json_args, s, ensure_ascii=False, indent=4) - - proc = subprocess.Popen([ - binary_path, - "--background", - "-noaudio", - tfpath, - "--python", os.path.join(script_path, "autothumb_model_bg.py"), - "--", datafile, - ], bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, creationflags=utils.get_process_flags()) - - eval_path_computing = "bpy.data.objects['%s'].blenderkit.is_generating_thumbnail" % json_args['asset_name'] - eval_path_state = "bpy.data.objects['%s'].blenderkit.thumbnail_generating_state" % json_args['asset_name'] - eval_path = "bpy.data.objects['%s']" % json_args['asset_name'] - - bg_blender.add_bg_process(name = f"{json_args['asset_name']} thumbnailer" ,eval_path_computing=eval_path_computing, eval_path_state=eval_path_state, - eval_path=eval_path, process_type='THUMBNAILER', process=proc) - - - except Exception as e: - self.report({'WARNING'}, "Error while exporting file: %s" % str(e)) - return {'FINISHED'} - - -def start_material_thumbnailer(self=None, json_args=None, props=None, wait=False, add_bg_process=True): - ''' - - Parameters - ---------- - self - json_args - all arguments: - props - blenderkit upload props with thumbnail settings, to communicate back, if not present, not used. - wait - wait for the rendering to finish - - Returns - ------- - - ''' - if props: - props.is_generating_thumbnail = True - props.thumbnail_generating_state = 'starting blender instance' - - binary_path = bpy.app.binary_path - script_path = os.path.dirname(os.path.realpath(__file__)) - - tfpath = paths.get_material_thumbnailer_filepath() - datafile = os.path.join(json_args['tempdir'], BLENDERKIT_EXPORT_DATA_FILE) - - try: - with open(datafile, 'w', encoding='utf-8') as s: - json.dump(json_args, s, ensure_ascii=False, indent=4) - - proc = subprocess.Popen([ - binary_path, - "--background", - "-noaudio", - tfpath, - "--python", os.path.join(script_path, "autothumb_material_bg.py"), - "--", datafile, - ], bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, creationflags=utils.get_process_flags()) - - eval_path_computing = "bpy.data.materials['%s'].blenderkit.is_generating_thumbnail" % json_args['asset_name'] - eval_path_state = "bpy.data.materials['%s'].blenderkit.thumbnail_generating_state" % json_args['asset_name'] - eval_path = "bpy.data.materials['%s']" % json_args['asset_name'] - - bg_blender.add_bg_process(name=f"{json_args['asset_name']} thumbnailer", eval_path_computing=eval_path_computing, - eval_path_state=eval_path_state, - eval_path=eval_path, process_type='THUMBNAILER', process=proc) - if props: - props.thumbnail_generating_state = 'Saving .blend file' - - if wait: - while proc.poll() is None: - stdout_data, stderr_data = proc.communicate() - print(stdout_data) - except Exception as e: - if self: - self.report({'WARNING'}, "Error while packing file: %s" % str(e)) - else: - print(e) - return {'FINISHED'} - - -class GenerateThumbnailOperator(bpy.types.Operator): - """Generate Cycles thumbnail for model assets""" - bl_idname = "object.blenderkit_generate_thumbnail" - bl_label = "BlenderKit Thumbnail Generator" - bl_options = {'REGISTER', 'INTERNAL'} - - @classmethod - def poll(cls, context): - return bpy.context.view_layer.objects.active 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_resolution') - layout.prop(props, 'thumbnail_denoising') - preferences = bpy.context.preferences.addons['blenderkit'].preferences - layout.prop(preferences, "thumbnail_use_gpu") - - def execute(self, context): - asset = utils.get_active_model() - asset.blenderkit.is_generating_thumbnail = True - asset.blenderkit.thumbnail_generating_state = 'starting blender instance' - - tempdir = tempfile.mkdtemp() - ext = '.blend' - filepath = os.path.join(tempdir, "thumbnailer_blenderkit" + ext) - - path_can_be_relative = True - file_dir = os.path.dirname(bpy.data.filepath) - if file_dir == '': - file_dir = tempdir - path_can_be_relative = False - - an_slug = paths.slugify(asset.name) - thumb_path = os.path.join(file_dir, an_slug) - if path_can_be_relative: - rel_thumb_path = os.path.join('//', an_slug) - else: - rel_thumb_path = thumb_path - - - i = 0 - while os.path.isfile(thumb_path + '.jpg'): - thumb_path = os.path.join(file_dir, an_slug + '_' + str(i).zfill(4)) - rel_thumb_path = os.path.join('//', an_slug + '_' + str(i).zfill(4)) - i += 1 - bkit = asset.blenderkit - - bkit.thumbnail = rel_thumb_path + '.jpg' - bkit.thumbnail_generating_state = 'Saving .blend file' - - # if this isn't here, blender crashes. - bpy.context.preferences.filepaths.file_preview_type = 'NONE' - # 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) - # get all included objects - obs = utils.get_hierarchy(asset) - obnames = [] - for ob in obs: - obnames.append(ob.name) - - args_dict = { - "type": "material", - "asset_name": asset.name, - "filepath": filepath, - "thumbnail_path": thumb_path, - "tempdir": tempdir, - } - thumbnail_args = { - "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_resolution": bkit.thumbnail_resolution, - "thumbnail_samples": bkit.thumbnail_samples, - "thumbnail_denoising": bkit.thumbnail_denoising, - } - args_dict.update(thumbnail_args) - - start_thumbnailer(self, - json_args=args_dict, - props=asset.blenderkit, wait=False) - return {'FINISHED'} - - def invoke(self, context, event): - wm = context.window_manager - # if bpy.data.filepath == '': - # ui_panels.ui_message( - # title="Can't render thumbnail", - # message="please save your file first") - # - # return {'FINISHED'} - - return wm.invoke_props_dialog(self) - - -class ReGenerateThumbnailOperator(bpy.types.Operator): - """ - Generate default thumbnail with Cycles renderer and upload it. - Works also for assets from search results, without being downloaded before - """ - bl_idname = "object.blenderkit_regenerate_thumbnail" - bl_label = "BlenderKit Thumbnail Re-generate" - bl_options = {'REGISTER', 'INTERNAL'} - - asset_index: IntProperty(name="Asset Index", description='asset index in search results', default=-1) - - thumbnail_background_lightness: FloatProperty(name="Thumbnail Background Lightness", - description="set to make your material stand out", default=1.0, - 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_resolution: EnumProperty( - name="Resolution", - items=thumbnail_resolutions, - description="Thumbnail resolution", - default="1024", - ) - - thumbnail_samples: IntProperty(name="Cycles Samples", - description="cycles samples setting", default=100, - min=5, max=5000) - thumbnail_denoising: BoolProperty(name="Use Denoising", - description="Use denoising", default=True) - - @classmethod - def poll(cls, context): - return True # bpy.context.view_layer.objects.active is not None - - def draw(self, context): - props = self - layout = self.layout - # layout.label('This will re-generate thumbnail and directly upload it to server. You should see your updated thumbnail online depending ') - 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_resolution') - layout.prop(props, 'thumbnail_denoising') - preferences = bpy.context.preferences.addons['blenderkit'].preferences - layout.prop(preferences, "thumbnail_use_gpu") - - def execute(self, context): - if not self.asset_index > -1: - return {'CANCELLED'} - - # either get the data from search results - sr = bpy.context.window_manager['search results'] - asset_data = sr[self.asset_index].to_dict() - - tempdir = tempfile.mkdtemp() - - an_slug = paths.slugify(asset_data['name']) - thumb_path = os.path.join(tempdir, an_slug) - - - args_dict = { - "type": "material", - "asset_name": asset_data['name'], - "asset_data": asset_data, - # "filepath": filepath, - "thumbnail_path": thumb_path, - "tempdir": tempdir, - "do_download": True, - "upload_after_render": True, - } - thumbnail_args = { - "type": "model", - "thumbnail_angle": self.thumbnail_angle, - "thumbnail_snap_to": self.thumbnail_snap_to, - "thumbnail_background_lightness": self.thumbnail_background_lightness, - "thumbnail_resolution": self.thumbnail_resolution, - "thumbnail_samples": self.thumbnail_samples, - "thumbnail_denoising": self.thumbnail_denoising, - } - args_dict.update(thumbnail_args) - - start_thumbnailer(self, - json_args=args_dict, - wait=False) - return {'FINISHED'} - - def invoke(self, context, event): - wm = context.window_manager - # if bpy.data.filepath == '': - # ui_panels.ui_message( - # title="Can't render thumbnail", - # message="please save your file first") - # - # return {'FINISHED'} - - return wm.invoke_props_dialog(self) - - -class GenerateMaterialThumbnailOperator(bpy.types.Operator): - """Generate default thumbnail with Cycles renderer""" - bl_idname = "object.blenderkit_generate_material_thumbnail" - bl_label = "BlenderKit Material Thumbnail Generator" - bl_options = {'REGISTER', 'INTERNAL'} - - @classmethod - def poll(cls, context): - return bpy.context.view_layer.objects.active 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_resolution') - layout.prop(props, 'thumbnail_samples') - layout.prop(props, 'thumbnail_denoising') - layout.prop(props, 'adaptive_subdivision') - preferences = bpy.context.preferences.addons['blenderkit'].preferences - layout.prop(preferences, "thumbnail_use_gpu") - - def execute(self, context): - asset = bpy.context.active_object.active_material - tempdir = tempfile.mkdtemp() - filepath = os.path.join(tempdir, "material_thumbnailer_cycles.blend") - # if this isn't here, blender crashes. - bpy.context.preferences.filepaths.file_preview_type = 'NONE' - - # 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) - - thumb_dir = os.path.dirname(bpy.data.filepath) - an_slug = paths.slugify(asset.name) - - thumb_path = os.path.join(thumb_dir, an_slug) - rel_thumb_path = os.path.join('//', an_slug) - - # auto increase number of the generated thumbnail. - i = 0 - while os.path.isfile(thumb_path + '.png'): - thumb_path = os.path.join(thumb_dir, an_slug + '_' + str(i).zfill(4)) - rel_thumb_path = os.path.join('//', an_slug + '_' + str(i).zfill(4)) - i += 1 - - asset.blenderkit.thumbnail = rel_thumb_path + '.png' - bkit = asset.blenderkit - - args_dict = { - "type": "material", - "asset_name": asset.name, - "filepath": filepath, - "thumbnail_path": thumb_path, - "tempdir": tempdir, - } - - thumbnail_args = { - "thumbnail_type": bkit.thumbnail_generator_type, - "thumbnail_scale": bkit.thumbnail_scale, - "thumbnail_background": bkit.thumbnail_background, - "thumbnail_background_lightness": bkit.thumbnail_background_lightness, - "thumbnail_resolution": bkit.thumbnail_resolution, - "thumbnail_samples": bkit.thumbnail_samples, - "thumbnail_denoising": bkit.thumbnail_denoising, - "adaptive_subdivision": bkit.adaptive_subdivision, - "texture_size_meters": bkit.texture_size_meters, - } - args_dict.update(thumbnail_args) - start_material_thumbnailer(self, - json_args=args_dict, - props=asset.blenderkit, wait=False) - - return {'FINISHED'} - - def invoke(self, context, event): - wm = context.window_manager - return wm.invoke_props_dialog(self) - - -class ReGenerateMaterialThumbnailOperator(bpy.types.Operator): - """ - Generate default thumbnail with Cycles renderer and upload it. - Works also for assets from search results, without being downloaded before - """ - bl_idname = "object.blenderkit_regenerate_material_thumbnail" - bl_label = "BlenderKit Material Thumbnail Re-Generator" - bl_options = {'REGISTER', 'INTERNAL'} - - asset_index: IntProperty(name="Asset Index", description='asset index in search results', default=-1) - - 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 (for Glass only)", - description="For refractive materials, you might need a background.\n" - "Don't use for other types of materials.\n" - "Transparent background is preferred", - default=False) - thumbnail_background_lightness: FloatProperty(name="Thumbnail Background Lightness", - description="Set to make your material stand out with enough contrast", - default=.9, - min=0.00001, max=1) - thumbnail_samples: IntProperty(name="Cycles Samples", - description="Cycles samples", default=100, - 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_resolution: EnumProperty( - name="Resolution", - items=thumbnail_resolutions, - description="Thumbnail resolution", - default="1024", - ) - - thumbnail_generator_type: EnumProperty( - name="Thumbnail Style", - items=( - ('BALL', 'Ball', ""), - ('BALL_COMPLEX', 'Ball complex', 'Complex ball to highlight edgewear or material thickness'), - ('FLUID', 'Fluid', 'Fluid'), - ('CLOTH', 'Cloth', 'Cloth'), - ('HAIR', 'Hair', 'Hair ') - ), - description="Style of asset", - default="BALL", - ) - - @classmethod - def poll(cls, context): - return True # bpy.context.view_layer.objects.active is not None - - def check(self, context): - return True - - def draw(self, context): - layout = self.layout - props = self - 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_resolution') - layout.prop(props, 'thumbnail_samples') - layout.prop(props, 'thumbnail_denoising') - layout.prop(props, 'adaptive_subdivision') - preferences = bpy.context.preferences.addons['blenderkit'].preferences - layout.prop(preferences, "thumbnail_use_gpu") - - def execute(self, context): - - if not self.asset_index > -1: - return {'CANCELLED'} - - # either get the data from search results - sr = bpy.context.window_manager['search results'] - asset_data = sr[self.asset_index].to_dict() - an_slug = paths.slugify(asset_data['name']) - - tempdir = tempfile.mkdtemp() - - thumb_path = os.path.join(tempdir,an_slug) - - args_dict = { - "type": "material", - "asset_name": asset_data['name'], - "asset_data": asset_data, - "thumbnail_path": thumb_path, - "tempdir": tempdir, - "do_download": True, - "upload_after_render": True, - } - thumbnail_args = { - "thumbnail_type": self.thumbnail_generator_type, - "thumbnail_scale": self.thumbnail_scale, - "thumbnail_background": self.thumbnail_background, - "thumbnail_background_lightness": self.thumbnail_background_lightness, - "thumbnail_resolution": self.thumbnail_resolution, - "thumbnail_samples": self.thumbnail_samples, - "thumbnail_denoising": self.thumbnail_denoising, - "adaptive_subdivision": self.adaptive_subdivision, - "texture_size_meters": utils.get_param(asset_data, 'textureSizeMeters', 1.0), - } - args_dict.update(thumbnail_args) - start_material_thumbnailer(self, - json_args=args_dict, - wait=False) - - return {'FINISHED'} - - def invoke(self, context, event): - # scene = bpy.context.scene - # ui_props = bpy.context.window_manager.blenderkitUI - # if ui_props.active_index > -1: - # sr = bpy.context.window_manager['search results'] - # self.asset_data = dict(sr[ui_props.active_index]) - # else: - # - # active_asset = utils.get_active_asset_by_type(asset_type = self.asset_type) - # self.asset_data = active_asset.get('asset_data') - - wm = context.window_manager - return wm.invoke_props_dialog(self) - - -def register_thumbnailer(): - bpy.utils.register_class(GenerateThumbnailOperator) - bpy.utils.register_class(ReGenerateThumbnailOperator) - bpy.utils.register_class(GenerateMaterialThumbnailOperator) - bpy.utils.register_class(ReGenerateMaterialThumbnailOperator) - - -def unregister_thumbnailer(): - bpy.utils.unregister_class(GenerateThumbnailOperator) - bpy.utils.unregister_class(ReGenerateThumbnailOperator) - bpy.utils.unregister_class(GenerateMaterialThumbnailOperator) - bpy.utils.unregister_class(ReGenerateMaterialThumbnailOperator) diff --git a/blenderkit/autothumb_material_bg.py b/blenderkit/autothumb_material_bg.py deleted file mode 100644 index 37d7c783..00000000 --- a/blenderkit/autothumb_material_bg.py +++ /dev/null @@ -1,171 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - - -from blenderkit import utils, append_link, bg_blender, upload_bg, download - -import sys, json, math, os -import bpy -from pathlib import Path - - -BLENDERKIT_EXPORT_DATA = sys.argv[-1] - - -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') - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - - with open(BLENDERKIT_EXPORT_DATA, 'r',encoding='utf-8') as s: - data = json.load(s) - # append_material(file_name, matname = None, link = False, fake_user = True) - if data.get('do_download'): - #need to save the file, so that asset doesn't get downloaded into addon directory - temp_blend_path = os.path.join(data['tempdir'], 'temp.blend') - - # if this isn't here, blender crashes. - bpy.context.preferences.filepaths.file_preview_type = 'NONE' - - bpy.ops.wm.save_as_mainfile(filepath=temp_blend_path) - - asset_data = data['asset_data'] - has_url = download.get_download_url(asset_data, download.get_scene_id(), user_preferences.api_key, tcom=None, - resolution='blend') - if not has_url: - bg_blender.progress("couldn't download asset for thumnbail re-rendering") - exit() - # download first, or rather make sure if it's already downloaded - bg_blender.progress('downloading asset') - fpath = download.download_asset_file(asset_data) - data['filepath'] = fpath - - mat = append_link.append_material(file_name=data['filepath'], matname=data["asset_name"], link=True, - fake_user=False) - - - s = bpy.context.scene - - colmapdict = { - 'BALL': 'Ball', - 'BALL_COMPLEX': 'Ball complex', - 'FLUID': 'Fluid', - 'CLOTH': 'Cloth', - 'HAIR': 'Hair' - } - unhide_collection(colmapdict[data["thumbnail_type"]]) - if data['thumbnail_background']: - unhide_collection('Background') - bpy.data.materials["bg checker colorable"].node_tree.nodes['input_level'].outputs['Value'].default_value \ - = data['thumbnail_background_lightness'] - tscale = data["thumbnail_scale"] - scaler = bpy.context.view_layer.objects['scaler'] - scaler.scale = (tscale, tscale, tscale) - utils.activate(scaler) - bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) - - bpy.context.view_layer.update() - - for ob in bpy.context.visible_objects: - if ob.name[:15] == 'MaterialPreview': - utils.activate(ob) - bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) - - ob.material_slots[0].material = mat - ob.data.use_auto_texspace = False - 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', 'BALL_COMPLEX', 'CLOTH']: - utils.automap(ob.name, tex_size = ts / tscale, just_scale = True, bg_exception=True) - bpy.context.view_layer.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.resolution_x = int(data['thumbnail_resolution']) - bpy.context.scene.render.resolution_y = int(data['thumbnail_resolution']) - - bpy.context.scene.render.filepath = data['thumbnail_path'] - bg_blender.progress('rendering thumbnail') - # bpy.ops.wm.save_as_mainfile(filepath='C:/tmp/test.blend') - # fal - render_thumbnails() - if data.get('upload_after_render') and data.get('asset_data'): - bg_blender.progress('uploading thumbnail') - preferences = bpy.context.preferences.addons['blenderkit'].preferences - - file = { - "type": "thumbnail", - "index": 0, - "file_path": data['thumbnail_path'] + '.png' - } - upload_data = { - "name": data['asset_data']['name'], - "token": preferences.api_key, - "id": data['asset_data']['id'] - } - upload_bg.upload_file(upload_data, file) - 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 deleted file mode 100644 index 2ce76830..00000000 --- a/blenderkit/autothumb_model_bg.py +++ /dev/null @@ -1,207 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - - -from blenderkit import utils, append_link, bg_blender, download, upload_bg, upload - -import sys, json, math, os -import bpy -import mathutils - -BLENDERKIT_EXPORT_DATA = sys.argv[-1] - - -def get_obnames(): - with open(BLENDERKIT_EXPORT_DATA, 'r',encoding='utf-8') 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] - if parent.type == 'EMPTY' and parent.instance_collection is not None: - obs = parent.instance_collection.objects[:] - - 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.view_layer.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) - bpy.context.view_layer.update() - - -def render_thumbnails(): - bpy.ops.render.render(write_still=True, animation=False) - - -if __name__ == "__main__": - try: - with open(BLENDERKIT_EXPORT_DATA, 'r',encoding='utf-8') as s: - data = json.load(s) - - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - - - if data.get('do_download'): - # if this isn't here, blender crashes. - bpy.context.preferences.filepaths.file_preview_type = 'NONE' - - #need to save the file, so that asset doesn't get downloaded into addon directory - temp_blend_path = os.path.join(data['tempdir'], 'temp.blend') - bpy.ops.wm.save_as_mainfile(filepath = temp_blend_path) - - bg_blender.progress('Downloading asset') - asset_data = data['asset_data'] - has_url = download.get_download_url(asset_data, download.get_scene_id(), user_preferences.api_key, tcom=None, - resolution='blend') - if not has_url == True: - bg_blender.progress("couldn't download asset for thumnbail re-rendering") - # download first, or rather make sure if it's already downloaded - bg_blender.progress('downloading asset') - fpath = download.download_asset_file(asset_data) - data['filepath'] = fpath - main_object, allobs = append_link.link_collection(fpath, - location=(0,0,0), - rotation=(0,0,0), - link=True, - name=asset_data['name'], - parent=None) - allobs = [main_object] - else: - bg_blender.progress('preparing thumbnail scene') - - obnames = get_obnames() - main_object, allobs = append_link.append_objects(file_name=data['filepath'], - obnames=obnames, - link=True) - bpy.context.view_layer.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 = data['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.view_layer.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() - - bpy.context.scene.render.resolution_x = int(data['thumbnail_resolution']) - bpy.context.scene.render.resolution_y = int(data['thumbnail_resolution']) - - bg_blender.progress('rendering thumbnail') - render_thumbnails() - fpath = data['thumbnail_path'] + '.jpg' - if data.get('upload_after_render') and data.get('asset_data'): - # try to patch for the sake of older assets where thumbnail update doesn't work for the reasont - # that original thumbnail files aren't available. - # upload.patch_individual_metadata(data['asset_data']['id'], {}, user_preferences) - bg_blender.progress('uploading thumbnail') - file = { - "type": "thumbnail", - "index": 0, - "file_path": fpath - } - upload_data = { - "name": data['asset_data']['name'], - "token": user_preferences.api_key, - "id": data['asset_data']['id'] - } - - upload_bg.upload_file(upload_data, file) - - 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 deleted file mode 100644 index 8495c076..00000000 --- a/blenderkit/bg_blender.py +++ /dev/null @@ -1,278 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import utils - -import bpy -import sys, threading, os -import re - -from bpy.props import ( - EnumProperty, -) - -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. - this threads basically waits for a stdout line to come in, - fills the data, dies.''' - found = False - while not found: - if tcom.proc.poll() is not None: - #process terminated - return - 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) - try: - sys.stdout.write('progress{%s%s}\n' % (text, n)) - - sys.stdout.flush() - except Exception as e: - print('background progress reporting race condition') - print(e) - - -# @bpy.app.handlers.persistent -def bg_update(): - '''monitoring of background process''' - text = '' - #utils.p('timer search') - # utils.p('start bg_blender timer bg_update') - - s = bpy.context.scene - - global bg_processes - if len(bg_processes) == 0: - # utils.p('end bg_blender timer bg_update') - - return 2 - #cleanup dead processes first - remove_processes = [] - for p in bg_processes: - if p[1].proc.poll() is not None: - remove_processes.append(p) - for p in remove_processes: - bg_processes.remove(p) - - #Parse process output - for p in bg_processes: - # proc=p[1].proc - readthread = p[0] - tcom = p[1] - if not readthread.is_alive(): - readthread.join() - # readthread. - estring = None - if tcom.error: - estring = tcom.eval_path_computing + ' = False' - tcom.lasttext = tcom.outtext - if tcom.outtext != '': - tcom.outtext = '' - text =tcom.lasttext.replace("'","") - estring = tcom.eval_path_state + ' = text' - # print(tcom.lasttext) - if 'finished successfully' in tcom.lasttext: - bg_processes.remove(p) - estring = tcom.eval_path_computing + ' = False' - else: - readthread = threading.Thread(target=threadread, args=([tcom]), daemon=True) - readthread.start() - p[0] = readthread - if estring: - try: - exec(estring) - except Exception as e: - print('Exception while reading from background process') - print(e) - - # if len(bg_processes) == 0: - # bpy.app.timers.unregister(bg_update) - if len(bg_processes) > 0: - # utils.p('end bg_blender timer bg_update') - - return .3 - # utils.p('end bg_blender timer bg_update') - - return 1. - - -process_types = ( - ('UPLOAD', 'Upload', ''), - ('THUMBNAILER', 'Thumbnailer', ''), -) - -process_sources = ( - ('MODEL', 'Model', 'set of objects'), - ('SCENE', 'Scene', 'set of scenes'), - ('HDR', 'HDR', 'HDR image'), - ('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'} - - 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) - kill = False - #TODO HDR - add killing of process - 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 == 'Scene' and self.process_source == 'SCENE': - if source.name == bpy.context.scene.name: - kill = True - if source.bl_rna.name == 'Image' and self.process_source == 'HDR': - ui_props = bpy.context.window_manager.blenderkitUI - if source.name == ui_props.hdr_upload_image.name: - kill = False - - 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 register(): - bpy.utils.register_class(KillBgProcess) - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.use_timers and not bpy.app.background: - bpy.app.timers.register(bg_update) - - -def unregister(): - bpy.utils.unregister_class(KillBgProcess) - if bpy.app.timers.is_registered(bg_update): - bpy.app.timers.unregister(bg_update) diff --git a/blenderkit/bkit_oauth.py b/blenderkit/bkit_oauth.py deleted file mode 100644 index b4e944a3..00000000 --- a/blenderkit/bkit_oauth.py +++ /dev/null @@ -1,201 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import tasks_queue, utils, paths, search, categories, oauth, ui, ui_panels, colors, reports - -import bpy - -import threading -import requests -import time -import logging - - -bk_logger = logging.getLogger('blenderkit') - -from bpy.props import ( - BoolProperty, -) - -CLIENT_ID = "IdFRwa3SGA8eMpzhRVFMg5Ts8sPK93xBjif93x0F" -PORTS = [62485, 65425, 55428, 49452, 35452, 25152, 5152, 1234] - -active_authenticator = None - - -def login_thread(signup=False): - global active_authenticator - r_url = paths.get_oauth_landing_url() - url = paths.get_bkit_url() - authenticator = oauth.SimpleOAuthAuthenticator(server_url=url, client_id=CLIENT_ID, ports=PORTS) - # we store authenticator globally to be able to ping the server if connection fails. - active_authenticator = authenticator - thread = threading.Thread(target=login, args=([signup, url, r_url, authenticator]), daemon=True) - thread.start() - - -def login(signup, url, r_url, authenticator): - try: - auth_token, refresh_token, oauth_response = authenticator.get_new_token(register=signup, redirect_url=r_url) - except Exception as e: - tasks_queue.add_task((reports.add_report, (e, 20, colors.RED))) - - bk_logger.debug('tokens retrieved') - tasks_queue.add_task((write_tokens, (auth_token, refresh_token, oauth_response))) - - -def refresh_token_thread(): - preferences = bpy.context.preferences.addons['blenderkit'].preferences - if len(preferences.api_key_refresh) > 0 and preferences.refresh_in_progress == False: - preferences.refresh_in_progress = True - url = paths.get_bkit_url() - thread = threading.Thread(target=refresh_token, args=([preferences.api_key_refresh, url]), daemon=True) - thread.start() - else: - reports.add_report('Already Refreshing token, will be ready soon. If this fails, please login again in Login panel.') - - -def refresh_token(api_key_refresh, url): - authenticator = oauth.SimpleOAuthAuthenticator(server_url=url, client_id=CLIENT_ID, ports=PORTS) - auth_token, refresh_token, oauth_response = authenticator.get_refreshed_token(api_key_refresh) - if auth_token is not None and refresh_token is not None: - tasks_queue.add_task((write_tokens, (auth_token, refresh_token, oauth_response))) - return auth_token, refresh_token, oauth_response - - -def write_tokens(auth_token, refresh_token, oauth_response): - bk_logger.debug('writing tokens') - preferences = bpy.context.preferences.addons['blenderkit'].preferences - preferences.api_key_refresh = refresh_token - preferences.api_key = auth_token - preferences.api_key_timeout = time.time() + oauth_response['expires_in'] - preferences.api_key_life = oauth_response['expires_in'] - preferences.login_attempt = False - preferences.refresh_in_progress = False - props = utils.get_search_props() - if props is not None: - props.report = '' - reports.add_report('BlenderKit Re-Login success') - search.get_profile() - - categories.fetch_categories_thread(auth_token, force = False) - - -class RegisterLoginOnline(bpy.types.Operator): - """Login online on BlenderKit webpage""" - - bl_idname = "wm.blenderkit_login" - bl_label = "BlenderKit login/signup" - bl_options = {'REGISTER', 'UNDO'} - - signup: BoolProperty( - name="create a new account", - description="True for register, otherwise login", - default=False, - options={'SKIP_SAVE'} - ) - - message: bpy.props.StringProperty( - name="Message", - description="", - default="You were logged out from BlenderKit.\n Clicking OK takes you to web login. ") - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout - utils.label_multiline(layout, text=self.message, width = 300) - - def execute(self, context): - preferences = bpy.context.preferences.addons['blenderkit'].preferences - preferences.login_attempt = True - login_thread(self.signup) - return {'FINISHED'} - - def invoke(self, context, event): - wm = bpy.context.window_manager - preferences = bpy.context.preferences.addons['blenderkit'].preferences - preferences.api_key_refresh = '' - preferences.api_key = '' - return wm.invoke_props_dialog(self) - - -class Logout(bpy.types.Operator): - """Logout from BlenderKit immediately""" - - bl_idname = "wm.blenderkit_logout" - bl_label = "BlenderKit logout" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - preferences = bpy.context.preferences.addons['blenderkit'].preferences - preferences.login_attempt = False - preferences.api_key_refresh = '' - preferences.api_key = '' - if bpy.context.window_manager.get('bkit profile'): - del (bpy.context.window_manager['bkit profile']) - return {'FINISHED'} - - -class CancelLoginOnline(bpy.types.Operator): - """Cancel login attempt""" - - bl_idname = "wm.blenderkit_login_cancel" - bl_label = "BlenderKit login cancel" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - global active_authenticator - preferences = bpy.context.preferences.addons['blenderkit'].preferences - preferences.login_attempt = False - try: - if active_authenticator is not None: - requests.get(active_authenticator.redirect_uri) - active_authenticator = None - except Exception as e: - print('stopped login attempt') - print(e) - return {'FINISHED'} - - -classes = ( - RegisterLoginOnline, - CancelLoginOnline, - Logout, -) - - -def register(): - for c in classes: - bpy.utils.register_class(c) - - -def unregister(): - for c in classes: - bpy.utils.unregister_class(c) diff --git a/blenderkit/bl_ui_widgets/__init__.py b/blenderkit/bl_ui_widgets/__init__.py deleted file mode 100644 index a1db444d..00000000 --- a/blenderkit/bl_ui_widgets/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -bl_info = { - "name": "BL UI Widgets", - "description": "UI Widgets to draw in the 3D view", - "author": "Jayanam", - "version": (0, 6, 4, 2), - "blender": (2, 80, 0), - "location": "View3D", - "category": "Object"} - -# Blender imports -import bpy - -from bpy.props import * - - -addon_keymaps = [] - -def register(): - - bpy.utils.register_class(DP_OT_draw_operator) - kcfg = bpy.context.window_manager.keyconfigs.addon - if kcfg: - km = kcfg.keymaps.new(name='3D View', space_type='VIEW_3D') - - - addon_keymaps.append((km, kmi)) - -def unregister(): - for km, kmi in addon_keymaps: - km.keymap_items.remove(kmi) - addon_keymaps.clear() - - bpy.utils.unregister_class(DP_OT_draw_operator) - -if __name__ == "__main__": - register() diff --git a/blenderkit/bl_ui_widgets/bl_ui_button.py b/blenderkit/bl_ui_widgets/bl_ui_button.py deleted file mode 100644 index db81b9f8..00000000 --- a/blenderkit/bl_ui_widgets/bl_ui_button.py +++ /dev/null @@ -1,205 +0,0 @@ -from . bl_ui_widget import * - -import blf -import bpy - -class BL_UI_Button(BL_UI_Widget): - - def __init__(self, x, y, width, height): - super().__init__(x, y, width, height) - self._text_color = (1.0, 1.0, 1.0, 1.0) - self._hover_bg_color = (0.5, 0.5, 0.5, 1.0) - self._select_bg_color = (0.7, 0.7, 0.7, 1.0) - - self._text = "Button" - self._text_size = 16 - self._textpos = (x, y) - - self.__state = 0 - self.__image = None - self.__image_size = (24, 24) - self.__image_position = (4, 2) - - @property - def text_color(self): - return self._text_color - - @text_color.setter - def text_color(self, value): - self._text_color = value - - @property - def text(self): - return self._text - - @text.setter - def text(self, value): - self._text = value - - @property - def text_size(self): - return self._text_size - - @text_size.setter - def text_size(self, value): - self._text_size = value - - @property - def hover_bg_color(self): - return self._hover_bg_color - - @hover_bg_color.setter - def hover_bg_color(self, value): - self._hover_bg_color = value - - @property - def select_bg_color(self): - return self._select_bg_color - - @select_bg_color.setter - def select_bg_color(self, value): - self._select_bg_color = value - - def set_image_size(self, imgage_size): - self.__image_size = imgage_size - - def set_image_position(self, image_position): - self.__image_position = image_position - - def set_image(self, rel_filepath): - #first try to access the image, for cases where it can get removed - try: - self.__image - self.__image.filepath - self.__image.pixels - except: - self.__image = None - try: - if self.__image is None or self.__image.filepath != rel_filepath: - self.__image = bpy.data.images.load(rel_filepath, check_existing=True) - self.__image.gl_load() - - if self.__image and len(self.__image.pixels) == 0: - self.__image.reload() - self.__image.gl_load() - - except Exception as e: - self.__image = None - - def update(self, x, y): - super().update(x, y) - self._textpos = [x, y] - - def draw(self): - if not self._is_visible: - return - area_height = self.get_area_height() - - self.shader.bind() - - self.set_colors() - - bgl.glEnable(bgl.GL_BLEND) - - self.batch_panel.draw(self.shader) - - self.draw_image() - - bgl.glDisable(bgl.GL_BLEND) - - # Draw text - self.draw_text(area_height) - - def set_colors(self): - color = self._bg_color - text_color = self._text_color - - # pressed - if self.__state == 1: - color = self._select_bg_color - - # hover - elif self.__state == 2: - color = self._hover_bg_color - - self.shader.uniform_float("color", color) - - def draw_text(self, area_height): - font_id = 1 - blf.size(font_id, self._text_size, 72) - size = blf.dimensions(0, self._text) - - textpos_y = area_height - self._textpos[1] - (self.height + size[1]) / 2.0 - blf.position(font_id, self._textpos[0] + (self.width - size[0]) / 2.0, textpos_y + 1, 0) - - r, g, b, a = self._text_color - blf.color(font_id, r, g, b, a) - - blf.draw(font_id, self._text) - - def draw_image(self): - if self.__image is not None: - try: - y_screen_flip = self.get_area_height() - self.y_screen - - off_x, off_y = self.__image_position - sx, sy = self.__image_size - - # bottom left, top left, top right, bottom right - vertices = ( - (self.x_screen + off_x, y_screen_flip - off_y), - (self.x_screen + off_x, y_screen_flip - sy - off_y), - (self.x_screen + off_x + sx, y_screen_flip - sy - off_y), - (self.x_screen + off_x + sx, y_screen_flip - off_y)) - - self.shader_img = gpu.shader.from_builtin('2D_IMAGE') - self.batch_img = batch_for_shader(self.shader_img, 'TRI_FAN', - { "pos" : vertices, - "texCoord": ((0, 1), (0, 0), (1, 0), (1, 1)) - },) - - # send image to gpu if it isn't there already - if self.__image.gl_load(): - raise Exception() - - bgl.glActiveTexture(bgl.GL_TEXTURE0) - bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.__image.bindcode) - - self.shader_img.bind() - self.shader_img.uniform_int("image", 0) - self.batch_img.draw(self.shader_img) - return True - except: - pass - - return False - - def set_mouse_down(self, mouse_down_func): - self.mouse_down_func = mouse_down_func - - def mouse_down(self, x, y): - if self.is_in_rect(x,y): - self.__state = 1 - try: - self.mouse_down_func(self) - except Exception as e: - print(e) - - return True - - return False - - def mouse_move(self, x, y): - if self.is_in_rect(x,y): - if(self.__state != 1): - - # hover state - self.__state = 2 - else: - self.__state = 0 - - def mouse_up(self, x, y): - if self.is_in_rect(x,y): - self.__state = 2 - else: - self.__state = 0 diff --git a/blenderkit/bl_ui_widgets/bl_ui_drag_panel.py b/blenderkit/bl_ui_widgets/bl_ui_drag_panel.py deleted file mode 100644 index 1c318bab..00000000 --- a/blenderkit/bl_ui_widgets/bl_ui_drag_panel.py +++ /dev/null @@ -1,59 +0,0 @@ -from . bl_ui_widget import * - -class BL_UI_Drag_Panel(BL_UI_Widget): - - def __init__(self, x, y, width, height): - super().__init__(x,y, width, height) - self.drag_offset_x = 0 - self.drag_offset_y = 0 - self.is_drag = False - self.widgets = [] - - def set_location(self, x, y): - super().set_location(x,y) - self.layout_widgets() - - def add_widget(self, widget): - self.widgets.append(widget) - - def add_widgets(self, widgets): - self.widgets = widgets - self.layout_widgets() - - - def layout_widgets(self): - for widget in self.widgets: - widget.update(self.x_screen + widget.x, self.y_screen + widget.y) - - def update(self, x, y): - super().update(x - self.drag_offset_x, y + self.drag_offset_y) - - def child_widget_focused(self, x, y): - for widget in self.widgets: - if widget.is_in_rect(x, y): - return True - return False - - def mouse_down(self, x, y): - if self.child_widget_focused(x, y): - return False - - if self.is_in_rect(x,y): - height = self.get_area_height() - self.is_drag = True - self.drag_offset_x = x - self.x_screen - self.drag_offset_y = y - (height - self.y_screen) - return True - - return False - - def mouse_move(self, x, y): - if self.is_drag: - height = self.get_area_height() - self.update(x, height - y) - self.layout_widgets() - - def mouse_up(self, x, y): - self.is_drag = False - self.drag_offset_x = 0 - self.drag_offset_y = 0 diff --git a/blenderkit/bl_ui_widgets/bl_ui_draw_op.py b/blenderkit/bl_ui_widgets/bl_ui_draw_op.py deleted file mode 100644 index bc2e9cb5..00000000 --- a/blenderkit/bl_ui_widgets/bl_ui_draw_op.py +++ /dev/null @@ -1,93 +0,0 @@ -import bpy - -from bpy.types import Operator - -class BL_UI_OT_draw_operator(Operator): - bl_idname = "object.bl_ui_ot_draw_operator" - bl_label = "bl ui widgets operator" - bl_description = "Operator for bl ui widgets" - bl_options = {'REGISTER'} - - def __init__(self): - self.draw_handle = None - self.draw_event = None - self._finished = False - - self.widgets = [] - - def init_widgets(self, context, widgets): - self.widgets = widgets - for widget in self.widgets: - widget.init(context) - - def on_invoke(self, context, event): - pass - - def on_finish(self, context): - self._finished = True - - def invoke(self, context, event): - - self.on_invoke(context, event) - - args = (self, context) - - self.register_handlers(args, context) - - context.window_manager.modal_handler_add(self) - - self.active_window_pointer = context.window.as_pointer() - self.active_area_pointer = context.area.as_pointer() - self.active_region_pointer = context.region.as_pointer() - return {"RUNNING_MODAL"} - - def register_handlers(self, args, context): - self.draw_handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, args, "WINDOW", "POST_PIXEL") - self.draw_event = context.window_manager.event_timer_add(0.1, window=context.window) - - def unregister_handlers(self, context): - - context.window_manager.event_timer_remove(self.draw_event) - - bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, "WINDOW") - - self.draw_handle = None - self.draw_event = None - - def handle_widget_events(self, event): - result = False - for widget in self.widgets: - if widget.handle_event(event): - result = True - return result - - def modal(self, context, event): - - if self._finished: - return {'FINISHED'} - - if context.area: - context.area.tag_redraw() - - if self.handle_widget_events(event): - return {'RUNNING_MODAL'} - - if event.type in {"ESC"}: - self.finish() - - return {"PASS_THROUGH"} - - def finish(self): - self.unregister_handlers(bpy.context) - self.on_finish(bpy.context) - - # Draw handler to paint onto the screen - def draw_callback_px(self, op, context): - try: - if context.area.as_pointer() == self.active_area_pointer: - for widget in self.widgets: - widget.draw() - except: - pass; - # context.window_manager.event_timer_remove(self.draw_event) - # bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, "WINDOW")
\ No newline at end of file diff --git a/blenderkit/bl_ui_widgets/bl_ui_image.py b/blenderkit/bl_ui_widgets/bl_ui_image.py deleted file mode 100644 index a11c52c5..00000000 --- a/blenderkit/bl_ui_widgets/bl_ui_image.py +++ /dev/null @@ -1,97 +0,0 @@ -from . bl_ui_widget import * - -import blf -import bpy - -class BL_UI_Image(BL_UI_Widget): - - def __init__(self, x, y, width, height): - super().__init__(x, y, width, height) - - self.__state = 0 - self.__image = None - self.__image_size = (24, 24) - self.__image_position = (4, 2) - - def set_image_size(self, imgage_size): - self.__image_size = imgage_size - - def set_image_position(self, image_position): - self.__image_position = image_position - - def set_image(self, rel_filepath): - try: - if self.__image is None or self.__image.filepath != rel_filepath: - self.__image = bpy.data.images.load(rel_filepath, check_existing=True) - self.__image.gl_load() - except: - pass - - def update(self, x, y): - super().update(x, y) - - def draw(self): - if not self._is_visible: - return - - area_height = self.get_area_height() - - self.shader.bind() - - bgl.glEnable(bgl.GL_BLEND) - - self.batch_panel.draw(self.shader) - - self.draw_image() - - bgl.glDisable(bgl.GL_BLEND) - - - def draw_image(self): - if self.__image is not None: - try: - y_screen_flip = self.get_area_height() - self.y_screen - - off_x, off_y = self.__image_position - sx, sy = self.__image_size - - # bottom left, top left, top right, bottom right - vertices = ( - (self.x_screen + off_x, y_screen_flip - off_y), - (self.x_screen + off_x, y_screen_flip - sy - off_y), - (self.x_screen + off_x + sx, y_screen_flip - sy - off_y), - (self.x_screen + off_x + sx, y_screen_flip - off_y)) - - self.shader_img = gpu.shader.from_builtin('2D_IMAGE') - self.batch_img = batch_for_shader(self.shader_img, 'TRI_FAN', - { "pos" : vertices, - "texCoord": ((0, 1), (0, 0), (1, 0), (1, 1)) - },) - - # send image to gpu if it isn't there already - if self.__image.gl_load(): - raise Exception() - - bgl.glActiveTexture(bgl.GL_TEXTURE0) - bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.__image.bindcode) - - self.shader_img.bind() - self.shader_img.uniform_int("image", 0) - self.batch_img.draw(self.shader_img) - return True - except: - pass - - return False - - def set_mouse_down(self, mouse_down_func): - self.mouse_down_func = mouse_down_func - - def mouse_down(self, x, y): - return False - - def mouse_move(self, x, y): - return - - def mouse_up(self, x, y): - return diff --git a/blenderkit/bl_ui_widgets/bl_ui_label.py b/blenderkit/bl_ui_widgets/bl_ui_label.py deleted file mode 100644 index 37ba09f6..00000000 --- a/blenderkit/bl_ui_widgets/bl_ui_label.py +++ /dev/null @@ -1,72 +0,0 @@ -from . bl_ui_widget import * - -import blf - -class BL_UI_Label(BL_UI_Widget): - - def __init__(self, x, y, width, height): - super().__init__(x, y, width, height) - - self._text_color = (1.0, 1.0, 1.0, 1.0) - self._text = "Label" - self._text_size = 16 - self._ralign = 'LEFT' - self._valign = 'TOP' - - @property - def text_color(self): - return self._text_color - - @text_color.setter - def text_color(self, value): - self._text_color = value - - @property - def text(self): - return self._text - - @text.setter - def text(self, value): - self._text = value - - @property - def text_size(self): - return self._text_size - - @text_size.setter - def text_size(self, value): - self._text_size = value - - def is_in_rect(self, x, y): - return False - - def draw(self): - if not self._is_visible: - return - - - area_height = self.get_area_height() - - font_id = 1 - blf.size(font_id, self._text_size, 72) - size = blf.dimensions(font_id, self._text) - - textpos_y = area_height - self.y_screen - self.height - - r, g, b, a = self._text_color - x = self.x_screen - y = textpos_y - if self._halign != 'LEFT': - width, height = blf.dimensions(font_id, self._text) - if self._halign == 'RIGHT': - x -= width - elif self._halign == 'CENTER': - x -= width // 2 - if self._valign == 'CENTER': - y -= height // 2 - # bottom could be here but there's no reason for it - blf.position(font_id, x, y, 0) - - blf.color(font_id, r, g, b, a) - - blf.draw(font_id, self._text) diff --git a/blenderkit/bl_ui_widgets/bl_ui_widget.py b/blenderkit/bl_ui_widgets/bl_ui_widget.py deleted file mode 100644 index 90fefddf..00000000 --- a/blenderkit/bl_ui_widgets/bl_ui_widget.py +++ /dev/null @@ -1,195 +0,0 @@ -import gpu -import bgl - -from gpu_extras.batch import batch_for_shader - -class BL_UI_Widget: - - def __init__(self, x, y, width, height): - self.x = x - self.y = y - self.x_screen = x - self.y_screen = y - self.width = width - self.height = height - self._bg_color = (0.8, 0.8, 0.8, 1.0) - self._tag = None - self.context = None - self.__inrect = False - self._mouse_down = False - self._mouse_down_right = False - self._is_visible = True - - def set_location(self, x, y): - self.x = x - self.y = y - self.x_screen = x - self.y_screen = y - self.update(x,y) - - @property - def bg_color(self): - return self._bg_color - - @bg_color.setter - def bg_color(self, value): - self._bg_color = value - - @property - def visible(self): - return self._is_visible - - @visible.setter - def visible(self, value): - self._is_visible = value - - @property - def tag(self): - return self._tag - - @tag.setter - def tag(self, value): - self._tag = value - - def draw(self): - if not self._is_visible: - return - - self.shader.bind() - self.shader.uniform_float("color", self._bg_color) - - bgl.glEnable(bgl.GL_BLEND) - self.batch_panel.draw(self.shader) - bgl.glDisable(bgl.GL_BLEND) - - def init(self, context): - self.context = context - self.update(self.x, self.y) - - def update(self, x, y): - - area_height = self.get_area_height() - self.x_screen = x - self.y_screen = y - - indices = ((0, 1, 2), (0, 2, 3)) - - y_screen_flip = area_height - self.y_screen - - # bottom left, top left, top right, bottom right - vertices = ( - (self.x_screen, y_screen_flip), - (self.x_screen, y_screen_flip - self.height), - (self.x_screen + self.width, y_screen_flip - self.height), - (self.x_screen + self.width, y_screen_flip)) - - self.shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') - self.batch_panel = batch_for_shader(self.shader, 'TRIS', {"pos" : vertices}, indices=indices) - - def handle_event(self, event): - if not self._is_visible: - return False - x = event.mouse_region_x - y = event.mouse_region_y - - if (event.type == 'LEFTMOUSE'): - if (event.value == 'PRESS'): - self._mouse_down = True - return self.mouse_down(x, y) - else: - self._mouse_down = False - self.mouse_up(x, y) - - elif (event.type == 'RIGHTMOUSE'): - - if (event.value == 'PRESS'): - self._mouse_down_right = True - return self.mouse_down_right(x, y) - else: - self._mouse_down_right = False - self.mouse_up(x, y) - - elif (event.type == 'MOUSEMOVE'): - self.mouse_move(x, y) - inrect = self.is_in_rect(x, y) - - # we enter the rect - if not self.__inrect and inrect: - self.__inrect = True - self.mouse_enter(event, x, y) - - # we are leaving the rect - elif self.__inrect and not inrect: - self.__inrect = False - self.mouse_exit(event, x, y) - - return False - - elif event.value == 'PRESS' and self.__inrect and (event.ascii != '' or event.type in self.get_input_keys()): - - return self.text_input(event) - - return False - - def get_input_keys(self) : - return [] - - def get_area_height(self): - return self.context.area.height - - def is_in_rect(self, x, y): - area_height = self.get_area_height() - - widget_y = area_height - self.y_screen - if ( - (self.x_screen <= x <= (self.x_screen + self.width)) and - (widget_y >= y >= (widget_y - self.height)) - ): - # print('is in rect!?') - # print('area height', area_height) - # print ('x sceen ',self.x_screen,'x ', x, 'width', self.width) - # print ('widghet y', widget_y,'y', y, 'height',self.height) - return True - - return False - - def text_input(self, event): - return False - - def mouse_down(self, x, y): - return self.is_in_rect(x,y) - - def mouse_down_right(self, x, y): - return self.is_in_rect(x,y) - - def mouse_up(self, x, y): - pass - - def set_mouse_enter(self, mouse_enter_func): - self.mouse_enter_func = mouse_enter_func - - def call_mouse_enter(self): - try: - if self.mouse_enter_func: - self.mouse_enter_func(self) - except: - pass - - def mouse_enter(self, event, x, y): - self.call_mouse_enter() - - def set_mouse_exit(self, mouse_exit_func): - self.mouse_exit_func = mouse_exit_func - - def call_mouse_exit(self): - try: - if self.mouse_exit_func: - self.mouse_exit_func(self) - except: - pass - - def mouse_exit(self, event, x, y): - self.call_mouse_exit() - - def mouse_move(self, x, y): - pass diff --git a/blenderkit/blendfiles/cleaned.blend b/blenderkit/blendfiles/cleaned.blend Binary files differdeleted file mode 100644 index 4ad5f57c..00000000 --- a/blenderkit/blendfiles/cleaned.blend +++ /dev/null diff --git a/blenderkit/blendfiles/material_thumbnailer_cycles.blend b/blenderkit/blendfiles/material_thumbnailer_cycles.blend Binary files differdeleted file mode 100644 index 1faa5807..00000000 --- a/blenderkit/blendfiles/material_thumbnailer_cycles.blend +++ /dev/null diff --git a/blenderkit/blendfiles/thumbnailer.blend b/blenderkit/blendfiles/thumbnailer.blend Binary files differdeleted file mode 100644 index 6903130d..00000000 --- a/blenderkit/blendfiles/thumbnailer.blend +++ /dev/null diff --git a/blenderkit/categories.py b/blenderkit/categories.py deleted file mode 100644 index c97c339c..00000000 --- a/blenderkit/categories.py +++ /dev/null @@ -1,274 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import paths, utils, tasks_queue, rerequests, ui, colors, reports - -import requests -import json -import os -import bpy -import time - -import shutil -import threading -import logging - -bk_logger = logging.getLogger('blenderkit') - - -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): - ''' filter categories with no assets, so they aren't shown in search panel''' - 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_path(categories, category): - '''finds the category in all possible subcategories and returns the path to it''' - category_path = [] - check_categories = categories[:] - parents = {} - while len(check_categories) > 0: - ccheck = check_categories.pop() - # print(ccheck['name']) - if not ccheck.get('children'): - continue - - for ch in ccheck['children']: - # print(ch['name']) - parents[ch['slug']] = ccheck['slug'] - - if ch['slug'] == category: - category_path = [ch['slug']] - slug = ch['slug'] - while parents.get(slug): - slug = parents.get(slug) - category_path.insert(0, slug) - return category_path - check_categories.append(ch) - return category_path - -def get_category_name_path(categories, category): - '''finds the category in all possible subcategories and returns the path to it''' - category_path = [] - check_categories = categories[:] - parents = {} - # utils.pprint(categories) - while len(check_categories) > 0: - ccheck = check_categories.pop() - # print(ccheck['name']) - if not ccheck.get('children'): - continue - - for ch in ccheck['children']: - # print(ch['name']) - parents[ch['slug']] = ccheck - - if ch['slug'] == category: - category_path = [ch['name']] - slug = ch['slug'] - while parents.get(slug): - parent = parents.get(slug) - slug = parent['slug'] - - category_path.insert(0, parent['name']) - return category_path - check_categories.append(ch) - return category_path - -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 get_upload_asset_type(self): -# typemapper = { -# bpy.types.Object.blenderkit: 'model', -# bpy.types.Scene.blenderkit: 'scene', -# bpy.types.Image.blenderkit: 'hdr', -# bpy.types.Material.blenderkit: 'material', -# bpy.types.Brush.blenderkit: 'brush' -# } -# asset_type = typemapper[type(self)] -# return asset_type - -def update_category_enums(self, context): - '''Fixes if lower level is empty - sets it to None, because enum value can be higher.''' - enums = get_subcategory_enums(self, context) - if enums[0][0] == 'NONE' and self.subcategory != 'NONE': - self.subcategory = 'NONE' - - -def update_subcategory_enums(self, context): - '''Fixes if lower level is empty - sets it to None, because enum value can be higher.''' - enums = get_subcategory1_enums(self, context) - if enums[0][0] == 'NONE' and self.subcategory1 != 'NONE': - self.subcategory1 = 'NONE' - - -def get_category_enums(self, context): - wm = bpy.context.window_manager - props = bpy.context.window_manager.blenderkitUI - asset_type = props.asset_type.lower() - # asset_type = self.asset_type#get_upload_asset_type(self) - asset_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'])) - if len(items) == 0: - items.append(('NONE', '', 'no categories on this level defined')) - return items - - -def get_subcategory_enums(self, context): - wm = bpy.context.window_manager - props = bpy.context.window_manager.blenderkitUI - asset_type = props.asset_type.lower() - items = [] - if self.category != '': - asset_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'])) - if len(items) == 0: - items.append(('NONE', '', 'no categories on this level defined')) - # print('subcategory', items) - return items - - -def get_subcategory1_enums(self, context): - wm = bpy.context.window_manager - props = bpy.context.window_manager.blenderkitUI - asset_type = props.asset_type.lower() - items = [] - if self.category != '' and self.subcategory != '': - asset_categories = get_category(wm['bkit_categories'], cat_path=(asset_type, self.category, self.subcategory,)) - if asset_categories: - for c in asset_categories['children']: - items.append((c['slug'], c['name'], c['description'])) - if len(items) == 0: - items.append(('NONE', '', 'no categories on this level defined')) - return items - - -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 load_categories(): - copy_categories() - tempdir = paths.get_temp_dir() - categories_filepath = os.path.join(tempdir, 'categories.json') - - wm = bpy.context.window_manager - try: - with open(categories_filepath, 'r', encoding='utf-8') as catfile: - wm['bkit_categories'] = json.load(catfile) - - wm['active_category'] = { - 'MODEL': ['model'], - 'SCENE': ['scene'], - 'HDR': ['hdr'], - 'MATERIAL': ['material'], - 'BRUSH': ['brush'], - } - except: - print('categories failed to read') - - -# -catfetch_counter = 0 - - -def fetch_categories(API_key, force=False): - url = paths.get_api_url() + 'categories/' - - headers = utils.get_headers(API_key) - - tempdir = paths.get_temp_dir() - categories_filepath = os.path.join(tempdir, 'categories.json') - if os.path.exists(categories_filepath): - catfile_age = time.time() - os.path.getmtime(categories_filepath) - else: - catfile_age = 10000000 - - # global catfetch_counter - # catfetch_counter += 1 - # bk_logger.debug('fetching categories: ', catfetch_counter) - # bk_logger.debug('age of cat file', catfile_age) - try: - # read categories only once per day maximum, or when forced to do so. - if catfile_age > 86400 or force: - bk_logger.debug('requesting categories from server') - r = rerequests.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', encoding='utf-8') as s: - json.dump(categories, s, ensure_ascii=False, indent=4) - tasks_queue.add_task((load_categories, ())) - except Exception as e: - t = 'BlenderKit failed to download fresh categories from the server' - tasks_queue.add_task((reports.add_report(),(t, 15, colors.RED))) - bk_logger.debug(t) - bk_logger.exception(e) - 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, force=False): - cat_thread = threading.Thread(target=fetch_categories, args=([API_key, force]), daemon=True) - cat_thread.start() diff --git a/blenderkit/colors.py b/blenderkit/colors.py deleted file mode 100644 index fe2fb1ac..00000000 --- a/blenderkit/colors.py +++ /dev/null @@ -1,25 +0,0 @@ -# ##### 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 ##### - -# this module defines color palette for BlenderKit UI - -WHITE = (1, 1, 1, .9) - -TEXT = (.9, .9, .9, .6) -GREEN = (.9, 1, .9, .6) -RED = (1, .5, .5, .8) diff --git a/blenderkit/comments_utils.py b/blenderkit/comments_utils.py deleted file mode 100644 index 654bbb54..00000000 --- a/blenderkit/comments_utils.py +++ /dev/null @@ -1,232 +0,0 @@ -# ##### 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 ##### - -# mainly update functions and callbacks for ratings properties, here to avoid circular imports. -import bpy -from blenderkit import utils, paths, tasks_queue, rerequests - -import threading -import requests -import logging - -bk_logger = logging.getLogger('blenderkit') - - -def upload_comment_thread(url, comment='', api_key=None): - ''' Upload rating thread function / disconnected from blender data.''' - headers = utils.get_headers(api_key) - - bk_logger.debug('upload comment ' + comment) - - # rating_url = url + rating_name + '/' - data = { - "content_type": "", - "object_pk": "", - "timestamp": "", - "security_hash": "", - "honeypot": "", - "name": "", - "email": "", - "url": "", - "comment": comment, - "followup": False, - "reply_to": None - } - - # try: - r = rerequests.put(url, data=data, verify=True, headers=headers) - # print(r) - # print(dir(r)) - # print(r.text) - # except requests.exceptions.RequestException as e: - # print('ratings upload failed: %s' % str(e)) - - -def upload_comment_flag_thread( asset_id = '', comment_id='', flag='like', api_key=None): - ''' Upload rating thread function / disconnected from blender data.''' - headers = utils.get_headers(api_key) - - bk_logger.debug('upload comment flag' + str(comment_id)) - - # rating_url = url + rating_name + '/' - data = { - "comment": comment_id, - "flag": flag, - } - url = paths.get_api_url() + 'comments/feedback/' - - # try: - r = rerequests.post(url, data=data, verify=True, headers=headers) - # print(r.text) - - #here it's important we read back, so likes are updated accordingly: - get_comments(asset_id, api_key) - - -def send_comment_flag_to_thread(asset_id = '', comment_id='', flag='like', api_key = None): - '''Sens rating into thread rating, main purpose is for tasks_queue. - One function per property to avoid lost data due to stashing.''' - thread = threading.Thread(target=upload_comment_flag_thread, args=(asset_id, comment_id, flag, api_key)) - thread.start() - -def send_comment_to_thread(url, comment, api_key): - '''Sens rating into thread rating, main purpose is for tasks_queue. - One function per property to avoid lost data due to stashing.''' - thread = threading.Thread(target=upload_comment_thread, args=(url, comment, api_key)) - thread.start() - - -def store_comments_local(asset_id, comments): - context = bpy.context - ac = context.window_manager.get('asset comments', {}) - ac[asset_id] = comments - context.window_manager['asset comments'] = ac - - -def get_comments_local(asset_id): - context = bpy.context - context.window_manager['asset comments'] = context.window_manager.get('asset comments', {}) - comments = context.window_manager['asset comments'].get(asset_id) - if comments: - return comments - return None - -def get_comments_thread(asset_id, api_key): - thread = threading.Thread(target=get_comments, args=([asset_id, api_key]), daemon=True) - thread.start() - -def get_comments(asset_id, api_key): - ''' - Retrieve comments from BlenderKit server. Can be run from a thread - Parameters - ---------- - asset_id - headers - - Returns - ------- - ratings - dict of type:value ratings - ''' - headers = utils.get_headers(api_key) - - url = paths.get_api_url() + 'comments/assets-uuidasset/' + asset_id + '/' - params = {} - r = rerequests.get(url, params=params, verify=True, headers=headers) - if r is None: - return - # print(r.status_code) - if r.status_code == 200: - rj = r.json() - # store comments - send them to task queue - # print('retrieved comments') - # print(rj) - tasks_queue.add_task((store_comments_local, (asset_id, rj['results']))) - - # if len(rj['results'])==0: - # # store empty ratings too, so that server isn't checked repeatedly - # tasks_queue.add_task((store_rating_local_empty,(asset_id,))) - # return ratings - - -def store_notifications_count_local(all_count): - '''Store total count of notifications on server in preferences''' - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - user_preferences.notifications_counter = all_count - -def store_notifications_local(notifications): - '''Store notifications in Blender''' - bpy.context.window_manager['bkit notifications'] = notifications - -def count_all_notifications(): - '''Return count of all notifications on server''' - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - return user_preferences.notifications_counter - - -def check_notifications_read(): - '''checks if all notifications were already read, and removes them if so''' - notifications = bpy.context.window_manager.get('bkit notifications') - if notifications is None or notifications.get('count') == 0: - return True - for n in notifications['results']: - if n['unread'] == 1: - return False - bpy.context.window_manager['bkit notifications'] = None - return True - -def get_notifications_thread(api_key, all_count = 1000): - thread = threading.Thread(target=get_notifications, args=([api_key, all_count]), daemon=True) - thread.start() - -def get_notifications(api_key, all_count = 1000): - ''' - Retrieve notifications from BlenderKit server. Can be run from a thread. - - Parameters - ---------- - api_key - all_count - - Returns - ------- - ''' - headers = utils.get_headers(api_key) - - params = {} - - url = paths.get_api_url() + 'notifications/all_count/' - r = rerequests.get(url, params=params, verify=True, headers=headers) - if r.status_code ==200: - rj = r.json() - # print(rj) - # no new notifications? - if all_count >= rj['allCount']: - tasks_queue.add_task((store_notifications_count_local, ([rj['allCount']]))) - - return - url = paths.get_api_url() + 'notifications/unread/' - r = rerequests.get(url, params=params, verify=True, headers=headers) - if r is None: - return - if r.status_code == 200: - rj = r.json() - # store notifications - send them to task queue - tasks_queue.add_task((store_notifications_local, ([rj]))) - -def mark_notification_read_thread(api_key, notification_id): - thread = threading.Thread(target=mark_notification_read, args=([api_key, notification_id]), daemon=True) - thread.start() - -def mark_notification_read(api_key, notification_id): - ''' - mark notification as read - ''' - headers = utils.get_headers(api_key) - - url = paths.get_api_url() + f'notifications/mark-as-read/{notification_id}/' - params = {} - r = rerequests.get(url, params=params, verify=True, headers=headers) - if r is None: - return - # print(r.text) - # if r.status_code == 200: - # rj = r.json() - # # store notifications - send them to task queue - # print(rj) - # tasks_queue.add_task((mark_notification_read_local, ([notification_id]))) - diff --git a/blenderkit/data/categories.json b/blenderkit/data/categories.json deleted file mode 100644 index 376d26d4..00000000 --- a/blenderkit/data/categories.json +++ /dev/null @@ -1,6176 +0,0 @@ -[ - { - "name": "brush", - "slug": "brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "brush", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "anatomy", - "slug": "anatomy-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "anatomy", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 10, - "assetCountCumulative": 10 - }, - { - "name": "animal", - "slug": "animal-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "animal", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 5, - "assetCountCumulative": 5 - }, - { - "name": "art", - "slug": "art-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "art", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, - { - "name": "clothing", - "slug": "clothing-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "clothing", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 10, - "assetCountCumulative": 10 - }, - { - "name": "crack", - "slug": "crack", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "crack", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "cut", - "slug": "cut", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "cut", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "damage", - "slug": "damage", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "damage", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - }, - { - "name": "dirt", - "slug": "dirt-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "dirt", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "fabric", - "slug": "fabric-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "fabric", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 8, - "assetCountCumulative": 8 - }, - { - "name": "geometric", - "slug": "geometric", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "geometric", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 5, - "assetCountCumulative": 5 - }, - { - "name": "human", - "slug": "human-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "human", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 7, - "assetCountCumulative": 7 - }, - { - "name": "industrial", - "slug": "industrial-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "industrial", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 12, - "assetCountCumulative": 12 - }, - { - "name": "landscape", - "slug": "landscape-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "landscape", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 25, - "assetCountCumulative": 25 - }, - { - "name": "misc", - "slug": "misc", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "misc", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 10, - "assetCountCumulative": 10 - }, - { - "name": "nature", - "slug": "nature-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "nature", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "pattern", - "slug": "pattern", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "pattern", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 8, - "assetCountCumulative": 8 - }, - { - "name": "rock", - "slug": "rock-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "rock", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 8, - "assetCountCumulative": 8 - }, - { - "name": "rust", - "slug": "rust-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "rust", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "sculpture", - "slug": "sculpture-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "sculpture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, - { - "name": "stitches", - "slug": "stitches", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "stitches", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 10, - "assetCountCumulative": 10 - }, - { - "name": "stone", - "slug": "stone-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "stone", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "tree", - "slug": "tree-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "tree", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 5, - "assetCountCumulative": 5 - }, - { - "name": "wood", - "slug": "wood-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "wood", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 137, - "assetCountCumulative": 137 - }, - { - "name": "HDR", - "slug": "hdr", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "HDR", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Indoor", - "slug": "indoor", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Indoor", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Industrial", - "slug": "hdr-industrial", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Industrial", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 63, - "assetCountCumulative": 63 - }, - { - "name": "Public", - "slug": "hdr-public", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Public", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 31, - "assetCountCumulative": 31 - }, - { - "name": "Residential", - "slug": "residential", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Residential", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 85, - "assetCountCumulative": 85 - }, - { - "name": "Studio", - "slug": "hdr-studio", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Studio", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 13, - "assetCountCumulative": 13 - } - ], - "assetCount": 447, - "assetCountCumulative": 447 - }, - { - "name": "Outdoor", - "slug": "hdr-outdoor", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Outdoor", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Nature", - "slug": "hdr-nature", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Nature", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 41, - "assetCountCumulative": 41 - }, - { - "name": "Urban", - "slug": "hdr-urban", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Urban", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 80, - "assetCountCumulative": 80 - } - ], - "assetCount": 121, - "assetCountCumulative": 121 - } - ], - "assetCount": 580, - "assetCountCumulative": 580 - }, - { - "name": "material", - "slug": "material", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "materials", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "animal", - "slug": "animal-material", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "animal", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 51, - "assetCountCumulative": 51 - }, - { - "name": "asphalt", - "slug": "asphalt", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "asphalt", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 58, - "assetCountCumulative": 58 - }, - { - "name": "bricks", - "slug": "bricks", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "bricks", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 93, - "assetCountCumulative": 93 - }, - { - "name": "ceramic", - "slug": "ceramic", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "ceramic", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 30, - "assetCountCumulative": 30 - }, - { - "name": "concrete", - "slug": "concrete", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "concrete", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 132, - "assetCountCumulative": 132 - }, - { - "name": "dirt", - "slug": "dirt", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "dirt", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 37, - "assetCountCumulative": 37 - }, - { - "name": "fabric", - "slug": "fabric", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "fabric", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 205, - "assetCountCumulative": 205 - }, - { - "name": "floor", - "slug": "floor", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "floor", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 72, - "assetCountCumulative": 72 - }, - { - "name": "food", - "slug": "food-material", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "food", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 44, - "assetCountCumulative": 44 - }, - { - "name": "fx", - "slug": "fx", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "fx", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 54, - "assetCountCumulative": 54 - }, - { - "name": "glass", - "slug": "glass", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "glass", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 73, - "assetCountCumulative": 73 - }, - { - "name": "grass", - "slug": "grass", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "grass", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 15, - "assetCountCumulative": 15 - }, - { - "name": "ground", - "slug": "ground-material", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "ground", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 134, - "assetCountCumulative": 134 - }, - { - "name": "human", - "slug": "human", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "human", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 5, - "assetCountCumulative": 5 - }, - { - "name": "ice", - "slug": "ice", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "ice", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 28, - "assetCountCumulative": 28 - }, - { - "name": "leather", - "slug": "leather", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "leather", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 88, - "assetCountCumulative": 88 - }, - { - "name": "liquid", - "slug": "liquid", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "liquid", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 25, - "assetCountCumulative": 25 - }, - { - "name": "marble", - "slug": "marble", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "marble", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 14, - "assetCountCumulative": 14 - }, - { - "name": "metal", - "slug": "metal", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "metal", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 349, - "assetCountCumulative": 349 - }, - { - "name": "organic", - "slug": "organic", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "organic", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 50, - "assetCountCumulative": 50 - }, - { - "name": "ornaments", - "slug": "ornaments", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "ornaments", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 40, - "assetCountCumulative": 40 - }, - { - "name": "paper", - "slug": "paper", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "paper", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 64, - "assetCountCumulative": 64 - }, - { - "name": "paving", - "slug": "paving", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "paving", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 51, - "assetCountCumulative": 51 - }, - { - "name": "plaster", - "slug": "plaster", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "plaster", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 76, - "assetCountCumulative": 76 - }, - { - "name": "plastic", - "slug": "plastic", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "plastic", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 77, - "assetCountCumulative": 77 - }, - { - "name": "rock", - "slug": "rock", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "rock", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 55, - "assetCountCumulative": 55 - }, - { - "name": "roofing", - "slug": "roofing", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "roofing", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 24, - "assetCountCumulative": 24 - }, - { - "name": "rubber", - "slug": "rubber", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "rubber", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 12, - "assetCountCumulative": 12 - }, - { - "name": "rust", - "slug": "rust", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "rust", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 25, - "assetCountCumulative": 25 - }, - { - "name": "sand", - "slug": "sand", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "sand", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 28, - "assetCountCumulative": 28 - }, - { - "name": "soil", - "slug": "soil", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "soil", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 13, - "assetCountCumulative": 13 - }, - { - "name": "stone", - "slug": "stone", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "stone", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 153, - "assetCountCumulative": 153 - }, - { - "name": "tech", - "slug": "tech", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "tech", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 115, - "assetCountCumulative": 115 - }, - { - "name": "tiles", - "slug": "tiles", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "tiles", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 164, - "assetCountCumulative": 164 - }, - { - "name": "wood", - "slug": "wood", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "wood", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 351, - "assetCountCumulative": 351 - } - ], - "assetCount": 2805, - "assetCountCumulative": 2805 - }, - { - "name": "model", - "slug": "model", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "model", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Architecture", - "slug": "architecture", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Architecture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Building", - "slug": "building", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Building", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Commercial", - "slug": "public", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Commercial", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 39, - "assetCountCumulative": 39 - }, - { - "name": "Historic", - "slug": "historic", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Historic", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 15, - "assetCountCumulative": 15 - }, - { - "name": "Other", - "slug": "stadium", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Other", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 12, - "assetCountCumulative": 12 - }, - { - "name": "Private", - "slug": "house", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Private", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 31, - "assetCountCumulative": 31 - }, - { - "name": "Sci-fi", - "slug": "sci-fi", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sci-fi", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - } - ], - "assetCount": 112, - "assetCountCumulative": 112 - }, - { - "name": "Door", - "slug": "door", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Door", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 124, - "assetCountCumulative": 124 - }, - { - "name": "Exterior element", - "slug": "exterior", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Exterior element", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Bench", - "slug": "bench", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bench", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 55, - "assetCountCumulative": 55 - }, - { - "name": "Facade element", - "slug": "facade-element", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Facade element", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 58, - "assetCountCumulative": 58 - }, - { - "name": "Fence", - "slug": "fence", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Fence", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 32, - "assetCountCumulative": 32 - }, - { - "name": "Fountain", - "slug": "fountain", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Fountain", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - }, - { - "name": "Other", - "slug": "exterior-other", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Other", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 147, - "assetCountCumulative": 147 - }, - { - "name": "Playground", - "slug": "playground", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Playground", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 8, - "assetCountCumulative": 8 - }, - { - "name": "Swimming pool", - "slug": "swimming-pool", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Swimming pool", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 7, - "assetCountCumulative": 7 - }, - { - "name": "Urban Environment", - "slug": "cityspace", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Urban Environment", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 67, - "assetCountCumulative": 67 - } - ], - "assetCount": 384, - "assetCountCumulative": 384 - }, - { - "name": "Floor Covering", - "slug": "floor-covering", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Floor Covering", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 5, - "assetCountCumulative": 5 - }, - { - "name": "Molding / Carving", - "slug": "molding-carving", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Molding / Carving", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "Other", - "slug": "elements", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Other", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 64, - "assetCountCumulative": 64 - }, - { - "name": "Scenes", - "slug": "landmark", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Scenes", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 5, - "assetCountCumulative": 5 - }, - { - "name": "Stairs", - "slug": "stairs", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Stairs", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 23, - "assetCountCumulative": 23 - }, - { - "name": "Structure", - "slug": "street", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Structure", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 51, - "assetCountCumulative": 51 - }, - { - "name": "Wall Panel", - "slug": "wall-panel", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Wall Panel", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "3D Panel", - "slug": "3d-panel", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "3D Panel", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - }, - { - "name": "Stone Panel", - "slug": "stone-panel", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Stone Panel", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "Upholstery Panel", - "slug": "upholstery-panel", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Upholstery Panel", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "Wood Panel", - "slug": "wood-panel", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Wood Panel", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - } - ], - "assetCount": 11, - "assetCountCumulative": 11 - }, - { - "name": "Window", - "slug": "window", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Window", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 67, - "assetCountCumulative": 67 - } - ], - "assetCount": 849, - "assetCountCumulative": 849 - }, - { - "name": "Character", - "slug": "character", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Character", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Anatomy", - "slug": "anatomy", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Anatomy", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Full Body", - "slug": "full-body", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Full Body", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - }, - { - "name": "Head", - "slug": "head", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Head", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 9, - "assetCountCumulative": 9 - }, - { - "name": "Internal organ", - "slug": "internal-organ", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Internal organ", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "Limbs", - "slug": "limbs", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Limbs", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Musculature", - "slug": "musculature", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Musculature", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Skeleton", - "slug": "skeleton", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Skeleton", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - } - ], - "assetCount": 16, - "assetCountCumulative": 16 - }, - { - "name": "Animal", - "slug": "animal-nature", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Animal", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Bird", - "slug": "bird", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bird", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "Dinosaur", - "slug": "dinosaur", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Dinosaur", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Fish", - "slug": "fish", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Fish", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "Insect", - "slug": "insect", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Insect", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 6, - "assetCountCumulative": 6 - }, - { - "name": "Mammal", - "slug": "mammal", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Mammal", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 12, - "assetCountCumulative": 12 - }, - { - "name": "Other", - "slug": "animal", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Other", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "Reptile", - "slug": "reptile", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Reptile", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 25, - "assetCountCumulative": 25 - }, - { - "name": "Clothing", - "slug": "clothing", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Clothing", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Accessories", - "slug": "clothing-accessories", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Accessories", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 22, - "assetCountCumulative": 22 - }, - { - "name": "Footwear", - "slug": "footwear", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Footwear", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 25, - "assetCountCumulative": 25 - }, - { - "name": "Headwear", - "slug": "headwear", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Headwear", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 15, - "assetCountCumulative": 15 - }, - { - "name": "Lingerie", - "slug": "lingerie", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Lingerie", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Man Clothing", - "slug": "man-clothing", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Man Clothing", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 10, - "assetCountCumulative": 10 - }, - { - "name": "Woman Clothing", - "slug": "woman-clothing", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Woman Clothing", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 14, - "assetCountCumulative": 14 - } - ], - "assetCount": 87, - "assetCountCumulative": 87 - }, - { - "name": "Humanoids", - "slug": "people", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Humanoids", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Child", - "slug": "child", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Child", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 6, - "assetCountCumulative": 6 - }, - { - "name": "Fantasy Hero", - "slug": "fantasy", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Fantasy Hero", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 17, - "assetCountCumulative": 17 - }, - { - "name": "Medical", - "slug": "humanoids-medical", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Medical", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Men", - "slug": "man", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Men", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 27, - "assetCountCumulative": 27 - }, - { - "name": "Military", - "slug": "humanoids-military", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Military", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, - { - "name": "Police", - "slug": "police", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Police", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "Sci-Fi", - "slug": "sci-fi-character", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sci-Fi", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 12, - "assetCountCumulative": 12 - }, - { - "name": "Sports", - "slug": "humanoids-sports", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sports", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Women", - "slug": "woman", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Women", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 18, - "assetCountCumulative": 18 - } - ], - "assetCount": 84, - "assetCountCumulative": 84 - }, - { - "name": "Monster / Creature", - "slug": "monster-creature", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Monster / Creature", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 5, - "assetCountCumulative": 5 - }, - { - "name": "Robot", - "slug": "robot", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Robot", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 16, - "assetCountCumulative": 16 - } - ], - "assetCount": 233, - "assetCountCumulative": 233 - }, - { - "name": "Decoration", - "slug": "decoration", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Decoration", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Bag / Suitcase", - "slug": "bag-case", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bag / Suitcase", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 22, - "assetCountCumulative": 22 - }, - { - "name": "Bed sheet", - "slug": "bed-sheet", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bed sheet", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Blanket", - "slug": "blanket", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Blanket", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Book", - "slug": "literature", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Book", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 69, - "assetCountCumulative": 69 - }, - { - "name": "Carpet", - "slug": "carpet", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Carpet", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 35, - "assetCountCumulative": 35 - }, - { - "name": "Clock / Watch", - "slug": "design", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Clock / Watch", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 42, - "assetCountCumulative": 42 - }, - { - "name": "Curtain", - "slug": "curtain", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Curtain", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 35, - "assetCountCumulative": 35 - }, - { - "name": "Decoration Set", - "slug": "photo", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Decoration Set", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 45, - "assetCountCumulative": 45 - }, - { - "name": "Fabrics", - "slug": "fabrics", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Fabrics", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 10, - "assetCountCumulative": 10 - }, - { - "name": "Fireplace", - "slug": "fireplace", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Fireplace", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 29, - "assetCountCumulative": 29 - }, - { - "name": "Food / Drinks", - "slug": "food-drink", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Food / Drinks", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Beverage", - "slug": "drink", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Beverage", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 100, - "assetCountCumulative": 100 - }, - { - "name": "Food", - "slug": "food", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Food", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 62, - "assetCountCumulative": 62 - }, - { - "name": "Fruit / Vegetable", - "slug": "fruitvegetable", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Fruit/Vegetable", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 71, - "assetCountCumulative": 71 - }, - { - "name": "Kitchenware", - "slug": "container", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Kitchenware", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 164, - "assetCountCumulative": 164 - }, - { - "name": "Other", - "slug": "drugs", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Other", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 14, - "assetCountCumulative": 14 - }, - { - "name": "Sweets / Dessert", - "slug": "sweetsdessert", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sweets/Dessert", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 26, - "assetCountCumulative": 26 - }, - { - "name": "Tableware set", - "slug": "tableware-set", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Tableware set", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 157, - "assetCountCumulative": 157 - } - ], - "assetCount": 599, - "assetCountCumulative": 599 - }, - { - "name": "Holiday Decoration", - "slug": "supplies", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Holiday Decoration", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 38, - "assetCountCumulative": 38 - }, - { - "name": "Mirror", - "slug": "mirror", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Mirror", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 50, - "assetCountCumulative": 50 - }, - { - "name": "Miscellaneous", - "slug": "art", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Miscellaneous", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 272, - "assetCountCumulative": 272 - }, - { - "name": "Money", - "slug": "money", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Money", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 21, - "assetCountCumulative": 21 - }, - { - "name": "Other textile", - "slug": "other-textile", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Other textile", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 20, - "assetCountCumulative": 20 - }, - { - "name": "Picture", - "slug": "painting", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Picture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 160, - "assetCountCumulative": 160 - }, - { - "name": "Pillow", - "slug": "pillow", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Pillow", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 56, - "assetCountCumulative": 56 - }, - { - "name": "Sculpture", - "slug": "sculpture", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sculpture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 110, - "assetCountCumulative": 110 - }, - { - "name": "Vase", - "slug": "drawing", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Vase", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 132, - "assetCountCumulative": 132 - } - ], - "assetCount": 1749, - "assetCountCumulative": 1749 - }, - { - "name": "Industrial", - "slug": "industrial", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Industrial", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Container", - "slug": "container-industrial", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Container", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 83, - "assetCountCumulative": 83 - }, - { - "name": "Equipment", - "slug": "utility-industrial", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Equipment", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 17, - "assetCountCumulative": 17 - }, - { - "name": "Machinery", - "slug": "machine", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Machinery", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 23, - "assetCountCumulative": 23 - }, - { - "name": "Other", - "slug": "agriculture", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Other", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 50, - "assetCountCumulative": 50 - }, - { - "name": "Parts", - "slug": "construction", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Parts", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 81, - "assetCountCumulative": 81 - }, - { - "name": "Sign", - "slug": "communication", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sign", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 36, - "assetCountCumulative": 36 - }, - { - "name": "Tools", - "slug": "tool", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Tools", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Handtools", - "slug": "handtools", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Handtools", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 34, - "assetCountCumulative": 34 - }, - { - "name": "Powertools", - "slug": "powertools", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Powertools", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 16, - "assetCountCumulative": 16 - } - ], - "assetCount": 50, - "assetCountCumulative": 50 - } - ], - "assetCount": 340, - "assetCountCumulative": 340 - }, - { - "name": "Interior", - "slug": "interior", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Interior", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Armchair", - "slug": "furniture", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Armchair", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 223, - "assetCountCumulative": 223 - }, - { - "name": "Bathroom furniture", - "slug": "bathroom", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bathroom furniture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Accessories", - "slug": "utility", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Accessories", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 93, - "assetCountCumulative": 93 - }, - { - "name": "Bathhub", - "slug": "bathhub", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bathhub", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 19, - "assetCountCumulative": 19 - }, - { - "name": "Faucet", - "slug": "bathroomfurniture-faucet", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Faucet", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 21, - "assetCountCumulative": 21 - }, - { - "name": "Furniture Set", - "slug": "bathroomfurniture-furniture-set", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Furniture Set", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 19, - "assetCountCumulative": 19 - }, - { - "name": "Laundry", - "slug": "laundry", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Laundry", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 11, - "assetCountCumulative": 11 - }, - { - "name": "Shower", - "slug": "shower", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Shower", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 21, - "assetCountCumulative": 21 - }, - { - "name": "Toilet / Bidet", - "slug": "toilet-bidet", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Toilet / Bidet", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 19, - "assetCountCumulative": 19 - }, - { - "name": "Towel rail", - "slug": "towel-rail", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Towel rail", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 10, - "assetCountCumulative": 10 - }, - { - "name": "Wash Basin", - "slug": "wash-basin", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Wash Basin", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 35, - "assetCountCumulative": 35 - } - ], - "assetCount": 255, - "assetCountCumulative": 255 - }, - { - "name": "Bed", - "slug": "bed", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bed", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 57, - "assetCountCumulative": 57 - }, - { - "name": "Cabinets", - "slug": "cabinets", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Cabinets", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Bookcase", - "slug": "bookcase", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bookcase", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 31, - "assetCountCumulative": 31 - }, - { - "name": "Commode", - "slug": "commode", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Commode", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 108, - "assetCountCumulative": 108 - }, - { - "name": "Shelving", - "slug": "shelving", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Shelving", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 103, - "assetCountCumulative": 103 - }, - { - "name": "TV Cabinets", - "slug": "tv-cabinets", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "TV Cabinets", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 43, - "assetCountCumulative": 43 - } - ], - "assetCount": 327, - "assetCountCumulative": 327 - }, - { - "name": "Chair", - "slug": "chair", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Chair", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Bar Chair", - "slug": "bar-chair", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bar Chair", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 72, - "assetCountCumulative": 72 - }, - { - "name": "Regular Chair", - "slug": "regular-chair", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Regular Chair", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 265, - "assetCountCumulative": 265 - } - ], - "assetCount": 364, - "assetCountCumulative": 364 - }, - { - "name": "Console", - "slug": "bedroom", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Console", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 39, - "assetCountCumulative": 39 - }, - { - "name": "Dressing Table", - "slug": "living-room", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Dressing Table", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 13, - "assetCountCumulative": 13 - }, - { - "name": "Kids furniture", - "slug": "kids-room", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Kids furniture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Bed", - "slug": "kidsfurniture-bed", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bed", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 14, - "assetCountCumulative": 14 - }, - { - "name": "Chair", - "slug": "kidsfurniture-chair", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Chair", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, - { - "name": "Furniture Set", - "slug": "furniture-set", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Furniture Set", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 8, - "assetCountCumulative": 8 - }, - { - "name": "Miscellaneous", - "slug": "miscellaneous", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Miscellaneous", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 13, - "assetCountCumulative": 13 - }, - { - "name": "Table", - "slug": "tablechair", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Table", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "Toy", - "slug": "toy", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Toy", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 37, - "assetCountCumulative": 37 - }, - { - "name": "Wardrobe", - "slug": "kidsfurniture-wardrobe", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Wardrobe", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - } - ], - "assetCount": 82, - "assetCountCumulative": 82 - }, - { - "name": "Kitchen Furniture", - "slug": "kitchen", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Kitchen Furniture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Faucet", - "slug": "faucet", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Faucet", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 33, - "assetCountCumulative": 33 - }, - { - "name": "Kitchen Appliance", - "slug": "kitchen-appliance", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Kitchen Appliance", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 126, - "assetCountCumulative": 126 - }, - { - "name": "Kitchen Set", - "slug": "kitchen-set", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Kitchen Set", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 43, - "assetCountCumulative": 43 - }, - { - "name": "Sink", - "slug": "sink", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sink", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 18, - "assetCountCumulative": 18 - }, - { - "name": "Storage", - "slug": "storage", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Storage", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 127, - "assetCountCumulative": 127 - } - ], - "assetCount": 361, - "assetCountCumulative": 361 - }, - { - "name": "Lights", - "slug": "lighting", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Lights", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Ceiling Light", - "slug": "ceiling-light", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Ceiling Light", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 187, - "assetCountCumulative": 187 - }, - { - "name": "Floor Lamp", - "slug": "floor-lamp", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Floor Lamp", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 57, - "assetCountCumulative": 57 - }, - { - "name": "IES Light", - "slug": "ies-light", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "IES Light", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 10, - "assetCountCumulative": 10 - }, - { - "name": "Industrial Light", - "slug": "industrial-light", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Industrial Light", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 19, - "assetCountCumulative": 19 - }, - { - "name": "Outdoor Light", - "slug": "outdoor-light", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Outdoor Light", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 36, - "assetCountCumulative": 36 - }, - { - "name": "Table Lamp", - "slug": "table-lamps", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Table Lamp", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 99, - "assetCountCumulative": 99 - }, - { - "name": "Wall Light", - "slug": "wall-light", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Wall Light", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 51, - "assetCountCumulative": 51 - } - ], - "assetCount": 480, - "assetCountCumulative": 480 - }, - { - "name": "Office Furniture", - "slug": "office", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Office Furniture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Chair", - "slug": "office-chair", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Chair", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 26, - "assetCountCumulative": 26 - }, - { - "name": "Desk", - "slug": "desk", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Desk", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 63, - "assetCountCumulative": 63 - }, - { - "name": "Stationery", - "slug": "stationery", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Stationery", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 52, - "assetCountCumulative": 52 - }, - { - "name": "Storage", - "slug": "office-storage", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Storage", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 32, - "assetCountCumulative": 32 - }, - { - "name": "Table", - "slug": "office-table", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Table", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - } - ], - "assetCount": 186, - "assetCountCumulative": 186 - }, - { - "name": "Outdoor Furniture", - "slug": "outdoor-furniture", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Outdoor Furniture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 36, - "assetCountCumulative": 36 - }, - { - "name": "Pouf", - "slug": "pouf", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Pouf", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 43, - "assetCountCumulative": 43 - }, - { - "name": "Restaurant / Bar", - "slug": "restaurant-bar", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Restaurant / Bar", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 40, - "assetCountCumulative": 40 - }, - { - "name": "Seating Set", - "slug": "seating", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Seating Set", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Chair-table Set", - "slug": "chair-table-set", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Chair-table Set", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 18, - "assetCountCumulative": 18 - }, - { - "name": "Sofa-table Set", - "slug": "sofa-table-set", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sofa-table Set", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 5, - "assetCountCumulative": 5 - } - ], - "assetCount": 31, - "assetCountCumulative": 31 - }, - { - "name": "Shopping / Retail", - "slug": "shopping-retail", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Shopping / Retail", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, - { - "name": "Sideboard / Drawers Chest", - "slug": "hall", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sideboard / Drawers Chest", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 124, - "assetCountCumulative": 124 - }, - { - "name": "Sofa", - "slug": "sofa", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sofa", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 208, - "assetCountCumulative": 208 - }, - { - "name": "Table", - "slug": "table", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Table", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 444, - "assetCountCumulative": 444 - }, - { - "name": "Wardrobe", - "slug": "wardrobe", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Wardrobe", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 62, - "assetCountCumulative": 62 - } - ], - "assetCount": 3378, - "assetCountCumulative": 3378 - }, - { - "name": "Military", - "slug": "military", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Military", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Aircraft", - "slug": "air", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Aircraft", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 15, - "assetCountCumulative": 15 - }, - { - "name": "Vehicles", - "slug": "ground", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Vehicles", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - }, - { - "name": "Watercraft", - "slug": "naval", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Watercraft", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "Weapon / Armor", - "slug": "weapon", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Weapon / Armor", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Historic", - "slug": "historic-military", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Historic", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 67, - "assetCountCumulative": 67 - }, - { - "name": "Modern", - "slug": "equipment", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Modern", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 47, - "assetCountCumulative": 47 - }, - { - "name": "Sci-Fi", - "slug": "military-sci-fi", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sci-Fi", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 22, - "assetCountCumulative": 22 - } - ], - "assetCount": 144, - "assetCountCumulative": 144 - } - ], - "assetCount": 164, - "assetCountCumulative": 164 - }, - { - "name": "Nature", - "slug": "nature", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Nature", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Atmosphere", - "slug": "atmosphere", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Atmosphere", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Cloud", - "slug": "weather", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Cloud", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, - { - "name": "Fog", - "slug": "fog", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Fog", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Smoke / Fire", - "slug": "smoke-fire", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Smoke / Fire", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "Wind Setup", - "slug": "wind-setup", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Wind Setup", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 5, - "assetCountCumulative": 5 - }, - { - "name": "Grass", - "slug": "nature-grass", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Grass", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 30, - "assetCountCumulative": 30 - }, - { - "name": "Landscape", - "slug": "landscape-nature", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Landscape", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Environment Elements", - "slug": "environment-elements", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Environment Elements", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 128, - "assetCountCumulative": 128 - }, - { - "name": "Terrain", - "slug": "landscape", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Terrain", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 26, - "assetCountCumulative": 26 - } - ], - "assetCount": 155, - "assetCountCumulative": 155 - }, - { - "name": "Plant", - "slug": "plant", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Plant", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Bouquet", - "slug": "bouquet", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bouquet", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 12, - "assetCountCumulative": 12 - }, - { - "name": "Fitowall", - "slug": "fitowall", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Fitowall", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - }, - { - "name": "Indoor", - "slug": "nature-indoor", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Indoor", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 109, - "assetCountCumulative": 109 - }, - { - "name": "Outdoor", - "slug": "nature-outdoor", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Outdoor", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 73, - "assetCountCumulative": 73 - } - ], - "assetCount": 201, - "assetCountCumulative": 201 - }, - { - "name": "Tree", - "slug": "tree", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Tree", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 246, - "assetCountCumulative": 246 - } - ], - "assetCount": 638, - "assetCountCumulative": 638 - }, - { - "name": "Science", - "slug": "science", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Science", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Lab Equipment", - "slug": "medicine", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Lab Equipment", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 15, - "assetCountCumulative": 15 - }, - { - "name": "Medical Equipment", - "slug": "medical", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Medical Equipment", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 8, - "assetCountCumulative": 8 - }, - { - "name": "Microbiology", - "slug": "microbiology", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Microbiology", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 6, - "assetCountCumulative": 6 - }, - { - "name": "Miscellaneous", - "slug": "science-miscellaneous", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Miscellaneous", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 21, - "assetCountCumulative": 21 - }, - { - "name": "Pharmacy", - "slug": "pharmacy", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Pharmacy", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 10, - "assetCountCumulative": 10 - } - ], - "assetCount": 60, - "assetCountCumulative": 60 - }, - { - "name": "Space", - "slug": "space", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Space", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Miscellaneous", - "slug": "sci-fi-space", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Miscellaneous", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 6, - "assetCountCumulative": 6 - }, - { - "name": "Planet", - "slug": "planets", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Planet", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 31, - "assetCountCumulative": 31 - }, - { - "name": "Satellite", - "slug": "satellite", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Satellite", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, - { - "name": "Spacecraft", - "slug": "spacecraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Spacecraft", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 23, - "assetCountCumulative": 23 - }, - { - "name": "Station", - "slug": "astronomy", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Station", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 63, - "assetCountCumulative": 63 - }, - { - "name": "Sport / Hobby", - "slug": "sports", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sport / Hobby", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Fishing", - "slug": "outdoor", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Fishing", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Gym", - "slug": "individual", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Gym", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 43, - "assetCountCumulative": 43 - }, - { - "name": "Hobby Accessories", - "slug": "team", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Hobby Accessories", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 21, - "assetCountCumulative": 21 - }, - { - "name": "Miscellaneous", - "slug": "exercise", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Miscellaneous", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 30, - "assetCountCumulative": 30 - }, - { - "name": "Music", - "slug": "music", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Music", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Accessories", - "slug": "accessories", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Accessories", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 22, - "assetCountCumulative": 22 - }, - { - "name": "Instruments", - "slug": "instruments", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Instruments", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 36, - "assetCountCumulative": 36 - }, - { - "name": "Stage", - "slug": "stage", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Stage", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "Studio", - "slug": "studio", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Studio", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - } - ], - "assetCount": 65, - "assetCountCumulative": 65 - }, - { - "name": "Sport", - "slug": "extreme", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sport", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 50, - "assetCountCumulative": 50 - } - ], - "assetCount": 209, - "assetCountCumulative": 209 - }, - { - "name": "Technology", - "slug": "technology", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Technology", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Audio Devices", - "slug": "audio", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Audio Devices", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 79, - "assetCountCumulative": 79 - }, - { - "name": "Computer", - "slug": "computer", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Computer", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Components / Hardware", - "slug": "components-hardware", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Components / Hardware", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 88, - "assetCountCumulative": 88 - }, - { - "name": "Desktop", - "slug": "desktop", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Desktop", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 16, - "assetCountCumulative": 16 - }, - { - "name": "Game Console", - "slug": "game-console", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Game Console", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 15, - "assetCountCumulative": 15 - }, - { - "name": "Keyboard", - "slug": "keyboard", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Keyboard", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 11, - "assetCountCumulative": 11 - }, - { - "name": "Laptop", - "slug": "laptop", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Laptop", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 17, - "assetCountCumulative": 17 - }, - { - "name": "Monitor", - "slug": "monitor", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Monitor", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 12, - "assetCountCumulative": 12 - }, - { - "name": "Mouse", - "slug": "mouse", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Mouse", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 6, - "assetCountCumulative": 6 - }, - { - "name": "Peripheral", - "slug": "peripheral", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Peripheral", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 10, - "assetCountCumulative": 10 - } - ], - "assetCount": 180, - "assetCountCumulative": 180 - }, - { - "name": "Devices", - "slug": "devices", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Devices", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Celullar Phone", - "slug": "celullar-phone", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Celullar Phone", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - }, - { - "name": "Corded Phone", - "slug": "corded-phone", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Corded Phone", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 6, - "assetCountCumulative": 6 - }, - { - "name": "Smartphone", - "slug": "phone", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Smartphone", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 23, - "assetCountCumulative": 23 - }, - { - "name": "Smart Watch", - "slug": "smart-watch", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Smart Watch", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Tablet", - "slug": "tablet", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Tablet", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 6, - "assetCountCumulative": 6 - } - ], - "assetCount": 39, - "assetCountCumulative": 39 - }, - { - "name": "Household Appliances", - "slug": "household-appliances", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Household Appliances", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 50, - "assetCountCumulative": 50 - }, - { - "name": "Miscellaneous", - "slug": "industrial-exterior", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Miscellaneous", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 170, - "assetCountCumulative": 170 - }, - { - "name": "Photography", - "slug": "photography", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Photography", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 20, - "assetCountCumulative": 20 - }, - { - "name": "Robotics", - "slug": "ai", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Robotics", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 47, - "assetCountCumulative": 47 - }, - { - "name": "Video devices", - "slug": "video", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Video devices", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 21, - "assetCountCumulative": 21 - } - ], - "assetCount": 606, - "assetCountCumulative": 606 - }, - { - "name": "Transport", - "slug": "vehicle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Transport", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Aircraft", - "slug": "aircraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Aircraft", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Accessories / Part", - "slug": "part-aircraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Accessories / Part", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - }, - { - "name": "Air Baloon", - "slug": "air-baloon", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Air Baloon", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Airplane", - "slug": "commercial", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Airplane", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, - { - "name": "Drone", - "slug": "drone", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Drone", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, - { - "name": "Glider", - "slug": "glider", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Glider", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Helicopter", - "slug": "helicopter", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Helicopter", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, - { - "name": "Historic Plane", - "slug": "historic-aircraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Historic Plane", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Private Jet", - "slug": "private", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Private Jet", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Seaplane", - "slug": "jet", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Seaplane", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - } - ], - "assetCount": 16, - "assetCountCumulative": 16 - }, - { - "name": "Bicycle", - "slug": "bicycle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bicycle", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 9, - "assetCountCumulative": 9 - }, - { - "name": "Car", - "slug": "car", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Car", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Buggy", - "slug": "buggy", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Buggy", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "Concept", - "slug": "concept", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Concept", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 17, - "assetCountCumulative": 17 - }, - { - "name": "Historical", - "slug": "historic-vehicle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Historical", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 14, - "assetCountCumulative": 14 - }, - { - "name": "Luxury / Supercar", - "slug": "luxury-supercar", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Luxury / Supercar", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 27, - "assetCountCumulative": 27 - }, - { - "name": "Racing", - "slug": "racing", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Racing", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "Sci-Fi", - "slug": "transport-sci-fi", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sci-Fi", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - }, - { - "name": "Standard", - "slug": "standard", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Standard", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 40, - "assetCountCumulative": 40 - } - ], - "assetCount": 112, - "assetCountCumulative": 112 - }, - { - "name": "Emergency", - "slug": "emergency", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Emergency", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Ambulance", - "slug": "ambulance", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Ambulance", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Fire Department", - "slug": "fire-department", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Fire Department", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Police", - "slug": "transport-police", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Police", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - } - ], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "Heavy Vehicle", - "slug": "heavy-vehicle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Heavy Vehicle", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Industrial", - "slug": "industrial-vehicle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Industrial", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - }, - { - "name": "Trailer", - "slug": "trailer", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Trailer", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Truck", - "slug": "truck", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Truck", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, - { - "name": "Van", - "slug": "van", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Van", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - } - ], - "assetCount": 10, - "assetCountCumulative": 10 - }, - { - "name": "Motocycle", - "slug": "motorcycle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Motocycle", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Historical", - "slug": "historical", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Historical", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Sport", - "slug": "sport", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Sport", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Standard", - "slug": "transport-standard", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Standard", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - } - ], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "Public Transport", - "slug": "public-transport", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Public Transport", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Bus", - "slug": "bus", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Bus", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "Taxi", - "slug": "taxi", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Taxi", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "Railed vehicle", - "slug": "railed-vehicle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Railed vehicle", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Cargo", - "slug": "cargo", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Cargo", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "Passenger", - "slug": "train", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Passenger", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - } - ], - "assetCount": 5, - "assetCountCumulative": 5 - }, - { - "name": "Small Electric Vehicles", - "slug": "small-electric-vehicles", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Small Electric Vehicles", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Vehicle Parts", - "slug": "part-vehicle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Vehicle Parts", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 167, - "assetCountCumulative": 167 - }, - { - "name": "Watercraft", - "slug": "watercraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Watercraft", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Accessories / Part", - "slug": "part-watercraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Accessories / Part", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Boat", - "slug": "recreational", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Boat", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - }, - { - "name": "Hovercraft", - "slug": "hovercraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Hovercraft", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "Ship", - "slug": "industrial-watercraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Ship", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "Submarine", - "slug": "historic-watercraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Submarine", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Yacht", - "slug": "personal", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Yacht", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - } - ], - "assetCount": 7, - "assetCountCumulative": 7 - } - ], - "assetCount": 334, - "assetCountCumulative": 334 - } - ], - "assetCount": 8630, - "assetCountCumulative": 8630 - }, - { - "name": "Scene", - "slug": "scene", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "scene", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Indoor", - "slug": "scene-indoor", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Indoor", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 38, - "assetCountCumulative": 38 - }, - { - "name": "Outdoor", - "slug": "scene-outdoor", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Outdoor", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 8, - "assetCountCumulative": 8 - }, - { - "name": "templates", - "slug": "templates", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "templates", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "brush templates", - "slug": "brush-templates", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "brush templates", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 4, - "assetCountCumulative": 4 - } - ], - "assetCount": 4, - "assetCountCumulative": 4 - } - ], - "assetCount": 50, - "assetCountCumulative": 50 - }, - { - "name": "texture", - "slug": "texture", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "texture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Animals", - "slug": "animals", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Animals", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Mammals", - "slug": "mammals", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Mammals", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Plants", - "slug": "plants", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Plants", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 0, - "assetCountCumulative": 12 - } - ], - "assetCount": 0, - "assetCountCumulative": 0 - } -]
\ No newline at end of file diff --git a/blenderkit/download.py b/blenderkit/download.py deleted file mode 100644 index 7fda06a2..00000000 --- a/blenderkit/download.py +++ /dev/null @@ -1,1467 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import paths, append_link, utils, ui, colors, tasks_queue, rerequests, resolutions, ui_panels, search, reports - -import threading -import time -import requests -import shutil, sys, os -import uuid -import copy -import logging - -bk_logger = logging.getLogger('blenderkit') - -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, resolution=asset_data.get('resolution')) - 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.''' - # this is obviously broken. Blender should take care of the extra data automaticlaly - # first clean up collections - for c in bpy.data.collections: - if len(c.all_objects) == 0 and c.get('is_blenderkit_asset'): - bpy.data.collections.remove(c) - return; - 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 and l.getn('asset_data'): - 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. - if not bpy.app.background: - 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, resolution = should be here): - # 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 = utils.get_headers(api_key) - url = paths.get_api_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['assetBaseId'] - - 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']['assetBaseId'] - 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']['assetBaseId'] - 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: - bk_logger.debug('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() - mt = time.time() - mt - # print('report generation: ', mt) - - -def udpate_asset_data_in_dicts(asset_data): - ''' - updates asset data in all relevant dictionaries, after a threaded download task \ - - where the urls were retrieved, and now they can be reused - Parameters - ---------- - asset_data - data coming back from thread, thus containing also download urls - ''' - scene = bpy.context.scene - scene['assets used'] = scene.get('assets used', {}) - scene['assets used'][asset_data['assetBaseId']] = asset_data.copy() - - scene['assets rated'] = scene.get('assets rated', {}) - id = asset_data['assetBaseId'] - scene['assets rated'][id] = scene['assets rated'].get(id, False) - sr = bpy.context.window_manager['search results'] - if not sr: - return; - for i, r in enumerate(sr): - if r['assetBaseId'] == asset_data['assetBaseId']: - for f in asset_data['files']: - if f.get('url'): - for f1 in r['files']: - if f1['fileType'] == f['fileType']: - f1['url'] = f['url'] - - -def append_asset(asset_data, **kwargs): # downloaders=[], location=None, - '''Link asset to the scene. - - - ''' - file_names = paths.get_download_filepaths(asset_data, kwargs['resolution']) - 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!) - s = bpy.context.scene - wm = bpy.context.window_manager - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - - if user_preferences.api_key == '': - user_preferences.asset_counter += 1 - - if asset_data['assetType'] == 'scene': - sprops = wm.blenderkit_scene - - scene = append_link.append_scene(file_names[0], link=sprops.append_link == 'LINK', fake_user=False) - # print('scene appended') - if scene is not None: - props = scene.blenderkit - asset_main = scene - if sprops.switch_after_append: - bpy.context.window_manager.windows[0].scene = scene - - if asset_data['assetType'] == 'hdr': - hdr = append_link.load_HDR(file_name=file_names[0], name=asset_data['name']) - props = hdr.blenderkit - asset_main = hdr - - if asset_data['assetType'] == 'model': - downloaders = kwargs.get('downloaders') - sprops = wm.blenderkit_models - # TODO this is here because combinations of linking objects or appending groups are rather not-usefull - if sprops.append_method == 'LINK_COLLECTION': - sprops.append_link = 'LINK' - sprops.import_as = 'GROUP' - else: - sprops.append_link = 'APPEND' - sprops.import_as = 'INDIVIDUAL' - - # copy for override - al = sprops.append_link - # set consistency for objects already in scene, otherwise this literally breaks blender :) - ain, resolution = asset_in_scene(asset_data) - # this is commented out since it already happens in start_download function. - # if resolution: - # kwargs['resolution'] = resolution - # override based on history - if ain is not False: - if ain == 'LINKED': - al = 'LINK' - else: - al = 'APPEND' - if asset_data['assetType'] == 'model': - source_parent = get_asset_in_scene(asset_data) - if source_parent: - asset_main, new_obs = duplicate_asset(source=source_parent, **kwargs) - asset_main.location = kwargs['model_location'] - asset_main.rotation_euler = kwargs['model_rotation'] - # this is a case where asset is already in scene and should be duplicated instead. - # there is a big chance that the duplication wouldn't work perfectly(hidden or unselectable objects) - # so here we need to check and return if there was success - # also, if it was successful, no other operations are needed , basically all asset data is already ready from the original asset - if new_obs: - # update here assets rated/used because there might be new download urls? - udpate_asset_data_in_dicts(asset_data) - bpy.ops.wm.undo_push_context(message='add %s to scene' % asset_data['name']) - - return - - # first get conditions for append link - link = al == '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 '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 link: - asset_main, new_obs = append_link.link_collection(file_names[-1], - location=downloader['location'], - rotation=downloader['rotation'], - link=link, - name=asset_data['name'], - parent=kwargs.get('parent')) - - else: - - asset_main, new_obs = append_link.append_objects(file_names[-1], - location=downloader['location'], - rotation=downloader['rotation'], - link=link, - name=asset_data['name'], - parent=kwargs.get('parent')) - if asset_main.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) - asset_main.empty_display_size = size_min - - elif kwargs.get('model_location') is not None: - if link: - asset_main, new_obs = append_link.link_collection(file_names[-1], - location=kwargs['model_location'], - rotation=kwargs['model_rotation'], - link=link, - name=asset_data['name'], - parent=kwargs.get('parent')) - else: - asset_main, new_obs = append_link.append_objects(file_names[-1], - location=kwargs['model_location'], - rotation=kwargs['model_rotation'], - link=link, - name=asset_data['name'], - parent=kwargs.get('parent')) - - # scale Empty for assets, so they don't clutter the scene. - if asset_main.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) - asset_main.empty_display_size = size_min - - if link: - group = asset_main.instance_collection - - lib = group.library - lib['asset_data'] = asset_data - - elif asset_data['assetType'] == 'brush': - # TODO if already 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.view_layer.objects.active.mode == 'SCULPT': - bpy.context.tool_settings.sculpt.brush = brush - elif bpy.context.view_layer.objects.active.mode == 'TEXTURE_PAINT': # 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 - asset_main = brush - - elif asset_data['assetType'] == 'material': - inscene = False - sprops = wm.blenderkit_mat - - for m in bpy.data.materials: - if m.blenderkit.id == asset_data['id']: - inscene = True - material = m - break; - if not inscene: - link = sprops.append_method == 'LINK' - material = append_link.append_material(file_names[-1], link=link, 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 - - asset_main = material - - asset_data['resolution'] = kwargs['resolution'] - udpate_asset_data_in_dicts(asset_data) - - asset_main['asset_data'] = asset_data # TODO remove this??? should write to blenderkit Props? - asset_main.blenderkit.asset_base_id = asset_data['assetBaseId'] - asset_main.blenderkit.id = asset_data['id'] - - bpy.ops.wm.undo_push_context(message='add %s to scene' % asset_data['name']) - # moving reporting to on save. - # report_use_success(asset_data['id']) - - -def replace_resolution_linked(file_paths, asset_data): - # replace one asset resolution for another. - # this is the much simpler case - # - find the library, - # - replace the path and name of the library, reload. - file_name = os.path.basename(file_paths[-1]) - - for l in bpy.data.libraries: - if not l.get('asset_data'): - continue; - if not l['asset_data']['assetBaseId'] == asset_data['assetBaseId']: - continue; - - bk_logger.debug('try to re-link library') - - if not os.path.isfile(file_paths[-1]): - bk_logger.debug('library file doesnt exist') - break; - l.filepath = os.path.join(os.path.dirname(l.filepath), file_name) - l.name = file_name - udpate_asset_data_in_dicts(asset_data) - - -def replace_resolution_appended(file_paths, asset_data, resolution): - # In this case the texture paths need to be replaced. - # Find the file path pattern that is present in texture paths - # replace the pattern with the new one. - file_name = os.path.basename(file_paths[-1]) - - new_filename_pattern = os.path.splitext(file_name)[0] - all_patterns = [] - for suff in paths.resolution_suffix.values(): - pattern = f"{asset_data['id']}{os.sep}textures{suff}{os.sep}" - all_patterns.append(pattern) - new_pattern = f"{asset_data['id']}{os.sep}textures{paths.resolution_suffix[resolution]}{os.sep}" - - # replace the pattern with the new one. - # print(existing_filename_patterns) - # print(new_filename_pattern) - # print('existing images:') - for i in bpy.data.images: - - for old_pattern in all_patterns: - if i.filepath.find(old_pattern) > -1: - fp = i.filepath.replace(old_pattern, new_pattern) - fpabs = bpy.path.abspath(fp) - if not os.path.exists(fpabs): - # this currently handles .png's that have been swapped to .jpg's during resolution generation process. - # should probably also handle .exr's and similar others. - # bk_logger.debug('need to find a replacement') - base, ext = os.path.splitext(fp) - if resolution == 'blend' and i.get('original_extension'): - fp = base + i.get('original_extension') - elif ext in ('.png', '.PNG'): - fp = base + '.jpg' - i.filepath = fp - i.filepath_raw = fp # bpy.path.abspath(fp) - for pf in i.packed_files: - pf.filepath = fp - i.reload() - udpate_asset_data_in_dicts(asset_data) - - -# @bpy.app.handlers.persistent -def download_timer(): - # TODO might get moved to handle all blenderkit stuff, not to slow down. - ''' - check for running and finished downloads. - Running downloads get checked for progress which is passed to UI. - Finished downloads are processed and linked/appended to scene. - ''' - global download_threads - # utils.p('start download timer') - - # bk_logger.debug('timer download') - - if len(download_threads) == 0: - # utils.p('end download timer') - - return 2 - 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.window_manager.get('search results') - if sr is not None: - for r in sr: - if asset_data['id'] == r['id']: - r['downloaded'] = 0.5 # tcom.progress - if not t.is_alive(): - if tcom.error: - sprops = utils.get_search_props() - sprops.report = tcom.report - download_threads.remove(threaddata) - # utils.p('end download timer') - - return - file_paths = paths.get_download_filepaths(asset_data, tcom.passargs['resolution']) - - if len(file_paths) == 0: - bk_logger.debug('library names not found in asset data after download') - download_threads.remove(threaddata) - break; - - wm = bpy.context.window_manager - - at = asset_data['assetType'] - if ((bpy.context.mode == 'OBJECT' and \ - (at == 'model' or at == 'material'))) \ - or ((at == 'brush') \ - and wm.get('appendable') == True) or at == 'scene' or at == 'hdr': - # 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_paths) == 2: # todo this should try to check if both files exist and are ok. - utils.copy_asset(file_paths[0], file_paths[1]) - # shutil.copyfile(file_paths[0], file_paths[1]) - - bk_logger.debug('appending asset') - # progress bars: - - # 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_paths[-1] - l.reload() - - if tcom.passargs.get('replace_resolution'): - # try to relink - # HDRs are always swapped, so their swapping is handled without the replace_resolution option - - ain, resolution = asset_in_scene(asset_data) - - if ain == 'LINKED': - replace_resolution_linked(file_paths, asset_data) - - - elif ain == 'APPENDED': - replace_resolution_appended(file_paths, asset_data, tcom.passargs['resolution']) - - - - else: - done = try_finished_append(asset_data, **tcom.passargs) - if not done: - at = asset_data['assetType'] - tcom.passargs['retry_counter'] = tcom.passargs.get('retry_counter', 0) + 1 - download(asset_data, **tcom.passargs) - - if bpy.context.window_manager['search results'] is not None and done: - for sres in bpy.context.window_manager['search results']: - if asset_data['id'] == sres['id']: - sres['downloaded'] = 100 - - bk_logger.debug('finished download thread') - # utils.p('end download timer') - - return .5 - - -def delete_unfinished_file(file_name): - ''' - Deletes download if it wasn't finished. If the folder it's containing is empty, it also removes the directory - Parameters - ---------- - file_name - - Returns - ------- - None - ''' - try: - os.remove(file_name) - except Exception as e: - print(e) - asset_dir = os.path.dirname(file_name) - if len(os.listdir(asset_dir)) == 0: - os.rmdir(asset_dir) - return - - -def download_asset_file(asset_data, resolution='blend', api_key=''): - # this is a simple non-threaded way to download files for background resolution genenration tool - file_names = paths.get_download_filepaths(asset_data, resolution) # prefer global dir if possible. - if len(file_names) == 0: - return None - - file_name = file_names[0] - - if check_existing(asset_data, resolution=resolution): - # this sends the thread for processing, where another check should occur, since the file might be corrupted. - bk_logger.debug('not downloading, already in db') - return file_name - - download_canceled = False - - with open(file_name, "wb") as f: - print("Downloading %s" % file_name) - headers = utils.get_headers(api_key) - res_file_info, resolution = paths.get_res_file(asset_data, resolution) - response = requests.get(res_file_info['url'], stream=True) - total_length = response.headers.get('Content-Length') - - if total_length is None or int(total_length) < 1000: # no content length header - download_canceled = True - print(response.content) - else: - total_length = int(total_length) - dl = 0 - last_percent = 0 - percent = 0 - for data in response.iter_content(chunk_size=4096 * 10): - dl += len(data) - - # the exact output you're looking for: - fs_str = utils.files_size_to_text(total_length) - - percent = int(dl * 100 / total_length) - if percent > last_percent: - last_percent = percent - # sys.stdout.write('\r') - # sys.stdout.write(f'Downloading {asset_data['name']} {fs_str} {percent}% ') # + int(dl * 50 / total_length) * 'x') - print( - f'Downloading {asset_data["name"]} {fs_str} {percent}% ') # + int(dl * 50 / total_length) * 'x') - # sys.stdout.flush() - - # print(int(dl*50/total_length)*'x'+'\r') - f.write(data) - if download_canceled: - delete_unfinished_file(file_name) - return None - - return file_name - - -class Downloader(threading.Thread): - def __init__(self, asset_data, tcom, scene_id, api_key, resolution='blend'): - super(Downloader, self).__init__() - self.asset_data = asset_data - self.tcom = tcom - self.scene_id = scene_id - self.api_key = api_key - self.resolution = resolution - self._stop_event = threading.Event() - - def stop(self): - self._stop_event.set() - - def stopped(self): - return self._stop_event.is_set() - - # def main_download_thread(asset_data, tcom, scene_id, api_key): - def run(self): - '''try to download file from blenderkit''' - # utils.p('start downloader thread') - asset_data = self.asset_data - tcom = self.tcom - scene_id = self.scene_id - api_key = self.api_key - tcom.report = 'Looking for asset' - # TODO get real link here... - has_url = get_download_url(asset_data, scene_id, api_key, resolution=self.resolution, tcom=tcom) - - if not has_url: - tasks_queue.add_task( - (reports.add_report, ('Failed to obtain download URL for %s.' % asset_data['name'], 5, colors.RED))) - return; - if tcom.error: - return - # only now we can check if the file already 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, resolution=self.resolution) 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 - bk_logger.debug('not downloading, trying to append again') - return - - file_name = paths.get_download_filepaths(asset_data, self.resolution)[0] # prefer global dir if possible. - # for k in asset_data: - # print(asset_data[k]) - if self.stopped(): - bk_logger.debug('stopping download: ' + asset_data['name']) - return - - download_canceled = False - with open(file_name, "wb") as f: - bk_logger.debug("Downloading %s" % file_name) - headers = utils.get_headers(api_key) - res_file_info, self.resolution = paths.get_res_file(asset_data, self.resolution) - response = requests.get(res_file_info['url'], stream=True) - total_length = response.headers.get('Content-Length') - if total_length is None: # no content length header - print('no content length') - print(response.content) - tcom.report = response.content - download_canceled = True - else: - # bk_logger.debug(total_length) - if int(total_length) < 1000: # means probably no file returned. - tasks_queue.add_task((reports.add_report, (response.content, 20, colors.RED))) - - tcom.report = response.content - - tcom.file_size = int(total_length) - fsmb = tcom.file_size // (1024 * 1024) - fskb = tcom.file_size % 1024 - if fsmb == 0: - t = '%iKB' % fskb - else: - t = ' %iMB' % fsmb - - tcom.report = f'Downloading {t} {self.resolution}' - - dl = 0 - totdata = [] - for data in response.iter_content(chunk_size=4096 * 32): # crashed here... why? investigate: - dl += len(data) - tcom.downloaded = dl - tcom.progress = int(100 * tcom.downloaded / tcom.file_size) - f.write(data) - if self.stopped(): - bk_logger.debug('stopping download: ' + asset_data['name']) - download_canceled = True - break - - if download_canceled: - delete_unfinished_file(file_name) - return - # unpack the file immediately after download - - tcom.report = f'Unpacking files' - self.asset_data['resolution'] = self.resolution - resolutions.send_to_bg(self.asset_data, file_name, command='unpack') - # utils.p('end downloader thread') - - -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() - - tcom = ThreadCom() - tcom.passargs = kwargs - - if kwargs.get('retry_counter', 0) > 3: - sprops = utils.get_search_props() - report = f"Maximum retries exceeded for {asset_data['name']}" - sprops.report = report - reports.add_report(report, 5, colors.RED) - - bk_logger.debug(sprops.report) - return - - # 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() - readthread = Downloader(asset_data, tcom, scene_id, api_key, resolution=kwargs['resolution']) - readthread.start() - - global download_threads - download_threads.append( - [readthread, asset_data, tcom]) - - -def check_downloading(asset_data, **kwargs): - ''' check if an asset is already 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['assetType'] - 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, resolution='blend', can_return_others=False): - ''' check if the object exists on the hard drive''' - fexists = False - - if asset_data.get('files') == None: - # this is because of some very odl files where asset data had no files structure. - return False - - file_names = paths.get_download_filepaths(asset_data, resolution, can_return_others=can_return_others) - - bk_logger.debug('check if file already exists' + str(file_names)) - 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]) - utils.copy_asset(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. - utils.copy_asset(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 successfully delete source files. - This means probably wrong download, so download should restart''' - file_names = paths.get_download_filepaths(asset_data, kwargs['resolution']) - done = False - bk_logger.debug('try to append already 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: - # TODO: this should distinguis if the appending failed (wrong file) - # or something else happened(shouldn't delete the files) - print(e) - done = False - for f in file_names: - try: - os.remove(f) - except Exception as e: - # e = sys.exc_info()[0] - print(e) - pass; - return done - - return done - - -def get_asset_in_scene(asset_data): - '''tries to find an appended copy of particular asset and duplicate it - so it doesn't have to be appended again.''' - scene = bpy.context.scene - for ob in bpy.context.scene.objects: - ad1 = ob.get('asset_data') - if not ad1: - continue - if ad1.get('assetBaseId') == asset_data['assetBaseId']: - return ob - return None - - -def check_all_visible(obs): - '''checks all objects are visible, so they can be manipulated/copied.''' - for ob in obs: - if not ob.visible_get(): - return False - return True - - -def check_selectible(obs): - '''checks if all objects can be selected and selects them if possible. - this isn't only select_hide, but all possible combinations of collections e.t.c. so hard to check otherwise.''' - for ob in obs: - ob.select_set(True) - if not ob.select_get(): - return False - return True - - -def duplicate_asset(source, **kwargs): - ''' - Duplicate asset when it's already appended in the scene, - so that blender's append doesn't create duplicated data. - ''' - bk_logger.debug('duplicate asset instead') - # we need to save selection - sel = utils.selection_get() - bpy.ops.object.select_all(action='DESELECT') - - # check visibility - obs = utils.get_hierarchy(source) - if not check_all_visible(obs): - return None - # check selectability and select in one run - if not check_selectible(obs): - return None - - # duplicate the asset objects - bpy.ops.object.duplicate(linked=True) - - nobs = bpy.context.selected_objects[:] - # get asset main object - for ob in nobs: - if ob.parent not in nobs: - asset_main = ob - break - - # in case of replacement,there might be a paarent relationship that can be restored - if kwargs.get('parent'): - parent = bpy.data.objects[kwargs['parent']] - asset_main.parent = parent # even if parent is None this is ok without if condition - else: - asset_main.parent = None - # restore original selection - utils.selection_set(sel) - return asset_main, nobs - - -def asset_in_scene(asset_data): - '''checks if the asset is already in scene. If yes, modifies asset data so the asset can be reached again.''' - scene = bpy.context.scene - au = scene.get('assets used', {}) - - id = asset_data['assetBaseId'] - if id in au.keys(): - ad = au[id] - if ad.get('files'): - for fi in ad['files']: - if fi.get('file_name') != None: - - for fi1 in asset_data['files']: - if fi['fileType'] == fi1['fileType']: - fi1['file_name'] = fi['file_name'] - fi1['url'] = fi['url'] - - # browse all collections since linked collections can have same name. - if asset_data['assetType'] == 'MODEL': - for c in bpy.data.collections: - if c.name == ad['name']: - # there can also be more linked collections with same name, we need to check id. - if c.library and c.library.get('asset_data') and c.library['asset_data'][ - 'assetBaseId'] == id: - print('asset linked') - return 'LINKED', ad.get('resolution') - elif asset_data['assetType'] == 'MATERIAL': - for m in bpy.data.materials: - if not m.get('asset_data'): - continue - if m['asset_data']['assetBaseId'] == asset_data[ - 'assetBaseId'] and bpy.context.active_object.active_material.library: - return 'LINKED', ad.get('resolution') - - print('asset appended') - return 'APPENDED', ad.get('resolution') - return False, None - - -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, resolution='blend'): - ''''retrieves the download url. The server checks if user can download the item.''' - mt = time.time() - utils.pprint('getting download url') - - headers = utils.get_headers(api_key) - - data = { - 'scene_uuid': scene_id - } - r = None - - res_file_info, resolution = paths.get_res_file(asset_data, resolution) - - try: - r = rerequests.get(res_file_info['downloadUrl'], params=data, headers=headers) - except Exception as e: - print(e) - if tcom is not None: - tcom.error = True - if r == None: - if tcom is not None: - tcom.report = 'Connection Error' - tcom.error = True - return 'Connection Error' - - if r.status_code < 400: - data = r.json() - url = data['filePath'] - - res_file_info['url'] = url - res_file_info['file_name'] = paths.extract_filename_from_url(url) - - # print(res_file_info, url) - print(url) - return True - - # let's print it into UI - tasks_queue.add_task((reports.add_report, (str(r), 10, colors.RED))) - - if r.status_code == 403: - report = 'You need Full plan to get this item.' - # r1 = 'All materials and brushes are available for free. Only users registered to Standard plan can use all models.' - # tasks_queue.add_task((reports.add_report, (r1, 5, colors.RED))) - if tcom is not None: - tcom.report = report - tcom.error = True - - if r.status_code == 404: - report = 'Url not found - 404.' - # r1 = 'All materials and brushes are available for free. Only users registered to Standard plan can use all models.' - if tcom is not None: - tcom.report = report - tcom.error = True - - elif r.status_code >= 500: - # bk_logger.debug(r.text) - if tcom is not None: - tcom.report = 'Server error' - tcom.error = True - return False - - -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 already in scene. We can use that asset without checking with server - ain, resolution = asset_in_scene(asset_data) - # quota_ok = ain is not False - - # if resolution: - # kwargs['resolution'] = resolution - # 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 already. This check happens 2x once here(for free assets), - # once in thread(for non-free) - fexists = check_existing(asset_data, resolution=kwargs['resolution']) - bk_logger.debug('does file exist?' + str(fexists)) - bk_logger.debug('asset is in scene' + str(ain)) - if ain and not kwargs.get('replace_resolution'): - # this goes to appending asset - where it should duplicate the original asset already in scene. - done = try_finished_append(asset_data, **kwargs) - # else: - # props = utils.get_search_props() - # props.report = str('asset ') - if not done: - at = asset_data['assetType'] - if at in ('model', 'material'): - downloader = {'location': kwargs['model_location'], - 'rotation': kwargs['model_rotation']} - download(asset_data, downloaders=[downloader], **kwargs) - - else: - download(asset_data, **kwargs) - - -asset_types = ( - ('MODEL', 'Model', 'set of objects'), - ('SCENE', 'Scene', 'scene'), - ('HDR', 'Hdr', 'hdr'), - ('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 BlenderkitKillDownloadOperator(bpy.types.Operator): - """Kill a download""" - bl_idname = "scene.blenderkit_download_kill" - bl_label = "BlenderKit Kill Asset Download" - bl_options = {'REGISTER', 'INTERNAL'} - - thread_index: IntProperty(name="Thread index", description='index of the thread to kill', default=-1) - - def execute(self, context): - global download_threads - td = download_threads[self.thread_index] - download_threads.remove(td) - td[0].stop() - return {'FINISHED'} - - -def available_resolutions_callback(self, context): - ''' - Returns - checks active asset for available resolutions and offers only those available - TODO: this currently returns always the same list of resolutions, make it actually work - ''' - # print('callback called', self.asset_data) - pat_items = ( - ('512', '512', '', 1), - ('1024', '1024', '', 2), - ('2048', '2048', '', 3), - ('4096', '4096', '', 4), - ('8192', '8192', '', 5), - ) - items = [] - for item in pat_items: - if int(self.max_resolution) >= int(item[0]): - items.append(item) - items.append(('ORIGINAL', 'Original', '', 6)) - return items - - -def show_enum_values(obj, prop_name): - print([item.identifier for item in obj.bl_rna.properties[prop_name].enum_items]) - - -class BlenderkitDownloadOperator(bpy.types.Operator): - """Download and link asset to scene. Only link if asset already available locally""" - bl_idname = "scene.blenderkit_download" - bl_label = "Download" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - # 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) - - asset_base_id: StringProperty( - name="Asset base Id", - description="Asset base id, used instead of search result index", - default="") - - target_object: StringProperty( - name="Target Object", - description="Material or object target for replacement", - 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)) - - replace: BoolProperty(name='Replace', description='replace selection with the asset', default=False) - - replace_resolution: BoolProperty(name='Replace resolution', description='replace resolution of the active asset', - default=False) - - invoke_resolution: BoolProperty(name='Replace resolution popup', - description='pop up to ask which resolution to download', default=False) - invoke_scene_settings: BoolProperty(name='Scene import settings popup', - description='pop up scene import settings', default=False) - - resolution: EnumProperty( - items=available_resolutions_callback, - default=512, - description='Replace resolution' - ) - - # needs to be passed to the operator to not show all resolution possibilities - max_resolution: IntProperty( - name="Max resolution", - description="", - default=0) - # has_res_0_5k: BoolProperty(name='512', - # description='', default=False) - - cast_parent: StringProperty( - name="Particles Target Object", - description="", - default="") - - # close_window: BoolProperty(name='Close window', - # description='Try to close the window below mouse before download', - # default=False) - # @classmethod - # def poll(cls, context): - # return bpy.context.window_manager.BlenderKitModelThumbnails is not '' - tooltip: bpy.props.StringProperty( - default='Download and link asset to scene. Only link if asset already available locally') - - @classmethod - def description(cls, context, properties): - return properties.tooltip - - def get_asset_data(self, context): - # get asset data - it can come from scene, or from search results. - s = bpy.context.scene - - if self.asset_index > -1: - # either get the data from search results - sr = bpy.context.window_manager['search results'] - asset_data = sr[ - self.asset_index].to_dict() # TODO CHECK ALL OCCURRENCES OF PASSING BLENDER ID PROPS TO THREADS! - asset_base_id = asset_data['assetBaseId'] - else: - # or from the scene. - asset_base_id = self.asset_base_id - - au = s.get('assets used') - if au == None: - s['assets used'] = {} - if asset_base_id in s.get('assets used'): - # already used assets have already download link and especially file link. - asset_data = s['assets used'][asset_base_id].to_dict() - else: - # when not in scene nor in search results, we need to get it from the server - params = { - 'asset_base_id': self.asset_base_id - } - preferences = bpy.context.preferences.addons['blenderkit'].preferences - - results = search.get_search_simple(params, page_size=1, max_results=1, - api_key=preferences.api_key) - asset_data = search.parse_result(results[0]) - - return asset_data - - def execute(self, context): - sprops = utils.get_search_props() - - self.asset_data = self.get_asset_data(context) - - # print('after getting asset data') - # print(self.asset_base_id) - - atype = self.asset_data['assetType'] - if bpy.context.mode != 'OBJECT' and ( - atype == 'model' or atype == 'material') and bpy.context.view_layer.objects.active is not None: - bpy.ops.object.mode_set(mode='OBJECT') - - if self.resolution == 0 or self.resolution == '': - resolution = sprops.resolution - else: - resolution = self.resolution - - resolution = resolutions.resolution_props_to_server[resolution] - if self.replace: # cleanup first, assign later. - obs = utils.get_selected_replace_adepts() - # print(obs) - for ob in obs: - # print('replace attempt ', ob.name) - if self.asset_base_id != '': - # this is for a case when replace is called from a panel, - # this uses active object as replacement source instead of target. - if ob.get('asset_data') is not None and \ - (ob['asset_data']['assetBaseId'] == self.asset_base_id and ob['asset_data'][ - 'resolution'] == resolution): - # print('skipping this one') - continue; - parent = ob.parent - if parent: - parent = ob.parent.name # after this, parent is either name or None. - - kwargs = { - 'cast_parent': self.cast_parent, - 'target_object': ob.name, - 'material_target_slot': ob.active_material_index, - 'model_location': tuple(ob.matrix_world.translation), - 'model_rotation': tuple(ob.matrix_world.to_euler()), - 'replace': True, - 'replace_resolution': False, - 'parent': parent, - 'resolution': resolution - } - # TODO - move this After download, not before, so that the replacement - utils.delete_hierarchy(ob) - start_download(self.asset_data, **kwargs) - else: - # replace resolution needs to replace all instances of the resolution in the scene - # and deleting originals has to be thus done after the downlaod - - 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), - 'replace': False, - 'replace_resolution': self.replace_resolution, - 'resolution': resolution - } - - start_download(self.asset_data, **kwargs) - return {'FINISHED'} - - def draw(self, context): - layout = self.layout - if self.invoke_resolution: - layout.prop(self, 'resolution', expand=True, icon_only=False) - if self.invoke_scene_settings: - ui_panels.draw_scene_import_settings(self, context) - - def invoke(self, context, event): - # if self.close_window: - # context.window.cursor_warp(event.mouse_x-1000, event.mouse_y - 1000); - - # print(self.asset_base_id) - wm = context.window_manager - # only make a pop up in case of switching resolutions - if self.invoke_resolution: - # show_enum_values(self, 'resolution') - self.asset_data = self.get_asset_data(context) - sprops = utils.get_search_props() - - # set initial resolutions enum activation - if sprops.resolution != 'ORIGINAL' and int(sprops.resolution) <= int(self.max_resolution): - self.resolution = sprops.resolution - elif int(self.max_resolution) > 0: - self.resolution = str(self.max_resolution) - else: - self.resolution = 'ORIGINAL' - return wm.invoke_props_dialog(self) - - if self.invoke_scene_settings: - return wm.invoke_props_dialog(self) - # if self.close_window: - # time.sleep(0.1) - # context.area.tag_redraw() - # time.sleep(0.1) - # - # context.window.cursor_warp(event.mouse_x, event.mouse_y); - - return self.execute(context) - - -def register_download(): - bpy.utils.register_class(BlenderkitDownloadOperator) - bpy.utils.register_class(BlenderkitKillDownloadOperator) - bpy.app.handlers.load_post.append(scene_load) - bpy.app.handlers.save_pre.append(scene_save) - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.use_timers and not bpy.app.background: - bpy.app.timers.register(download_timer) - - -def unregister_download(): - bpy.utils.unregister_class(BlenderkitDownloadOperator) - bpy.utils.unregister_class(BlenderkitKillDownloadOperator) - bpy.app.handlers.load_post.remove(scene_load) - bpy.app.handlers.save_pre.remove(scene_save) - if bpy.app.timers.is_registered(download_timer): - bpy.app.timers.unregister(download_timer) diff --git a/blenderkit/icons.py b/blenderkit/icons.py deleted file mode 100644 index 379062ed..00000000 --- a/blenderkit/icons.py +++ /dev/null @@ -1,78 +0,0 @@ -# ##### 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 os -import bpy - -# We can store multiple preview collections here, -# however in this example we only store "main" -icon_collections = {} - -icons_read = { - 'fp.png': 'free', - 'flp.png': 'full', - 'trophy.png': 'trophy', - 'dumbbell.png': 'dumbbell', - 'cc0.png': 'cc0', - 'royalty_free.png': 'royalty_free', - 'filter.png': 'filter', - 'filter_active.png': 'filter_active', - 'bell.png': 'bell', -} - -verification_icons = { - 'vs_ready.png':'ready', - 'vs_deleted.png':'deleted' , - 'vs_uploaded.png': 'uploaded', - 'vs_uploading.png': 'uploading', - 'vs_on_hold.png': 'on_hold', - 'vs_validated.png': 'validated', - 'vs_rejected.png': 'rejected' - -} - -icons_read.update(verification_icons) - -def register_icons(): - # Note that preview collections returned by bpy.utils.previews - # are regular py objects - you can use them to store custom data. - import bpy.utils.previews - pcoll = bpy.utils.previews.new() - - # path to the folder where the icon is - # the path is calculated relative to this py file inside the addon folder - icons_dir = os.path.join(os.path.dirname(__file__), "thumbnails") - - # load a preview thumbnail of a file and store in the previews collection - for ir in icons_read.keys(): - pcoll.load(icons_read[ir], os.path.join(icons_dir, ir), 'IMAGE') - - # iprev = pcoll.new(icons_read[ir]) - # img = bpy.data.images.load(os.path.join(icons_dir, ir)) - # iprev.image_size = (img.size[0], img.size[1]) - # iprev.image_pixels_float = img.pixels[:] - - icon_collections["main"] = pcoll - icon_collections["previews"] = bpy.utils.previews.new() - - -def unregister_icons(): - for pcoll in icon_collections.values(): - bpy.utils.previews.remove(pcoll) - icon_collections.clear() diff --git a/blenderkit/image_utils.py b/blenderkit/image_utils.py deleted file mode 100644 index b17878c3..00000000 --- a/blenderkit/image_utils.py +++ /dev/null @@ -1,505 +0,0 @@ -import bpy -import os -import time - - -def get_orig_render_settings(): - rs = bpy.context.scene.render - ims = rs.image_settings - - vs = bpy.context.scene.view_settings - - orig_settings = { - 'file_format': ims.file_format, - 'quality': ims.quality, - 'color_mode': ims.color_mode, - 'compression': ims.compression, - 'exr_codec': ims.exr_codec, - 'view_transform': vs.view_transform - } - return orig_settings - - -def set_orig_render_settings(orig_settings): - rs = bpy.context.scene.render - ims = rs.image_settings - vs = bpy.context.scene.view_settings - - ims.file_format = orig_settings['file_format'] - ims.quality = orig_settings['quality'] - ims.color_mode = orig_settings['color_mode'] - ims.compression = orig_settings['compression'] - ims.exr_codec = orig_settings['exr_codec'] - - vs.view_transform = orig_settings['view_transform'] - - -def img_save_as(img, filepath='//', file_format='JPEG', quality=90, color_mode='RGB', compression=15, - view_transform='Raw', exr_codec='DWAA'): - '''Uses Blender 'save render' to save images - BLender isn't really able so save images with other methods correctly.''' - - ors = get_orig_render_settings() - - rs = bpy.context.scene.render - vs = bpy.context.scene.view_settings - - ims = rs.image_settings - ims.file_format = file_format - ims.quality = quality - ims.color_mode = color_mode - ims.compression = compression - ims.exr_codec = exr_codec - vs.view_transform = view_transform - - img.save_render(filepath=bpy.path.abspath(filepath), scene=bpy.context.scene) - - set_orig_render_settings(ors) - - -def set_colorspace(img, colorspace): - '''sets image colorspace, but does so in a try statement, because some people might actually replace the default - colorspace settings, and it literally can't be guessed what these people use, even if it will mostly be the filmic addon. - ''' - try: - if colorspace == 'Non-Color': - img.colorspace_settings.is_data = True - else: - img.colorspace_settings.name = colorspace - except: - print(f'Colorspace {colorspace} not found.') - -def analyze_image_is_true_hdr(image): - import numpy - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - size = image.size - imageWidth = size[0] - imageHeight = size[1] - tempBuffer = numpy.empty(imageWidth * imageHeight * 4, dtype=numpy.float32) - image.pixels.foreach_get(tempBuffer) - image.blenderkit.true_hdr = numpy.amax(tempBuffer) > 1.05 - -def generate_hdr_thumbnail(): - import numpy - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - hdr_image = ui_props.hdr_upload_image # bpy.data.images.get(ui_props.hdr_upload_image) - - base, ext = os.path.splitext(hdr_image.filepath) - thumb_path = base + '.jpg' - thumb_name = os.path.basename(thumb_path) - - max_thumbnail_size = 2048 - size = hdr_image.size - ratio = size[0] / size[1] - - imageWidth = size[0] - imageHeight = size[1] - thumbnailWidth = min(size[0], max_thumbnail_size) - thumbnailHeight = min(size[1], int(max_thumbnail_size / ratio)) - - tempBuffer = numpy.empty(imageWidth * imageHeight * 4, dtype=numpy.float32) - inew = bpy.data.images.new(thumb_name, imageWidth, imageHeight, alpha=False, float_buffer=False) - - hdr_image.pixels.foreach_get(tempBuffer) - - hdr_image.blenderkit.true_hdr = numpy.amax(tempBuffer) > 1.05 - - inew.filepath = thumb_path - set_colorspace(inew, 'Linear') - inew.pixels.foreach_set(tempBuffer) - - bpy.context.view_layer.update() - if thumbnailWidth < imageWidth: - inew.scale(thumbnailWidth, thumbnailHeight) - - img_save_as(inew, filepath=inew.filepath) - - -def find_color_mode(image): - if not isinstance(image, bpy.types.Image): - raise (TypeError) - else: - depth_mapping = { - 8: 'BW', - 24: 'RGB', - 32: 'RGBA', # can also be bw.. but image.channels doesn't work. - 96: 'RGB', - 128: 'RGBA', - } - return depth_mapping.get(image.depth, 'RGB') - - -def find_image_depth(image): - if not isinstance(image, bpy.types.Image): - raise (TypeError) - else: - depth_mapping = { - 8: '8', - 24: '8', - 32: '8', # can also be bw.. but image.channels doesn't work. - 96: '16', - 128: '16', - } - return depth_mapping.get(image.depth, '8') - - -def can_erase_alpha(na): - alpha = na[3::4] - alpha_sum = alpha.sum() - if alpha_sum == alpha.size: - print('image can have alpha erased') - # print(alpha_sum, alpha.size) - return alpha_sum == alpha.size - - -def is_image_black(na): - r = na[::4] - g = na[1::4] - b = na[2::4] - - rgbsum = r.sum() + g.sum() + b.sum() - - # print('rgb sum', rgbsum, r.sum(), g.sum(), b.sum()) - if rgbsum == 0: - print('image can have alpha channel dropped') - return rgbsum == 0 - - -def is_image_bw(na): - r = na[::4] - g = na[1::4] - b = na[2::4] - - rg_equal = r == g - gb_equal = g == b - rgbequal = rg_equal.all() and gb_equal.all() - if rgbequal: - print('image is black and white, can have channels reduced') - - return rgbequal - - -def numpytoimage(a, iname, width=0, height=0, channels=3): - t = time.time() - foundimage = False - - for image in bpy.data.images: - - if image.name[:len(iname)] == iname and image.size[0] == a.shape[0] and image.size[1] == a.shape[1]: - i = image - foundimage = True - if not foundimage: - if channels == 4: - bpy.ops.image.new(name=iname, width=width, height=height, color=(0, 0, 0, 1), alpha=True, - generated_type='BLANK', float=True) - if channels == 3: - bpy.ops.image.new(name=iname, width=width, height=height, color=(0, 0, 0), alpha=False, - generated_type='BLANK', float=True) - - i = None - - for image in bpy.data.images: - # print(image.name[:len(iname)],iname, image.size[0],a.shape[0],image.size[1],a.shape[1]) - if image.name[:len(iname)] == iname and image.size[0] == width and image.size[1] == height: - i = image - if i is None: - i = bpy.data.images.new(iname, width, height, alpha=False, float_buffer=False, stereo3d=False, is_data=False, - tiled=False) - - # dropping this re-shaping code - just doing flat array for speed and simplicity - # d = a.shape[0] * a.shape[1] - # a = a.swapaxes(0, 1) - # a = a.reshape(d) - # a = a.repeat(channels) - # a[3::4] = 1 - i.pixels.foreach_set(a) # this gives big speedup! - print('\ntime ' + str(time.time() - t)) - return i - - -def imagetonumpy_flat(i): - t = time.time() - - import numpy - - width = i.size[0] - height = i.size[1] - # print(i.channels) - - size = width * height * i.channels - na = numpy.empty(size, numpy.float32) - i.pixels.foreach_get(na) - - # dropping this re-shaping code - just doing flat array for speed and simplicity - # na = na[::4] - # na = na.reshape(height, width, i.channels) - # na = na.swapaxnes(0, 1) - - # print('\ntime of image to numpy ' + str(time.time() - t)) - return na - - -def imagetonumpy(i): - t = time.time() - - import numpy as np - - width = i.size[0] - height = i.size[1] - # print(i.channels) - - size = width * height * i.channels - na = np.empty(size, np.float32) - i.pixels.foreach_get(na) - - # dropping this re-shaping code - just doing flat array for speed and simplicity - # na = na[::4] - na = na.reshape(height, width, i.channels) - na = na.swapaxes(0, 1) - - # print('\ntime of image to numpy ' + str(time.time() - t)) - return na - - -def downscale(i): - minsize = 128 - - sx, sy = i.size[:] - sx = round(sx / 2) - sy = round(sy / 2) - if sx > minsize and sy > minsize: - i.scale(sx, sy) - - -def get_rgb_mean(i): - '''checks if normal map values are ok.''' - import numpy - - na = imagetonumpy_flat(i) - - r = na[::4] - g = na[1::4] - b = na[2::4] - - rmean = r.mean() - gmean = g.mean() - bmean = b.mean() - - rmedian = numpy.median(r) - gmedian = numpy.median(g) - bmedian = numpy.median(b) - - # return(rmedian,gmedian, bmedian) - return (rmean, gmean, bmean) - - -def check_nmap_mean_ok(i): - '''checks if normal map values are in standard range.''' - - rmean, gmean, bmean = get_rgb_mean(i) - - # we could/should also check blue, but some ogl substance exports have 0-1, while 90% nmaps have 0.5 - 1. - nmap_ok = 0.45 < rmean < 0.55 and .45 < gmean < .55 - - return nmap_ok - - -def check_nmap_ogl_vs_dx(i, mask=None, generated_test_images=False): - ''' - checks if normal map is directX or OpenGL. - Returns - String value - DirectX and OpenGL - ''' - import numpy - width = i.size[0] - height = i.size[1] - - rmean, gmean, bmean = get_rgb_mean(i) - - na = imagetonumpy(i) - - if mask: - mask = imagetonumpy(mask) - - red_x_comparison = numpy.zeros((width, height), numpy.float32) - green_y_comparison = numpy.zeros((width, height), numpy.float32) - - if generated_test_images: - red_x_comparison_img = numpy.empty((width, height, 4), numpy.float32) # images for debugging purposes - green_y_comparison_img = numpy.empty((width, height, 4), numpy.float32) # images for debugging purposes - - ogl = numpy.zeros((width, height), numpy.float32) - dx = numpy.zeros((width, height), numpy.float32) - - if generated_test_images: - ogl_img = numpy.empty((width, height, 4), numpy.float32) # images for debugging purposes - dx_img = numpy.empty((width, height, 4), numpy.float32) # images for debugging purposes - - for y in range(0, height): - for x in range(0, width): - # try to mask with UV mask image - if mask is None or mask[x, y, 3] > 0: - - last_height_x = ogl[max(x - 1, 0), min(y, height - 1)] - last_height_y = ogl[max(x, 0), min(y - 1, height - 1)] - - diff_x = ((na[x, y, 0] - rmean) / ((na[x, y, 2] - 0.5))) - diff_y = ((na[x, y, 1] - gmean) / ((na[x, y, 2] - 0.5))) - calc_height = (last_height_x + last_height_y) \ - - diff_x - diff_y - calc_height = calc_height / 2 - ogl[x, y] = calc_height - if generated_test_images: - rgb = calc_height * .1 + .5 - ogl_img[x, y] = [rgb, rgb, rgb, 1] - - # green channel - last_height_x = dx[max(x - 1, 0), min(y, height - 1)] - last_height_y = dx[max(x, 0), min(y - 1, height - 1)] - - diff_x = ((na[x, y, 0] - rmean) / ((na[x, y, 2] - 0.5))) - diff_y = ((na[x, y, 1] - gmean) / ((na[x, y, 2] - 0.5))) - calc_height = (last_height_x + last_height_y) \ - - diff_x + diff_y - calc_height = calc_height / 2 - dx[x, y] = calc_height - if generated_test_images: - rgb = calc_height * .1 + .5 - dx_img[x, y] = [rgb, rgb, rgb, 1] - - ogl_std = ogl.std() - dx_std = dx.std() - - # print(mean_ogl, mean_dx) - # print(max_ogl, max_dx) - print(ogl_std, dx_std) - print(i.name) - # if abs(mean_ogl) > abs(mean_dx): - if abs(ogl_std) > abs(dx_std): - print('this is probably a DirectX texture') - else: - print('this is probably an OpenGL texture') - - if generated_test_images: - # red_x_comparison_img = red_x_comparison_img.swapaxes(0,1) - # red_x_comparison_img = red_x_comparison_img.flatten() - # - # green_y_comparison_img = green_y_comparison_img.swapaxes(0,1) - # green_y_comparison_img = green_y_comparison_img.flatten() - # - # numpytoimage(red_x_comparison_img, 'red_' + i.name, width=width, height=height, channels=1) - # numpytoimage(green_y_comparison_img, 'green_' + i.name, width=width, height=height, channels=1) - - ogl_img = ogl_img.swapaxes(0, 1) - ogl_img = ogl_img.flatten() - - dx_img = dx_img.swapaxes(0, 1) - dx_img = dx_img.flatten() - - numpytoimage(ogl_img, 'OpenGL', width=width, height=height, channels=1) - numpytoimage(dx_img, 'DirectX', width=width, height=height, channels=1) - - if abs(ogl_std) > abs(dx_std): - return 'DirectX' - return 'OpenGL' - - -def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=False, do_downscale=False): - '''checks the image and saves it to drive with possibly reduced channels. - Also can remove the image from the asset if the image is pure black - - it finds it's usages and replaces the inputs where the image is used - with zero/black color. - currently implemented file type conversions: - PNG->JPG - ''' - colorspace = teximage.colorspace_settings.name - teximage.colorspace_settings.name = 'Non-Color' - # teximage.colorspace_settings.name = 'sRGB' color correction mambo jambo. - - JPEG_QUALITY = 90 - # is_image_black(na) - # is_image_bw(na) - - rs = bpy.context.scene.render - ims = rs.image_settings - - orig_file_format = ims.file_format - orig_quality = ims.quality - orig_color_mode = ims.color_mode - orig_compression = ims.compression - orig_depth = ims.color_depth - - # if is_image_black(na): - # # just erase the image from the asset here, no need to store black images. - # pass; - - # fp = teximage.filepath - - # setup image depth, 8 or 16 bit. - # this should normally divide depth with number of channels, but blender always states that number of channels is 4, even if there are only 3 - - print(teximage.name) - print(teximage.depth) - print(teximage.channels) - - bpy.context.scene.display_settings.display_device = 'None' - - image_depth = find_image_depth(teximage) - - ims.color_mode = find_color_mode(teximage) - # image_depth = str(max(min(int(teximage.depth / 3), 16), 8)) - print('resulting depth set to:', image_depth) - - fp = input_filepath - if do_reductions: - na = imagetonumpy_flat(teximage) - - if can_erase_alpha(na): - print(teximage.file_format) - if teximage.file_format == 'PNG': - print('changing type of image to JPG') - base, ext = os.path.splitext(fp) - teximage['original_extension'] = ext - - fp = fp.replace('.png', '.jpg') - fp = fp.replace('.PNG', '.jpg') - - teximage.name = teximage.name.replace('.png', '.jpg') - teximage.name = teximage.name.replace('.PNG', '.jpg') - - teximage.file_format = 'JPEG' - ims.quality = JPEG_QUALITY - ims.color_mode = 'RGB' - - if is_image_bw(na): - ims.color_mode = 'BW' - - ims.file_format = teximage.file_format - ims.color_depth = image_depth - - # all pngs with max compression - if ims.file_format == 'PNG': - ims.compression = 100 - # all jpgs brought to reasonable quality - if ims.file_format == 'JPG': - ims.quality = JPEG_QUALITY - - if do_downscale: - downscale(teximage) - - # it's actually very important not to try to change the image filepath and packed file filepath before saving, - # blender tries to re-pack the image after writing to image.packed_image.filepath and reverts any changes. - teximage.save_render(filepath=bpy.path.abspath(fp), scene=bpy.context.scene) - if len(teximage.packed_files) > 0: - teximage.unpack(method='REMOVE') - teximage.filepath = fp - teximage.filepath_raw = fp - teximage.reload() - - teximage.colorspace_settings.name = colorspace - - ims.file_format = orig_file_format - ims.quality = orig_quality - ims.color_mode = orig_color_mode - ims.compression = orig_compression - ims.color_depth = orig_depth diff --git a/blenderkit/oauth.py b/blenderkit/oauth.py deleted file mode 100644 index ad45ef6e..00000000 --- a/blenderkit/oauth.py +++ /dev/null @@ -1,117 +0,0 @@ -# ##### 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, quote as urlquote, urlparse - -import requests - - -class PortsBlockedException(Exception): - pass - - -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 hasattr(self, 'redirect_uri'): - data["redirect_uri"] = self.redirect_uri - if authorization_code: - data['code'] = authorization_code - if refresh_token: - data['refresh_token'] = refresh_token - - response = requests.post( - '%s/o/token/' % self.server_url, - data=data - ) - if response.status_code != 200: - print("error retrieving refresh tokens %s" % response.status_code) - print(response.content) - return None, None, None - - response_json = json.loads(response.content) - refresh_token = response_json['refresh_token'] - access_token = response_json['access_token'] - return access_token, refresh_token, response_json - - def get_new_token(self, register=True, redirect_url=None): - class HTTPServerHandler(BaseHTTPRequestHandler): - html_template = '<html>%(head)s<h1>%(message)s</h1></html>' - - 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 - if redirect_url: - redirect_string = ( - '<head><meta http-equiv="refresh" content="0;url=%(redirect_url)s"></head>' - '<script> window.location.href="%(redirect_url)s"; </script>' % {'redirect_url': redirect_url} - ) - else: - redirect_string = "" - self.wfile.write(bytes(self.html_template % {'head': redirect_string, 'message': 'You may now close this window.'}, 'utf-8')) - qs = parse_qs(urlparse(self.path).query) - self.server.authorization_code = qs['code'][0] - else: - self.wfile.write(bytes(self.html_template % {'head': '', 'message': 'Authorization failed.'}, 'utf-8')) - - for port in self.ports: - try: - httpServer = HTTPServer(('localhost', port), HTTPServerHandler) - except Exception as e: - print(f"Port {port}: {e}") - continue - break - else: - print("All available ports are blocked") - raise PortsBlockedException(f"All available ports are blocked: {self.ports}") - print(f"Choosen port {port}") - self.redirect_uri = f"http://localhost:{port}/consumer/exchange/" - authorize_url = ( - "/o/authorize?client_id=%s&state=random_state_string&response_type=code&" - "redirect_uri=%s" % (self.client_id, self.redirect_uri) - ) - if register: - authorize_url = "%s/accounts/register/?next=%s" % (self.server_url, urlquote(authorize_url)) - else: - authorize_url = "%s%s" % (self.server_url, authorize_url) - webbrowser.open_new(authorize_url) - - 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") diff --git a/blenderkit/overrides.py b/blenderkit/overrides.py deleted file mode 100644 index 03872d70..00000000 --- a/blenderkit/overrides.py +++ /dev/null @@ -1,300 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import utils - -import bpy, mathutils -from bpy.types import ( - Operator) - - -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(): -# utils.p('No proxies in Blender anymore') -# return False -# -# 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 -# utils.p('not sure what to proxify') -# return False - - -eevee_transp_nodes = [ - 'BSDF_GLASS', - 'BSDF_REFRACTION', - 'BSDF_TRANSPARENT', - 'PRINCIPLED_VOLUME', - 'VOLUME_ABSORPTION', - 'VOLUME_SCATTER' -] - - -def ensure_eevee_transparency(m): - ''' ensures alpha for transparent materials when the user didn't set it up correctly''' - # if the blend mode is opaque, it means user probably ddidn't know or forgot to - # set up material properly - if m.blend_method == 'OPAQUE': - alpha = False - for n in m.node_tree.nodes: - if n.type in eevee_transp_nodes: - alpha = True - elif n.type == 'BSDF_PRINCIPLED': - i = n.inputs['Transmission'] - if i.default_value > 0 or len(i.links) > 0: - alpha = True - if alpha: - m.blend_method = 'HASHED' - m.shadow_method = 'HASHED' - - -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.view_layer.objects.active is not None - - def execute(self, context): - - s = bpy.context.scene - sobs = s.collection.all_objects - aob = bpy.context.active_object - dg = aob.instance_collection - vlayer = bpy.context.view_layer - instances_emptys = [] - - # first, find instances of this collection in the scene - for ob in sobs: - if ob.instance_collection == dg and ob not in instances_emptys: - instances_emptys.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) - ob.select_set(True) - obs.append(ob) - if ob.parent == None: - parent = ob - bpy.context.view_layer.objects.active = parent - except Exception as e: - print(e) - - bpy.ops.object.make_local(type='ALL') - - for i, ob in enumerate(obs): - if ob.name in vlayer.objects: - obs[i] = vlayer.objects[ob.name] - try: - ob.select_set(True) - except Exception as e: - print('failed to select an object from the collection, getting a replacement.') - print(e) - - related = [] - - for i, ob in enumerate(instances_emptys): - 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): -# """Attempt to create proxy armature from the asset""" -# bl_idname = "object.blenderkit_make_proxy" -# bl_label = "BlenderKit Make Proxy" -# -# @classmethod -# def poll(cls, context): -# return bpy.context.view_layer.objects.active 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): - """Add color corector to the asset. """ - bl_idname = "object.blenderkit_color_corrector" - bl_label = "Add color corrector" - - @classmethod - def poll(cls, context): - return bpy.context.view_layer.objects.active 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 deleted file mode 100644 index 0170ae7b..00000000 --- a/blenderkit/paths.py +++ /dev/null @@ -1,407 +0,0 @@ -# ##### 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, tempfile, shutil -from blenderkit import tasks_queue, ui, utils, reports - -_presets = os.path.join(bpy.utils.user_resource('SCRIPTS'), "presets") -BLENDERKIT_LOCAL = "http://localhost:8001" -BLENDERKIT_MAIN = "https://www.blenderkit.com" -BLENDERKIT_DEVEL = "https://devel.blenderkit.com" -BLENDERKIT_API = "/api/v1/" -BLENDERKIT_REPORT_URL = "usage_report/" -BLENDERKIT_USER_ASSETS = "/my-assets" -BLENDERKIT_PLANS = "/plans/pricing/" -BLENDERKIT_MANUAL = "https://youtu.be/pSay3yaBWV0" -BLENDERKIT_MODEL_UPLOAD_INSTRUCTIONS_URL = "https://www.blenderkit.com/docs/upload/" -BLENDERKIT_MATERIAL_UPLOAD_INSTRUCTIONS_URL = "https://www.blenderkit.com/docs/uploading-material/" -BLENDERKIT_BRUSH_UPLOAD_INSTRUCTIONS_URL = "https://www.blenderkit.com/docs/uploading-brush/" -BLENDERKIT_HDR_UPLOAD_INSTRUCTIONS_URL = "https://www.blenderkit.com/docs/uploading-hdr/" -BLENDERKIT_SCENE_UPLOAD_INSTRUCTIONS_URL = "https://www.blenderkit.com/docs/uploading-scene/" -BLENDERKIT_LOGIN_URL = "https://www.blenderkit.com/accounts/login" -BLENDERKIT_OAUTH_LANDING_URL = "/oauth-landing/" -BLENDERKIT_SIGNUP_URL = "https://www.blenderkit.com/accounts/register" -BLENDERKIT_SETTINGS_FILENAME = os.path.join(_presets, "bkit.json") - - -def cleanup_old_folders(): - '''function to clean up any historical folders for BlenderKit. By now removes the temp folder.''' - orig_temp = os.path.join(os.path.expanduser('~'), 'blenderkit_data', 'temp') - if os.path.isdir(orig_temp): - try: - shutil.rmtree(orig_temp) - except Exception as e: - print(e) - print("couldn't delete old temp directory") - - -def get_bkit_url(): - # bpy.app.debug_value = 2 - d = bpy.app.debug_value - # d = 2 - if d == 1: - url = BLENDERKIT_LOCAL - elif d == 2: - url = BLENDERKIT_DEVEL - else: - url = BLENDERKIT_MAIN - return url - - -def find_in_local(text=''): - fs = [] - for p, d, f in os.walk('.'): - for file in f: - if text in file: - fs.append(file) - return fs - - -def get_api_url(): - return get_bkit_url() + BLENDERKIT_API - - -def get_oauth_landing_url(): - return get_bkit_url() + BLENDERKIT_OAUTH_LANDING_URL - - -def get_author_gallery_url(author_id): - return f'{get_bkit_url()}/asset-gallery?query=author_id:{author_id}' - -def get_asset_gallery_url(asset_id): - return f'{get_bkit_url()}/asset-gallery-detail/{asset_id}/' - -def default_global_dict(): - from os.path import expanduser - home = expanduser("~") - return home + os.sep + 'blenderkit_data' - - -def get_categories_filepath(): - tempdir = get_temp_dir() - return os.path.join(tempdir, 'categories.json') - -dirs_exist_dict = {}#cache these results since this is used very often -# this causes the function to fail if user deletes the directory while blender is running, -# but comes back when blender is restarted. -def get_temp_dir(subdir=None): - - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - #first try cached results - if subdir is not None: - d = dirs_exist_dict.get(subdir) - if d is not None: - return d - else: - d = dirs_exist_dict.get('top') - if d is not None: - return d - - # tempdir = user_preferences.temp_dir - tempdir = os.path.join(tempfile.gettempdir(), 'bkit_temp') - if tempdir.startswith('//'): - tempdir = bpy.path.abspath(tempdir) - try: - if not os.path.exists(tempdir): - os.makedirs(tempdir) - dirs_exist_dict['top'] = tempdir - - if subdir is not None: - tempdir = os.path.join(tempdir, subdir) - if not os.path.exists(tempdir): - os.makedirs(tempdir) - dirs_exist_dict[subdir] = tempdir - - cleanup_old_folders() - except: - tasks_queue.add_task((reports.add_report, ('Cache directory not found. Resetting Cache folder path.',))) - - p = default_global_dict() - if p == user_preferences.global_dir: - message = 'Global dir was already default, plese set a global directory in addon preferences to a dir where you have write permissions.' - tasks_queue.add_task((reports.add_report, (message,))) - return None - user_preferences.global_dir = p - tempdir = get_temp_dir(subdir=subdir) - 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', 'hdr':'hdrs'} - - 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) - - subd = subdmapping[asset_type] - subdir = os.path.join(ddir, subd) - if not os.path.exists(subdir): - os.makedirs(subdir) - 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) - - subd = subdmapping[asset_type] - - subdir = os.path.join(ddir, subd) - if not os.path.exists(subdir): - os.makedirs(subdir) - 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() - - characters = '<>:"/\\|?\*., ()#' - for ch in characters: - slug = slug.replace(ch, '_') - # 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) - slug = re.sub(r'\\\'\"', '_', slug) - if len(slug)>50: - slug = slug[:50] - return slug - - -def extract_filename_from_url(url): - # print(url) - if url is not None: - imgname = url.split('/')[-1] - imgname = imgname.split('?')[0] - return imgname - return '' - - -resolution_suffix = { - 'blend': '', - 'resolution_0_5K': '_05k', - 'resolution_1K': '_1k', - 'resolution_2K': '_2k', - 'resolution_4K': '_4k', - 'resolution_8K': '_8k', -} -resolutions = { - 'resolution_0_5K': 512, - 'resolution_1K': 1024, - 'resolution_2K': 2048, - 'resolution_4K': 4096, - 'resolution_8K': 8192, -} - - -def round_to_closest_resolution(res): - rdist = 1000000 - # while res/2>1: - # p2res*=2 - # res = res/2 - # print(p2res, res) - for rkey in resolutions: - # print(resolutions[rkey], rdist) - d = abs(res - resolutions[rkey]) - if d < rdist: - rdist = d - p2res = rkey - - return p2res - - -def get_res_file(asset_data, resolution, find_closest_with_url = False): - ''' - Returns closest resolution that current asset can offer. - If there are no resolutions, return orig file. - If orig file is requested, return it. - params - asset_data - resolution - ideal resolution - find_closest_with_url: - returns only resolutions that already containt url in the asset data, used in scenes where asset is/was already present. - Returns: - resolution file - resolution, so that other processess can pass correctly which resolution is downloaded. - ''' - orig = None - res = None - closest = None - target_resolution = resolutions.get(resolution) - mindist = 100000000 - - for f in asset_data['files']: - if f['fileType'] == 'blend': - orig = f - if resolution == 'blend': - #orig file found, return. - return orig , 'blend' - - if f['fileType'] == resolution: - #exact match found, return. - return f, resolution - # find closest resolution if the exact match won't be found. - rval = resolutions.get(f['fileType']) - if rval and target_resolution: - rdiff = abs(target_resolution - rval) - if rdiff < mindist: - closest = f - mindist = rdiff - # print('\n\n\n\n\n\n\n\n') - # print(closest) - # print('\n\n\n\n\n\n\n\n') - if not res and not closest: - # utils.pprint(f'will download blend instead of resolution {resolution}') - return orig , 'blend' - # utils.pprint(f'found closest resolution {closest["fileType"]} instead of the requested {resolution}') - return closest, closest['fileType'] - -def server_2_local_filename(asset_data, filename): - ''' - Convert file name on server to file name local. - This should get replaced - ''' - # print(filename) - fn = filename.replace('blend_', '') - fn = fn.replace('resolution_', '') - # print('after replace ', fn) - n = slugify(asset_data['name']) + '_' + fn - return n - -def get_texture_directory(asset_data, resolution = 'blend'): - tex_dir_path = f"//textures{resolution_suffix[resolution]}{os.sep}" - return tex_dir_path - -def get_download_filepaths(asset_data, resolution='blend', can_return_others = False): - '''Get all possible paths of the asset and resolution. Usually global and local directory.''' - dirs = get_download_dirs(asset_data['assetType']) - res_file, resolution = get_res_file(asset_data, resolution, find_closest_with_url = can_return_others) - name_slug = slugify(asset_data['name']) - asset_folder_name = f"{name_slug}_{asset_data['id']}" - - # utils.pprint('get download filenames ', dict(res_file)) - file_names = [] - - if not res_file: - return file_names - # fn = asset_data['file_name'].replace('blend_', '') - if res_file.get('url') is not None: - #Tweak the names a bit: - # remove resolution and blend words in names - # - fn = extract_filename_from_url(res_file['url']) - n = server_2_local_filename(asset_data,fn) - for d in dirs: - asset_folder_path = os.path.join(d,asset_folder_name) - if not os.path.exists(asset_folder_path): - os.makedirs(asset_folder_path) - - file_name = os.path.join(asset_folder_path, n) - file_names.append(file_name) - - utils.p('file paths', file_names) - return file_names - - -def delete_asset_debug(asset_data): - '''TODO fix this for resolutions - should get ALL files from ALL resolutions.''' - 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_filepaths(asset_data) - for f in file_names: - asset_dir = os.path.dirname(f) - - if os.path.isdir(asset_dir): - - try: - print(asset_dir) - shutil.rmtree(asset_dir) - 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) - -script_path = os.path.dirname(os.path.realpath(__file__)) - -def get_addon_thumbnail_path(name): - global script_path - # fpath = os.path.join(p, subpath) - ext = name.split('.')[-1] - next = '' - if not (ext == 'jpg' or ext == 'png'): # already 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 deleted file mode 100644 index 3baabb5b..00000000 --- a/blenderkit/ratings.py +++ /dev/null @@ -1,297 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -from blenderkit import paths, utils, rerequests, tasks_queue, ratings_utils, icons - -import bpy -import requests, threading -import logging - -bk_logger = logging.getLogger('blenderkit') - -from bpy.props import ( - IntProperty, - FloatProperty, - StringProperty, - EnumProperty, - BoolProperty, - PointerProperty, -) -from bpy.types import ( - Operator, - Panel, -) - - -def pretty_print_POST(req): - """ - pretty print a 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 upload_review_thread(url, reviews, headers): - r = rerequests.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 = utils.get_headers(api_key) - - bkit_ratings = asset.bkit_ratings - # print('rating asset', asset_data['name'], asset_data['assetBaseId']) - url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/' - - ratings = [ - - ] - - if bkit_ratings.rating_quality > 0.1: - ratings = (('quality', bkit_ratings.rating_quality),) - tasks_queue.add_task((ratings_utils.send_rating_to_thread_quality, (url, ratings, headers)), wait=2.5, - only_last=True) - if bkit_ratings.rating_work_hours > 0.1: - ratings = (('working_hours', round(bkit_ratings.rating_work_hours, 1)),) - tasks_queue.add_task((ratings_utils.send_rating_to_thread_work_hours, (url, ratings, headers)), wait=2.5, - only_last=True) - - thread = threading.Thread(target=ratings_utils.upload_rating_thread, args=(url, ratings, headers)) - thread.start() - - url = paths.get_api_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=upload_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']['assetBaseId']] = True - - -def get_assets_for_rating(): - ''' - gets assets from scene that could/should be rated by the user. - TODO this is only a draft. - - ''' - assets = [] - for ob in bpy.context.scene.objects: - if ob.get('asset_data'): - assets.append(ob) - for m in bpy.data.materials: - if m.get('asset_data'): - assets.append(m) - for b in bpy.data.brushes: - if b.get('asset_data'): - assets.append(b) - return assets - - -asset_types = ( - ('MODEL', 'Model', 'set of objects'), - ('SCENE', 'Scene', 'scene'), - ('HDR', 'HDR', 'hdr'), - ('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'), -) - - -# TODO drop this operator, not needed anymore. -class UploadRatingOperator(bpy.types.Operator): - """Upload rating to the web db""" - bl_idname = "object.blenderkit_rating_upload" - bl_label = "Send Rating" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - # 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_ratings_menu(self, context, layout): - pcoll = icons.icon_collections["main"] - - profile_name = '' - profile = bpy.context.window_manager.get('bkit profile') - if profile and len(profile['user']['firstName']) > 0: - profile_name = ' ' + profile['user']['firstName'] - - col = layout.column() - # layout.template_icon_view(bkit_ratings, property, show_labels=False, scale=6.0, scale_popup=5.0) - row = col.row() - row.label(text='Quality:', icon='SOLO_ON') - row = col.row() - row.label(text='Please help the community by rating quality:') - - row = col.row() - row.prop(self, 'rating_quality_ui', expand=True, icon_only=True, emboss=False) - if self.rating_quality > 0: - # row = col.row() - - row.label(text=f' Thanks{profile_name}!', icon='FUND') - # row.label(text=str(self.rating_quality)) - col.separator() - col.separator() - - row = col.row() - row.label(text='Complexity:', icon_value=pcoll['dumbbell'].icon_id) - row = col.row() - row.label(text=f"How many hours did this {self.asset_type} save you?") - - if utils.profile_is_validator(): - row = col.row() - row.prop(self, 'rating_work_hours') - - if self.asset_type in ('model', 'scene'): - row = col.row() - - row.prop(self, 'rating_work_hours_ui', expand=True, icon_only=False, emboss=True) - if float(self.rating_work_hours_ui) > 100: - utils.label_multiline(col, - text=f"\nThat's huge! please be sure to give such rating only to godly {self.asset_type}s.\n", - width=500) - elif float(self.rating_work_hours_ui) > 18: - col.separator() - - utils.label_multiline(col, - text=f"\nThat's a lot! please be sure to give such rating only to amazing {self.asset_type}s.\n", - width=500) - - - elif self.asset_type == 'hdr': - row = col.row() - row.prop(self, 'rating_work_hours_ui_1_10', expand=True, icon_only=False, emboss=True) - else: - row = col.row() - row.prop(self, 'rating_work_hours_ui_1_5', expand=True, icon_only=False, emboss=True) - - if self.rating_work_hours > 0: - row = col.row() - row.label(text=f'Thanks{profile_name}, you are amazing!', icon='FUND') - - -class FastRateMenu(Operator, ratings_utils.RatingsProperties): - """Rating of the assets , also directly from the asset bar - without need to download assets""" - bl_idname = "wm.blenderkit_menu_rating_upload" - bl_label = "Ratings" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - @classmethod - def poll(cls, context): - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - return True; - - def draw(self, context): - layout = self.layout - draw_ratings_menu(self, context, layout) - - def execute(self, context): - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - #get asset id - if ui_props.active_index > -1: - sr = bpy.context.window_manager['search results'] - asset_data = dict(sr[ui_props.active_index]) - self.asset_id = asset_data['id'] - self.asset_type = asset_data['assetType'] - - if self.asset_id == '': - return {'CANCELLED'} - - wm = context.window_manager - - self.prefill_ratings() - - if self.asset_type in ('model', 'scene'): - # spawn a wider one for validators for the enum buttons - return wm.invoke_popup(self, width=500) - else: - return wm.invoke_popup(self) - - -def rating_menu_draw(self, context): - layout = self.layout - - ui_props = context.window_manager.blenderkitUI - sr = bpy.context.window_manager['search results'] - - asset_search_index = ui_props.active_index - if asset_search_index > -1: - asset_data = dict(sr['results'][asset_search_index]) - - col = layout.column() - layout.label(text='Admin rating Tools:') - col.operator_context = 'INVOKE_DEFAULT' - - op = col.operator('wm.blenderkit_menu_rating_upload', text='Add Rating') - op.asset_id = asset_data['id'] - op.asset_name = asset_data['name'] - op.asset_type = asset_data['assetType'] - - -def register_ratings(): - bpy.utils.register_class(UploadRatingOperator) - bpy.utils.register_class(FastRateMenu) - # bpy.types.OBJECT_MT_blenderkit_asset_menu.append(rating_menu_draw) - - -def unregister_ratings(): - pass; - # bpy.utils.unregister_class(StarRatingOperator) - bpy.utils.unregister_class(UploadRatingOperator) - bpy.utils.unregister_class(FastRateMenu) diff --git a/blenderkit/ratings_utils.py b/blenderkit/ratings_utils.py deleted file mode 100644 index 1e13c7ae..00000000 --- a/blenderkit/ratings_utils.py +++ /dev/null @@ -1,369 +0,0 @@ -# ##### 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 ##### - -# mainly update functions and callbacks for ratings properties, here to avoid circular imports. -import bpy -from blenderkit import utils, paths, tasks_queue, rerequests - -from bpy.props import ( - IntProperty, - FloatProperty, - FloatVectorProperty, - StringProperty, - EnumProperty, - BoolProperty, - PointerProperty, -) - -import threading -import requests -import logging - -bk_logger = logging.getLogger('blenderkit') - - -def upload_rating_thread(url, ratings, headers): - ''' Upload rating thread function / disconnected from blender data.''' - bk_logger.debug('upload rating ' + url + str(ratings)) - 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 = rerequests.put(rating_url, data=data, verify=True, headers=headers) - - except requests.exceptions.RequestException as e: - print('ratings upload failed: %s' % str(e)) - - -def send_rating_to_thread_quality(url, ratings, headers): - '''Sens rating into thread rating, main purpose is for tasks_queue. - One function per property to avoid lost data due to stashing.''' - thread = threading.Thread(target=upload_rating_thread, args=(url, ratings, headers)) - thread.start() - - -def send_rating_to_thread_work_hours(url, ratings, headers): - '''Sens rating into thread rating, main purpose is for tasks_queue. - One function per property to avoid lost data due to stashing.''' - thread = threading.Thread(target=upload_rating_thread, args=(url, ratings, headers)) - thread.start() - - -def store_rating_local_empty(asset_id): - context = bpy.context - ar = context.window_manager['asset ratings'] - ar[asset_id] = ar.get(asset_id, {}) - - -def store_rating_local(asset_id, type='quality', value=0): - context = bpy.context - ar = context.window_manager['asset ratings'] - ar[asset_id] = ar.get(asset_id, {}) - ar[asset_id][type] = value - - -def get_rating(asset_id, headers): - ''' - Retrieve ratings from BlenderKit server. Can be run from a thread - Parameters - ---------- - asset_id - headers - - Returns - ------- - ratings - dict of type:value ratings - ''' - url = paths.get_api_url() + 'assets/' + asset_id + '/rating/' - params = {} - r = rerequests.get(url, params=params, verify=True, headers=headers) - if r is None: - return - if r.status_code == 200: - rj = r.json() - ratings = {} - # print(rj) - # store ratings - send them to task queue - for r in rj['results']: - ratings[r['ratingType']] = r['score'] - tasks_queue.add_task((store_rating_local,(asset_id, r['ratingType'], r['score']))) - # store_rating_local(asset_id, type = r['ratingType'], value = r['score']) - - if len(rj['results'])==0: - # store empty ratings too, so that server isn't checked repeatedly - tasks_queue.add_task((store_rating_local_empty,(asset_id,))) - # return ratings - - -def get_rating_local(asset_id): - context = bpy.context - context.window_manager['asset ratings'] = context.window_manager.get('asset ratings', {}) - rating = context.window_manager['asset ratings'].get(asset_id) - if rating: - return rating.to_dict() - return None - - -def update_ratings_quality(self, context): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - api_key = user_preferences.api_key - - headers = utils.get_headers(api_key) - - if not (hasattr(self, 'rating_quality')): - # first option is for rating of assets that are from scene - asset = self.id_data - bkit_ratings = asset.bkit_ratings - asset_id = asset['asset_data']['id'] - else: - # this part is for operator rating: - bkit_ratings = self - asset_id = self.asset_id - - if bkit_ratings.rating_quality > 0.1: - url = paths.get_api_url() + f'assets/{asset_id}/rating/' - - store_rating_local(asset_id, type='quality', value=bkit_ratings.rating_quality) - - ratings = [('quality', bkit_ratings.rating_quality)] - tasks_queue.add_task((send_rating_to_thread_quality, (url, ratings, headers)), wait=2.5, only_last=True) - - -def update_ratings_work_hours(self, context): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - api_key = user_preferences.api_key - headers = utils.get_headers(api_key) - if not (hasattr(self, 'rating_work_hours')): - # first option is for rating of assets that are from scene - asset = self.id_data - bkit_ratings = asset.bkit_ratings - asset_id = asset['asset_data']['id'] - else: - # this part is for operator rating: - bkit_ratings = self - asset_id = self.asset_id - - if bkit_ratings.rating_work_hours > 0.45: - url = paths.get_api_url() + f'assets/{asset_id}/rating/' - - store_rating_local(asset_id, type='working_hours', value=bkit_ratings.rating_work_hours) - - ratings = [('working_hours', round(bkit_ratings.rating_work_hours, 1))] - tasks_queue.add_task((send_rating_to_thread_work_hours, (url, ratings, headers)), wait=2.5, only_last=True) - - -def update_quality_ui(self, context): - '''Converts the _ui the enum into actual quality number.''' - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.api_key == '': - # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') - # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') - # return - bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', - message='Please login/signup to rate assets. Clicking OK takes you to web login.') - # self.rating_quality_ui = '0' - self.rating_quality = int(self.rating_quality_ui) - - -def update_ratings_work_hours_ui(self, context): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.api_key == '': - # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') - # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') - # return - bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', - message='Please login/signup to rate assets. Clicking OK takes you to web login.') - # self.rating_work_hours_ui = '0' - self.rating_work_hours = float(self.rating_work_hours_ui) - - -def update_ratings_work_hours_ui_1_5(self, context): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.api_key == '': - # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') - # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') - # return - bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', - message='Please login/signup to rate assets. Clicking OK takes you to web login.') - # self.rating_work_hours_ui_1_5 = '0' - self.rating_work_hours = float(self.rating_work_hours_ui_1_5) - - -def update_ratings_work_hours_ui_1_10(self, context): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.api_key == '': - # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') - # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') - # return - bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', - message='Please login/signup to rate assets. Clicking OK takes you to web login.') - # self.rating_work_hours_ui_1_5 = '0' - # print('updating 1-5') - # print(float(self.rating_work_hours_ui_1_5)) - self.rating_work_hours = float(self.rating_work_hours_ui_1_10) - - -def stars_enum_callback(self, context): - '''regenerates the enum property used to display rating stars, so that there are filled/empty stars correctly.''' - items = [] - for a in range(0, 10): - if self.rating_quality < a + 1: - icon = 'SOLO_OFF' - else: - icon = 'SOLO_ON' - # has to have something before the number in the value, otherwise fails on registration. - items.append((f'{a + 1}', f'{a + 1}', '', icon, a + 1)) - return items - - -class RatingsProperties(): - message: StringProperty( - name="message", - description="message", - default="Rating asset", - options={'SKIP_SAVE'}) - - asset_id: StringProperty( - name="Asset Base Id", - description="Unique id of the asset (hidden)", - default="", - options={'SKIP_SAVE'}) - - asset_name: StringProperty( - name="Asset Name", - description="Name of the asset (hidden)", - default="", - options={'SKIP_SAVE'}) - - asset_type: StringProperty( - name="Asset type", - description="asset type", - default="", - options={'SKIP_SAVE'}) - - rating_quality: IntProperty(name="Quality", - description="quality of the material", - default=0, - min=-1, max=10, - update=update_ratings_quality, - options={'SKIP_SAVE'}) - - # the following enum is only to ease interaction - enums support 'drag over' and enable to draw the stars easily. - rating_quality_ui: EnumProperty(name='rating_quality_ui', - items=stars_enum_callback, - description='Rating stars 0 - 10', - default=0, - update=update_quality_ui, - options={'SKIP_SAVE'}) - - rating_work_hours: FloatProperty(name="Work Hours", - description="How many hours did this work take?", - default=0.00, - min=0.0, max=300, - update=update_ratings_work_hours, - options={'SKIP_SAVE'} - ) - - high_rating_warning = "This is a high rating, please be sure to give such rating only to amazing assets" - - possible_wh_values = [0,.5,1,2,3,4,5,6,8,10,15,20,30,50,100,150,200,250] - items_models = [('0', '0', ''), - ('.5', '0.5', ''), - ('1', '1', ''), - ('2', '2', ''), - ('3', '3', ''), - ('4', '4', ''), - ('5', '5', ''), - ('6', '6', ''), - ('8', '8', ''), - ('10', '10', ''), - ('15', '15', ''), - ('20', '20', ''), - ('30', '30', high_rating_warning), - ('50', '50', high_rating_warning), - ('100', '100', high_rating_warning), - ('150', '150', high_rating_warning), - ('200', '200', high_rating_warning), - ('250', '250', high_rating_warning), - ] - rating_work_hours_ui: EnumProperty(name="Work Hours", - description="How many hours did this work take?", - items=items_models, - default='0', update=update_ratings_work_hours_ui, - options={'SKIP_SAVE'} - ) - possible_wh_values_1_5 = [0,.2, .5,1,2,3,4,5] - - items_1_5 = [('0', '0', ''), - ('.2', '0.2', ''), - ('.5', '0.5', ''), - ('1', '1', ''), - ('2', '2', ''), - ('3', '3', ''), - ('4', '4', ''), - ('5', '5', '') - ] - rating_work_hours_ui_1_5: EnumProperty(name="Work Hours", - description="How many hours did this work take?", - items=items_1_5, - default='0', - update=update_ratings_work_hours_ui_1_5, - options={'SKIP_SAVE'} - ) - possible_wh_values_1_10 = [0,1,2,3,4,5,6,7,8,9,10] - - items_1_10= [('0', '0', ''), - ('1', '1', ''), - ('2', '2', ''), - ('3', '3', ''), - ('4', '4', ''), - ('5', '5', ''), - ('6', '6', ''), - ('7', '7', ''), - ('8', '8', ''), - ('9', '9', ''), - ('10', '10', '') - ] - rating_work_hours_ui_1_10: EnumProperty(name="Work Hours", - description="How many hours did this work take?", - items= items_1_10, - default='0', - update=update_ratings_work_hours_ui_1_10, - options={'SKIP_SAVE'} - ) - - def prefill_ratings(self): - # pre-fill ratings - ratings = get_rating_local(self.asset_id) - print('prefill ratings') - print(ratings) - if ratings and ratings.get('quality'): - self.rating_quality = ratings['quality'] - if ratings and ratings.get('working_hours'): - wh = int(ratings['working_hours']) - whs = str(wh) - if wh in self.possible_wh_values: - self.rating_work_hours_ui = whs - if wh < 6 and wh in self.possible_wh_values_1_5: - self.rating_work_hours_ui_1_5 = whs - if wh < 11 and wh in self.possible_wh_values_1_10: - self.rating_work_hours_ui_1_10 = whs diff --git a/blenderkit/reports.py b/blenderkit/reports.py deleted file mode 100644 index 7a455863..00000000 --- a/blenderkit/reports.py +++ /dev/null @@ -1,67 +0,0 @@ -# ##### 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 time -import bpy -from blenderkit import colors, asset_bar_op, ui_bgl, utils - -reports = [] - - -def add_report(text='', timeout=5, color=colors.GREEN): - global reports - # check for same reports and just make them longer by the timeout. - for old_report in reports: - if old_report.text == text: - old_report.timeout = old_report.age + timeout - return - report = Report(text=text, timeout=timeout, color=color) - reports.append(report) - - -class Report(): - def __init__(self, area_pointer=0, text='', timeout=5, color=(.5, 1, .5, 1)): - self.text = text - self.timeout = timeout - self.start_time = time.time() - self.color = color - self.draw_color = color - self.age = 0 - if asset_bar_op.active_area_pointer == 0: - w, a, r = utils.get_largest_area(area_type='VIEW_3D') - - self.active_area_pointer = a.as_pointer() - else: - self.active_area_pointer = asset_bar_op.active_area_pointer - - def fade(self): - fade_time = 1 - self.age = time.time() - self.start_time - if self.age + fade_time > self.timeout: - alpha_multiplier = (self.timeout - self.age) / fade_time - self.draw_color = (self.color[0], self.color[1], self.color[2], self.color[3] * alpha_multiplier) - if self.age > self.timeout: - global reports - try: - reports.remove(self) - except Exception as e: - pass; - - def draw(self, x, y): - if (bpy.context.area is not None and bpy.context.area.as_pointer() == self.active_area_pointer): - ui_bgl.draw_text(self.text, x, y + 8, 16, self.draw_color) diff --git a/blenderkit/rerequests.py b/blenderkit/rerequests.py deleted file mode 100644 index 2a8adc41..00000000 --- a/blenderkit/rerequests.py +++ /dev/null @@ -1,115 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import ui, utils, paths, tasks_queue, bkit_oauth, reports - -import requests -import bpy -import logging - -bk_logger = logging.getLogger('rerequests') - - -class FakeResponse(): - def __init__(self, text='', status_code = 400): - self.text = text - self.status_code = status_code - def json(self): - return {} - -def rerequest(method, url, recursion=0, **kwargs): - # first get any additional args from kwargs - immediate = False - if kwargs.get('immediate'): - immediate = kwargs['immediate'] - kwargs.pop('immediate') - # first normal attempt - try: - response = requests.request(method, url, **kwargs) - except Exception as e: - print(e) - tasks_queue.add_task((reports.add_report, ( - 'Connection error.', 10))) - return FakeResponse() - - bk_logger.debug(url + str(kwargs)) - bk_logger.debug(response.status_code) - - if response.status_code == 401: - try: - rdata = response.json() - except: - rdata = {} - - tasks_queue.add_task((reports.add_report, (method + ' request Failed.' + str(rdata.get('detail')),))) - - if rdata.get('detail') == 'Invalid token.': - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.api_key != '': - if user_preferences.enable_oauth and user_preferences.api_key_refresh != '': - tasks_queue.add_task((reports.add_report, ( - 'refreshing token. If this fails, please login in BlenderKit Login panel.', 10))) - refresh_url = paths.get_bkit_url() - auth_token, refresh_token, oauth_response = bkit_oauth.refresh_token( - user_preferences.api_key_refresh, refresh_url) - - # bk_logger.debug(auth_token, refresh_token) - if auth_token is not None: - if immediate == True: - # this can write tokens occasionally into prefs. used e.g. in upload. Only possible - # in non-threaded tasks - bpy.context.preferences.addons['blenderkit'].preferences.api_key = auth_token - bpy.context.preferences.addons['blenderkit'].preferences.api_key_refresh = refresh_token - else: - tasks_queue.add_task((bkit_oauth.write_tokens, (auth_token, refresh_token, oauth_response))) - - kwargs['headers'] = utils.get_headers(auth_token) - response = requests.request(method, url, **kwargs) - bk_logger.debug('reresult', response.status_code) - if response.status_code >= 400: - bk_logger.debug('reresult', response.text) - tasks_queue.add_task((reports.add_report, ( - response.text, 10))) - - else: - tasks_queue.add_task((reports.add_report, ( - 'Refreshing token failed.Please login manually.', 10))) - # tasks_queue.add_task((bkit_oauth.write_tokens, ('', '', ''))) - tasks_queue.add_task((bpy.ops.wm.blenderkit_login, ('INVOKE_DEFAULT',)), fake_context=True) - return response - - -def get(url, **kwargs): - response = rerequest('get', url, **kwargs) - return response - - -def post(url, **kwargs): - response = rerequest('post', url, **kwargs) - return response - - -def put(url, **kwargs): - response = rerequest('put', url, **kwargs) - return response - - -def patch(url, **kwargs): - response = rerequest('patch', url, **kwargs) - return response diff --git a/blenderkit/resolutions.py b/blenderkit/resolutions.py deleted file mode 100644 index 23aa939d..00000000 --- a/blenderkit/resolutions.py +++ /dev/null @@ -1,716 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import paths, append_link, bg_blender, utils, download, search, rerequests, upload_bg, image_utils - -import sys, json, os, time -import subprocess -import tempfile -import bpy -import requests -import math -import threading - -resolutions = { - 'resolution_0_5K': 512, - 'resolution_1K': 1024, - 'resolution_2K': 2048, - 'resolution_4K': 4096, - 'resolution_8K': 8192, -} -rkeys = list(resolutions.keys()) - -resolution_props_to_server = { - - '512': 'resolution_0_5K', - '1024': 'resolution_1K', - '2048': 'resolution_2K', - '4096': 'resolution_4K', - '8192': 'resolution_8K', - 'ORIGINAL': 'blend', -} - - -def get_current_resolution(): - actres = 0 - for i in bpy.data.images: - if i.name != 'Render Result': - actres = max(actres, i.size[0], i.size[1]) - return actres - - -def save_image_safely(teximage, filepath): - ''' - Blender makes it really hard to save images... - Would be worth investigating PIL or similar instead - Parameters - ---------- - teximage - - Returns - ------- - - ''' - JPEG_QUALITY = 98 - - rs = bpy.context.scene.render - ims = rs.image_settings - - orig_file_format = ims.file_format - orig_quality = ims.quality - orig_color_mode = ims.color_mode - orig_compression = ims.compression - - ims.file_format = teximage.file_format - if teximage.file_format == 'PNG': - ims.color_mode = 'RGBA' - elif teximage.channels == 3: - ims.color_mode = 'RGB' - else: - ims.color_mode = 'BW' - - # all pngs with max compression - if ims.file_format == 'PNG': - ims.compression = 100 - # all jpgs brought to reasonable quality - if ims.file_format == 'JPG': - ims.quality = JPEG_QUALITY - # it's actually very important not to try to change the image filepath and packed file filepath before saving, - # blender tries to re-pack the image after writing to image.packed_image.filepath and reverts any changes. - teximage.save_render(filepath=bpy.path.abspath(filepath), scene=bpy.context.scene) - - teximage.filepath = filepath - for packed_file in teximage.packed_files: - packed_file.filepath = filepath - teximage.filepath_raw = filepath - teximage.reload() - - ims.file_format = orig_file_format - ims.quality = orig_quality - ims.color_mode = orig_color_mode - ims.compression = orig_compression - - -def extxchange_to_resolution(filepath): - base, ext = os.path.splitext(filepath) - if ext in ('.png', '.PNG'): - ext = 'jpg' - - - - - - -def upload_resolutions(files, asset_data): - preferences = bpy.context.preferences.addons['blenderkit'].preferences - - upload_data = { - "name": asset_data['name'], - "token": preferences.api_key, - "id": asset_data['id'] - } - - uploaded = upload_bg.upload_files(upload_data, files) - - if uploaded: - bg_blender.progress('upload finished successfully') - else: - bg_blender.progress('upload failed.') - - -def unpack_asset(data): - utils.p('unpacking asset') - asset_data = data['asset_data'] - # utils.pprint(asset_data) - - blend_file_name = os.path.basename(bpy.data.filepath) - ext = os.path.splitext(blend_file_name)[1] - - resolution = asset_data.get('resolution', 'blend') - # TODO - passing resolution inside asset data might not be the best solution - tex_dir_path = paths.get_texture_directory(asset_data, resolution=resolution) - tex_dir_abs = bpy.path.abspath(tex_dir_path) - if not os.path.exists(tex_dir_abs): - try: - os.mkdir(tex_dir_abs) - except Exception as e: - print(e) - bpy.data.use_autopack = False - for image in bpy.data.images: - if image.name != 'Render Result': - # suffix = paths.resolution_suffix(data['suffix']) - fp = get_texture_filepath(tex_dir_path, image, resolution=resolution) - utils.p('unpacking file', image.name) - utils.p(image.filepath, fp) - - for pf in image.packed_files: - pf.filepath = fp # bpy.path.abspath(fp) - image.filepath = fp # bpy.path.abspath(fp) - image.filepath_raw = fp # bpy.path.abspath(fp) - # image.save() - if len(image.packed_files) > 0: - # image.unpack(method='REMOVE') - image.unpack(method='WRITE_ORIGINAL') - - #mark asset browser asset - data_block = None - if asset_data['assetType'] == 'model': - for ob in bpy.data.objects: - if ob.parent is None and ob in bpy.context.visible_objects: - ob.asset_mark() - # for c in bpy.data.collections: - # if c.get('asset_data') is not None: - # c.asset_mark() - # data_block = c - elif asset_data['assetType'] == 'material': - for m in bpy.data.materials: - m.asset_mark() - data_block = m - elif asset_data['assetType'] == 'scene': - bpy.context.scene.asset_mark() - elif asset_data['assetType'] =='brush': - for b in bpy.data.brushes: - if b.get('asset_data') is not None: - b.asset_mark() - data_block = b - if data_block is not None: - tags = data_block.asset_data.tags - for t in tags: - tags.remove(t) - tags.new('description: ' + asset_data['description']) - tags.new('tags: ' + ','.join(asset_data['tags'])) - # - # if this isn't here, blender crashes when saving file. - bpy.context.preferences.filepaths.file_preview_type = 'NONE' - - bpy.ops.wm.save_as_mainfile(filepath = bpy.data.filepath, compress=False) - # now try to delete the .blend1 file - try: - - os.remove(bpy.data.filepath + '1') - except Exception as e: - print(e) - - -def patch_asset_empty(asset_id, api_key): - ''' - This function patches the asset for the purpose of it getting a reindex. - Should be removed once this is fixed on the server and - the server is able to reindex after uploads of resolutions - Returns - ------- - ''' - upload_data = { - } - url = paths.get_api_url() + 'assets/' + str(asset_id) + '/' - headers = utils.get_headers(api_key) - try: - r = rerequests.patch(url, json=upload_data, headers=headers, verify=True) # files = files, - except requests.exceptions.RequestException as e: - print(e) - return {'CANCELLED'} - return {'FINISHED'} - - -def reduce_all_images(target_scale=1024): - for img in bpy.data.images: - if img.name != 'Render Result': - print('scaling ', img.name, img.size[0], img.size[1]) - # make_possible_reductions_on_image(i) - if max(img.size) > target_scale: - ratio = float(target_scale) / float(max(img.size)) - print(ratio) - # i.save() - fp = '//tempimagestorage' - # print('generated filename',fp) - # for pf in img.packed_files: - # pf.filepath = fp # bpy.path.abspath(fp) - - img.filepath = fp - img.filepath_raw = fp - print(int(img.size[0] * ratio), int(img.size[1] * ratio)) - img.scale(int(img.size[0] * ratio), int(img.size[1] * ratio)) - img.update() - # img.save() - # img.reload() - img.pack() - - -def get_texture_filepath(tex_dir_path, image, resolution='blend'): - image_file_name = bpy.path.basename(image.filepath) - if image_file_name == '': - image_file_name = image.name.split('.')[0] - - suffix = paths.resolution_suffix[resolution] - - fp = os.path.join(tex_dir_path, image_file_name) - # check if there is allready an image with same name and thus also assigned path - # (can happen easily with genearted tex sets and more materials) - done = False - fpn = fp - i = 0 - while not done: - is_solo = True - for image1 in bpy.data.images: - if image != image1 and image1.filepath == fpn: - is_solo = False - fpleft, fpext = os.path.splitext(fp) - fpn = fpleft + str(i).zfill(3) + fpext - i += 1 - if is_solo: - done = True - - return fpn - - -def generate_lower_resolutions_hdr(asset_data, fpath): - '''generates lower resolutions for HDR images''' - hdr = bpy.data.images.load(fpath) - actres = max(hdr.size[0], hdr.size[1]) - p2res = paths.round_to_closest_resolution(actres) - original_filesize = os.path.getsize(fpath) # for comparison on the original level - i = 0 - finished = False - files = [] - while not finished: - dirn = os.path.dirname(fpath) - fn_strip, ext = os.path.splitext(fpath) - ext = '.exr' - if i>0: - image_utils.downscale(hdr) - - - hdr_resolution_filepath = fn_strip + paths.resolution_suffix[p2res] + ext - image_utils.img_save_as(hdr, filepath=hdr_resolution_filepath, file_format='OPEN_EXR', quality=20, color_mode='RGB', compression=15, - view_transform='Raw', exr_codec = 'DWAA') - - if os.path.exists(hdr_resolution_filepath): - reduced_filesize = os.path.getsize(hdr_resolution_filepath) - - # compare file sizes - print(f'HDR size was reduced from {original_filesize} to {reduced_filesize}') - if reduced_filesize < original_filesize: - # this limits from uploaidng especially same-as-original resolution files in case when there is no advantage. - # usually however the advantage can be big also for same as original resolution - files.append({ - "type": p2res, - "index": 0, - "file_path": hdr_resolution_filepath - }) - - print('prepared resolution file: ', p2res) - - if rkeys.index(p2res) == 0: - finished = True - else: - p2res = rkeys[rkeys.index(p2res) - 1] - i+=1 - - print('uploading resolution files') - upload_resolutions(files, asset_data) - - preferences = bpy.context.preferences.addons['blenderkit'].preferences - patch_asset_empty(asset_data['id'], preferences.api_key) - - -def generate_lower_resolutions(data): - asset_data = data['asset_data'] - actres = get_current_resolution() - # first let's skip procedural assets - base_fpath = bpy.data.filepath - - s = bpy.context.scene - - print('current resolution of the asset ', actres) - if actres > 0: - p2res = paths.round_to_closest_resolution(actres) - orig_res = p2res - print(p2res) - finished = False - files = [] - # now skip assets that have lowest possible resolution already - if p2res != [0]: - original_textures_filesize = 0 - for i in bpy.data.images: - abspath = bpy.path.abspath(i.filepath) - if os.path.exists(abspath): - original_textures_filesize += os.path.getsize(abspath) - - while not finished: - - blend_file_name = os.path.basename(base_fpath) - - dirn = os.path.dirname(base_fpath) - fn_strip, ext = os.path.splitext(blend_file_name) - - fn = fn_strip + paths.resolution_suffix[p2res] + ext - fpath = os.path.join(dirn, fn) - - tex_dir_path = paths.get_texture_directory(asset_data, resolution=p2res) - - tex_dir_abs = bpy.path.abspath(tex_dir_path) - if not os.path.exists(tex_dir_abs): - os.mkdir(tex_dir_abs) - - reduced_textures_filessize = 0 - for i in bpy.data.images: - if i.name != 'Render Result': - - print('scaling ', i.name, i.size[0], i.size[1]) - fp = get_texture_filepath(tex_dir_path, i, resolution=p2res) - - if p2res == orig_res: - # first, let's link the image back to the original one. - i['blenderkit_original_path'] = i.filepath - # first round also makes reductions on the image, while keeping resolution - image_utils.make_possible_reductions_on_image(i, fp, do_reductions=True, do_downscale=False) - - else: - # lower resolutions only downscale - image_utils.make_possible_reductions_on_image(i, fp, do_reductions=False, do_downscale=True) - - abspath = bpy.path.abspath(i.filepath) - if os.path.exists(abspath): - reduced_textures_filessize += os.path.getsize(abspath) - - i.pack() - # save - print(fpath) - # if this isn't here, blender crashes. - bpy.context.preferences.filepaths.file_preview_type = 'NONE' - - # save the file - bpy.ops.wm.save_as_mainfile(filepath=fpath, compress=True, copy=True) - # compare file sizes - print(f'textures size was reduced from {original_textures_filesize} to {reduced_textures_filessize}') - if reduced_textures_filessize < original_textures_filesize: - # this limits from uploaidng especially same-as-original resolution files in case when there is no advantage. - # usually however the advantage can be big also for same as original resolution - files.append({ - "type": p2res, - "index": 0, - "file_path": fpath - }) - - print('prepared resolution file: ', p2res) - if rkeys.index(p2res) == 0: - finished = True - else: - p2res = rkeys[rkeys.index(p2res) - 1] - print('uploading resolution files') - upload_resolutions(files, data['asset_data']) - preferences = bpy.context.preferences.addons['blenderkit'].preferences - patch_asset_empty(data['asset_data']['id'], preferences.api_key) - return - - -def regenerate_thumbnail_material(data): - # this should re-generate material thumbnail and re-upload it. - # first let's skip procedural assets - base_fpath = bpy.data.filepath - blend_file_name = os.path.basename(base_fpath) - bpy.ops.mesh.primitive_cube_add() - aob = bpy.context.active_object - bpy.ops.object.material_slot_add() - aob.material_slots[0].material = bpy.data.materials[0] - props = aob.active_material.blenderkit - props.thumbnail_generator_type = 'BALL' - props.thumbnail_background = False - props.thumbnail_resolution = '256' - # 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_resolution') - # layout.prop(props, 'thumbnail_samples') - # layout.prop(props, 'thumbnail_denoising') - # layout.prop(props, 'adaptive_subdivision') - # preferences = bpy.context.preferences.addons['blenderkit'].preferences - # layout.prop(preferences, "thumbnail_use_gpu") - # TODO: here it should call start_material_thumbnailer , but with the wait property on, so it can upload afterwards. - bpy.ops.object.blenderkit_generate_material_thumbnail() - time.sleep(130) - # save - # this does the actual job - - return - - -def assets_db_path(): - dpath = os.path.dirname(bpy.data.filepath) - fpath = os.path.join(dpath, 'all_assets.json') - return fpath - - -def get_assets_search(): - # bpy.app.debug_value = 2 - - results = [] - preferences = bpy.context.preferences.addons['blenderkit'].preferences - url = paths.get_api_url() + 'search/all' - i = 0 - while url is not None: - headers = utils.get_headers(preferences.api_key) - print('fetching assets from assets endpoint') - print(url) - retries = 0 - while retries < 3: - r = rerequests.get(url, headers=headers) - - try: - adata = r.json() - url = adata.get('next') - print(i) - i += 1 - except Exception as e: - print(e) - print('failed to get next') - if retries == 2: - url = None - if adata.get('results') != None: - results.extend(adata['results']) - retries = 3 - print(f'fetched page {i}') - retries += 1 - - fpath = assets_db_path() - with open(fpath, 'w', encoding = 'utf-8') as s: - json.dump(results, s, ensure_ascii=False, indent=4) - - -def get_assets_for_resolutions(page_size=100, max_results=100000000): - preferences = bpy.context.preferences.addons['blenderkit'].preferences - - dpath = os.path.dirname(bpy.data.filepath) - filepath = os.path.join(dpath, 'assets_for_resolutions.json') - params = { - 'order': '-created', - 'textureResolutionMax_gte': '100', - # 'last_resolution_upload_lt':'2020-9-01' - } - search.get_search_simple(params, filepath=filepath, page_size=page_size, max_results=max_results, - api_key=preferences.api_key) - return filepath - - -def get_materials_for_validation(page_size=100, max_results=100000000): - preferences = bpy.context.preferences.addons['blenderkit'].preferences - dpath = os.path.dirname(bpy.data.filepath) - filepath = os.path.join(dpath, 'materials_for_validation.json') - params = { - 'order': '-created', - 'asset_type': 'material', - 'verification_status': 'uploaded' - } - search.get_search_simple(params, filepath=filepath, page_size=page_size, max_results=max_results, - api_key=preferences.api_key) - return filepath - - - - -def load_assets_list(filepath): - if os.path.exists(filepath): - with open(filepath, 'r', encoding='utf-8') as s: - assets = json.load(s) - return assets - - -def check_needs_resolutions(a): - if a['verificationStatus'] == 'validated' and a['assetType'] in ('material', 'model', 'scene', 'hdr'): - # the search itself now picks the right assets so there's no need to filter more than asset types. - # TODO needs to check first if the upload date is older than resolution upload date, for that we need resolution upload date. - for f in a['files']: - if f['fileType'].find('resolution') > -1: - return False - - return True - return False - - -def download_asset(asset_data, resolution='blend', unpack=False, api_key=''): - ''' - Download an asset non-threaded way. - Parameters - ---------- - asset_data - search result from elastic or assets endpoints from API - - Returns - ------- - path to the resulting asset file or None if asset isn't accessible - ''' - - has_url = download.get_download_url(asset_data, download.get_scene_id(), api_key, tcom=None, - resolution='blend') - if has_url: - fpath = download.download_asset_file(asset_data, api_key = api_key) - if fpath and unpack and asset_data['assetType'] != 'hdr': - send_to_bg(asset_data, fpath, command='unpack', wait=True) - return fpath - - return None - - -def generate_resolution_thread(asset_data, api_key): - ''' - A thread that downloads file and only then starts an instance of Blender that generates the resolution - Parameters - ---------- - asset_data - - Returns - ------- - - ''' - - fpath = download_asset(asset_data, unpack=True, api_key=api_key) - - if fpath: - if asset_data['assetType'] != 'hdr': - print('send to bg ', fpath) - proc = send_to_bg(asset_data, fpath, command='generate_resolutions', wait=True); - else: - generate_lower_resolutions_hdr(asset_data, fpath) - # send_to_bg by now waits for end of the process. - # time.sleep((5)) - - -def iterate_for_resolutions(filepath, process_count=12, api_key='', do_checks = True): - ''' iterate through all assigned assets, check for those which need generation and send them to res gen''' - assets = load_assets_list(filepath) - print(len(assets)) - threads = [] - for asset_data in assets: - asset_data = search.parse_result(asset_data) - if asset_data is not None: - - if not do_checks or check_needs_resolutions(asset_data): - print('downloading and generating resolution for %s' % asset_data['name']) - # this is just a quick hack for not using original dirs in blendrkit... - generate_resolution_thread(asset_data, api_key) - # thread = threading.Thread(target=generate_resolution_thread, args=(asset_data, api_key)) - # thread.start() - # - # threads.append(thread) - # print('processes ', len(threads)) - # while len(threads) > process_count - 1: - # for t in threads: - # if not t.is_alive(): - # threads.remove(t) - # break; - # else: - # print(f'Failed to generate resolution:{asset_data["name"]}') - else: - print('not generated resolutions:', asset_data['name']) - - -def send_to_bg(asset_data, fpath, command='generate_resolutions', wait=True): - ''' - Send varioust task to a new blender instance that runs and closes after finishing the task. - This function waits until the process finishes. - The function tries to set the same bpy.app.debug_value in the instance of Blender that is run. - Parameters - ---------- - asset_data - fpath - file that will be processed - command - command which should be run in background. - - Returns - ------- - None - ''' - data = { - 'fpath': fpath, - 'debug_value': bpy.app.debug_value, - 'asset_data': asset_data, - 'command': command, - } - binary_path = bpy.app.binary_path - tempdir = tempfile.mkdtemp() - datafile = os.path.join(tempdir + 'resdata.json') - script_path = os.path.dirname(os.path.realpath(__file__)) - with open(datafile, 'w', encoding = 'utf-8') as s: - json.dump(data, s, ensure_ascii=False, indent=4) - - print('opening Blender instance to do processing - ', command) - - if wait: - proc = subprocess.run([ - binary_path, - "--background", - "-noaudio", - fpath, - "--python", os.path.join(script_path, "resolutions_bg.py"), - "--", datafile - ], bufsize=1, stdout=sys.stdout, stdin=subprocess.PIPE, creationflags=utils.get_process_flags()) - - else: - # TODO this should be fixed to allow multithreading. - proc = subprocess.Popen([ - binary_path, - "--background", - "-noaudio", - fpath, - "--python", os.path.join(script_path, "resolutions_bg.py"), - "--", datafile - ], bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, creationflags=utils.get_process_flags()) - return proc - - -def write_data_back(asset_data): - '''ensures that the data in the resolution file is the same as in the database.''' - pass; - - -def run_bg(datafile): - print('background file operation') - with open(datafile, 'r',encoding='utf-8') as f: - data = json.load(f) - bpy.app.debug_value = data['debug_value'] - write_data_back(data['asset_data']) - if data['command'] == 'generate_resolutions': - generate_lower_resolutions(data) - elif data['command'] == 'unpack': - unpack_asset(data) - elif data['command'] == 'regen_thumbnail': - regenerate_thumbnail_material(data) - -# load_assets_list() -# generate_lower_resolutions() -# class TestOperator(bpy.types.Operator): -# """Tooltip""" -# bl_idname = "object.test_anything" -# bl_label = "Test Operator" -# -# @classmethod -# def poll(cls, context): -# return True -# -# def execute(self, context): -# iterate_for_resolutions() -# return {'FINISHED'} -# -# -# def register(): -# bpy.utils.register_class(TestOperator) -# -# -# def unregister(): -# bpy.utils.unregister_class(TestOperator) diff --git a/blenderkit/resolutions_bg.py b/blenderkit/resolutions_bg.py deleted file mode 100644 index c59ca08d..00000000 --- a/blenderkit/resolutions_bg.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys -import json -from blenderkit import resolutions - -BLENDERKIT_EXPORT_DATA = sys.argv[-1] - -if __name__ == "__main__": - resolutions.run_bg(sys.argv[-1]) diff --git a/blenderkit/search.py b/blenderkit/search.py deleted file mode 100644 index 125b7266..00000000 --- a/blenderkit/search.py +++ /dev/null @@ -1,1682 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -from blenderkit import paths, utils, categories, ui, colors, bkit_oauth, version_checker, tasks_queue, rerequests, \ - resolutions, image_utils, ratings_utils, comments_utils, reports - -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 platform -import bpy -import copy -import json -import math -import unicodedata -import urllib -import queue -import logging - -bk_logger = logging.getLogger('blenderkit') - -search_start_time = 0 -prev_time = 0 - - -def check_errors(rdata): - if rdata.get('statusCode') and int(rdata.get('statusCode')) > 299: - utils.p(rdata) - if rdata.get('detail') == 'Invalid token.': - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.api_key != '': - if user_preferences.enable_oauth: - bkit_oauth.refresh_token_thread() - return False, rdata.get('detail') - return False, 'Use login panel to connect your profile.' - else: - return False, rdata.get('detail') - if rdata.get('statusCode') is None and rdata.get('results') is None: - return False, 'Connection error' - return True, '' - - -search_threads = [] -thumb_workers_sml = [] -thumb_workers_full = [] -thumb_sml_download_threads = queue.Queue() -thumb_full_download_threads = queue.Queue() -reports_queue = queue.Queue() -all_thumbs_loaded = True - -rtips_string = """You can disable tips in the add-on preferences. -Ratings help us distribute funds to creators. -Creators also gain credits for free assets from subscribers. -Click or drag model or material in scene to link/append -Right click in the asset bar for more options -Use Append in import settings if you want to edit downloaded objects. -Please rate responsively and plentifully. This helps us distribute rewards to the authors. -All materials are free. -Storage for public assets is unlimited. -Locked models are available if you subscribe to Full plan. -Login to upload your own models, materials or brushes. -Use 'A' key over the asset bar to search assets by the same author. -Use semicolon - ; to hide or show the AssetBar. -Support the authors by subscribing to Full plan. -""" -rtips = rtips_string.splitlines() - - -def refresh_token_timer(): - ''' this timer gets run every time the token needs refresh. It refreshes tokens and also categories.''' - utils.p('refresh timer') - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - fetch_server_data() - categories.load_categories() - - return max(3600, user_preferences.api_key_life - 3600) - - -def refresh_notifications_timer(): - ''' this timer gets notifications.''' - preferences = bpy.context.preferences.addons['blenderkit'].preferences - fetch_server_data() - all_notifications_count = comments_utils.count_all_notifications() - comments_utils.get_notifications_thread(preferences.api_key, all_count = all_notifications_count) - return 7200 - - -def update_ad(ad): - if not ad.get('assetBaseId'): - try: - ad['assetBaseId'] = ad['asset_base_id'] # this should stay ONLY for compatibility with older scenes - ad['assetType'] = ad['asset_type'] # this should stay ONLY for compatibility with older scenes - ad['verificationStatus'] = ad[ - 'verification_status'] # this should stay ONLY for compatibility with older scenes - ad['author'] = {} - ad['author']['id'] = ad['author_id'] # this should stay ONLY for compatibility with older scenes - ad['canDownload'] = ad['can_download'] # this should stay ONLY for compatibility with older scenes - except Exception as e: - bk_logger.error('BlenderKit failed to update older asset data') - return ad - - -def update_assets_data(): # updates assets data on scene load. - '''updates some properties that were changed on scenes with older assets. - The properties were mainly changed from snake_case to CamelCase to fit the data that is coming from the server. - ''' - data = bpy.data - - datablocks = [ - bpy.data.objects, - bpy.data.materials, - bpy.data.brushes, - ] - for dtype in datablocks: - for block in dtype: - if block.get('asset_data') != None: - update_ad(block['asset_data']) - - dicts = [ - 'assets used', - # 'assets rated',# assets rated stores only true/false, not asset data. - ] - for s in bpy.data.scenes: - for bkdict in dicts: - - d = s.get(bkdict) - if not d: - continue; - - for asset_id in d.keys(): - update_ad(d[asset_id]) - # bpy.context.scene['assets used'][ad] = ad - - -def purge_search_results(): - ''' clean up search results on save/load.''' - - s = bpy.context.scene - - sr_props = [ - 'search results', - 'search results orig', - ] - asset_types = ['model', 'material', 'scene', 'hdr', 'brush'] - for at in asset_types: - sr_props.append('bkit {at} search') - sr_props.append('bkit {at} search orig') - for sr_prop in sr_props: - if s.get(sr_prop): - del (s[sr_prop]) - - -@persistent -def scene_load(context): - ''' - Loads categories , checks timers registration, and updates scene asset data. - Should (probably)also update asset data from server (after user consent) - ''' - wm = bpy.context.window_manager - purge_search_results() - fetch_server_data() - categories.load_categories() - if not bpy.app.timers.is_registered(refresh_token_timer) and not bpy.app.background: - bpy.app.timers.register(refresh_token_timer, persistent=True, first_interval=36000) - # if utils.experimental_enabled() and not bpy.app.timers.is_registered( - # refresh_notifications_timer) and not bpy.app.background: - # bpy.app.timers.register(refresh_notifications_timer, persistent=True, first_interval=5) - - update_assets_data() - - -def fetch_server_data(): - ''' download categories , profile, and refresh token if needed.''' - if not bpy.app.background: - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - api_key = user_preferences.api_key - # Only refresh new type of tokens(by length), and only one hour before the token timeouts. - if user_preferences.enable_oauth and \ - len(user_preferences.api_key) < 38 and len(user_preferences.api_key) > 0 and \ - user_preferences.api_key_timeout < time.time() + 3600: - bkit_oauth.refresh_token_thread() - if api_key != '' and bpy.context.window_manager.get('bkit profile') == None: - get_profile() - if bpy.context.window_manager.get('bkit_categories') is None: - categories.fetch_categories_thread(api_key, force=False) - # all_notifications_count = comments_utils.count_all_notifications() - # comments_utils.get_notifications_thread(api_key, all_count = all_notifications_count) - -first_time = True -first_search_parsing = True -last_clipboard = '' - - -def check_clipboard(): - ''' - Checks clipboard for an exact string containing asset ID. - The string is generated on www.blenderkit.com as for example here: - https://www.blenderkit.com/get-blenderkit/54ff5c85-2c73-49e9-ba80-aec18616a408/ - ''' - - # clipboard monitoring to search assets from web - if platform.system() != 'Linux': - global last_clipboard - if bpy.context.window_manager.clipboard != last_clipboard: - last_clipboard = bpy.context.window_manager.clipboard - instr = 'asset_base_id:' - # first check if contains asset id, then asset type - if last_clipboard[:len(instr)] == instr: - atstr = 'asset_type:' - ati = last_clipboard.find(atstr) - # this only checks if the asset_type keyword is there but let's the keywords update function do the parsing. - if ati > -1: - search_props = utils.get_search_props() - search_props.search_keywords = last_clipboard - # don't run search after this - assigning to keywords runs the search_update function. - - -def parse_result(r): - ''' - needed to generate some extra data in the result(by now) - Parameters - ---------- - r - search result, also called asset_data - ''' - scene = bpy.context.scene - - # TODO remove this fix when filesSize is fixed. - # this is a temporary fix for too big numbers from the server. - # try: - # r['filesSize'] = int(r['filesSize'] / 1024) - # except: - # utils.p('asset with no files-size') - asset_type = r['assetType'] - if len(r['files']) > 0: # TODO remove this condition so all assets are parsed. - get_author(r) - - r['available_resolutions'] = [] - allthumbs = [] - durl, tname, small_tname = '', '', '' - - if r['assetType'] == 'hdr': - tname = paths.extract_filename_from_url(r['thumbnailLargeUrlNonsquared']) - else: - tname = paths.extract_filename_from_url(r['thumbnailMiddleUrl']) - small_tname = paths.extract_filename_from_url(r['thumbnailSmallUrl']) - allthumbs.append(tname) # TODO just first thumb is used now. - # if r['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. - - 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. - - if f['fileType'] == 'blend': - durl = f['downloadUrl'].split('?')[0] - # fname = paths.extract_filename_from_url(f['filePath']) - - if f['fileType'].find('resolution') > -1: - r['available_resolutions'].append(resolutions.resolutions[f['fileType']]) - - # code for more thumbnails - # tdict = {} - # for i, t in enumerate(allthumbs): - # tdict['thumbnail_%i'] = t - - r['max_resolution'] = 0 - if r['available_resolutions']: # should check only for non-empty sequences - r['max_resolution'] = max(r['available_resolutions']) - - # tooltip = generate_tooltip(r) - # for some reason, the id was still int on some occurances. investigate this. - r['author']['id'] = str(r['author']['id']) - - # some helper props, but generally shouldn't be renaming/duplifiying original properties, - # so blender's data is same as on server. - asset_data = {'thumbnail': tname, - 'thumbnail_small': small_tname, - # 'tooltip': tooltip, - - } - asset_data['downloaded'] = 0 - - # parse extra params needed for blender here - params = r['dictParameters'] # utils.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) - - au = scene.get('assets used', {}) - if au == {}: - scene['assets used'] = au - if r['assetBaseId'] in au.keys(): - asset_data['downloaded'] = 100 - # transcribe all urls already fetched from the server - r_previous = au[r['assetBaseId']] - if r_previous.get('files'): - for f in r_previous['files']: - if f.get('url'): - for f1 in r['files']: - if f1['fileType'] == f['fileType']: - f1['url'] = f['url'] - - # attempt to switch to use original data gradually, since the parsing as itself should become obsolete. - asset_data.update(r) - return asset_data - - -# @bpy.app.handlers.persistent -def search_timer(): - # this makes a first search after opening blender. showing latest assets. - # utils.p('timer search') - # utils.p('start search timer') - global first_time - preferences = bpy.context.preferences.addons['blenderkit'].preferences - if first_time and not bpy.app.background: # first time - - first_time = False - if preferences.show_on_start: - # TODO here it should check if there are some results, and only open assetbar if this is the case, not search. - # if bpy.context.window_manager.get('search results') is None: - search() - # preferences.first_run = False - if preferences.tips_on_start: - utils.get_largest_area() - ui.update_ui_size(ui.active_area_pointer, ui.active_region_pointer) - reports.add_report(text='BlenderKit Tip: ' + random.choice(rtips), timeout=12, color=colors.GREEN) - # utils.p('end search timer') - - return 3.0 - - # if preferences.first_run: - # search() - # preferences.first_run = False - - # check_clipboard() - - # finish loading thumbs from queues - global all_thumbs_loaded - if not all_thumbs_loaded: - ui_props = bpy.context.window_manager.blenderkitUI - search_name = f'bkit {ui_props.asset_type.lower()} search' - wm = bpy.context.window_manager - if wm.get(search_name) is not None: - all_loaded = True - for ri, r in enumerate(wm[search_name]): - if not r.get('thumb_small_loaded'): - preview_loaded = load_preview(r, ri) - all_loaded = all_loaded and preview_loaded - - all_thumbs_loaded = all_loaded - - global search_threads, first_search_parsing - if len(search_threads) == 0: - # utils.p('end search timer') - props = utils.get_search_props() - props.is_searching = False - return 1.0 - # don't do anything while dragging - this could switch asset during drag, and make results list length different, - # causing a lot of throuble. - if bpy.context.window_manager.blenderkitUI.dragging: - # utils.p('end search timer') - - return 0.5 - - for thread in search_threads: - # TODO this doesn't check all processes when one gets removed, - # but most of the time only one is running anyway - if not thread[0].is_alive(): - - #check for notifications only for users that actually use the add-on - if first_search_parsing: - first_search_parsing = False - all_notifications_count = comments_utils.count_all_notifications() - comments_utils.get_notifications_thread(preferences.api_key, all_count=all_notifications_count) - if utils.experimental_enabled() and not bpy.app.timers.is_registered( - refresh_notifications_timer) and not bpy.app.background: - bpy.app.timers.register(refresh_notifications_timer, persistent=True, first_interval=5) - - 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. - wm = bpy.context.window_manager - asset_type = thread[2] - - props = utils.get_search_props() - search_name = f'bkit {asset_type} search' - - if not thread[0].params.get('get_next'): - # wm[search_name] = [] - result_field = [] - else: - result_field = [] - for r in wm[search_name]: - result_field.append(r.to_dict()) - - global reports_queue - - while not reports_queue.empty(): - props.report = str(reports_queue.get()) - # utils.p('end search timer') - - return .2 - - rdata = thread[0].result - - ok, error = check_errors(rdata) - if ok: - ui_props = bpy.context.window_manager.blenderkitUI - orig_len = len(result_field) - - for ri, r in enumerate(rdata['results']): - asset_data = parse_result(r) - if asset_data != None: - result_field.append(asset_data) - all_thumbs_loaded = all_thumbs_loaded and load_preview(asset_data, ri + orig_len) - - # Get ratings from BlenderKit server - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - api_key = user_preferences.api_key - headers = utils.get_headers(api_key) - if utils.profile_is_validator(): - for r in rdata['results']: - if ratings_utils.get_rating_local(r['id']) is None: - rating_thread = threading.Thread(target=ratings_utils.get_rating, args=([r['id'], headers]), - daemon=True) - rating_thread.start() - - wm[search_name] = result_field - wm['search results'] = result_field - - # rdata=['results']=[] - wm[search_name + ' orig'] = rdata - wm['search results orig'] = rdata - - if len(result_field) < ui_props.scroll_offset or not (thread[0].params.get('get_next')): - # jump back - ui_props.scroll_offset = 0 - props.search_error = False - props.report = f"Found {wm['search results orig']['count']} results." - if len(wm['search results']) == 0: - tasks_queue.add_task((reports.add_report, ('No matching results found.',))) - else: - tasks_queue.add_task((reports.add_report, (f"Found {wm['search results orig']['count']} results.",))) - # undo push - # bpy.ops.wm.undo_push_context(message='Get BlenderKit search') - # show asset bar automatically, but only on first page - others are loaded also when asset bar is hidden. - if not ui_props.assetbar_on and not thread[0].params.get('get_next'): - bpy.ops.object.run_assetbar_fix_context() - - else: - bk_logger.error(error) - props.report = error - props.search_error = True - - props.is_searching = False - # print('finished search thread') - mt('preview loading finished') - # utils.p('end search timer') - if not all_thumbs_loaded: - return .1 - return .3 - - -def load_preview(asset, index): - # FIRST START SEARCH - props = bpy.context.window_manager.blenderkitUI - directory = paths.get_temp_dir('%s_search' % props.asset_type.lower()) - - tpath = os.path.join(directory, asset['thumbnail_small']) - if not asset['thumbnail_small'] or asset['thumbnail_small'] == '' or not os.path.exists(tpath): - # tpath = paths.get_addon_thumbnail_path('thumbnail_notready.jpg') - asset['thumb_small_loaded'] = False - - iname = utils.previmg_name(index) - - # if os.path.exists(tpath): # sometimes we are unlucky... - img = bpy.data.images.get(iname) - - if img is None or len(img.pixels) == 0: - if not os.path.exists(tpath): - return False - # wrap into try statement since sometimes - try: - img = bpy.data.images.load(tpath) - - img.name = iname - if len(img.pixels)>0: - return True - except: - pass - return False - elif img.filepath != tpath: - if not os.path.exists(tpath): - # unload loaded previews from previous results - bpy.data.images.remove(img) - return False - # had to add this check for autopacking files... - if bpy.data.use_autopack and img.packed_file is not None: - img.unpack(method='USE_ORIGINAL') - img.filepath = tpath - try: - img.reload() - except: - return False - - if asset['assetType'] == 'hdr': - # to display hdr thumbnails correctly, we use non-color, otherwise looks shifted - image_utils.set_colorspace(img, 'Non-Color') - else: - image_utils.set_colorspace(img, 'sRGB') - asset['thumb_small_loaded'] = True - return True - - -def load_previews(): - scene = bpy.context.scene - # FIRST START SEARCH - props = bpy.context.window_manager.blenderkitUI - directory = paths.get_temp_dir('%s_search' % props.asset_type.lower()) - s = bpy.context.scene - results = bpy.context.window_manager.get('search results') - # - if results is not None: - i = 0 - for r in results: - load_preview(r, i) - i += 1 - - -# line splitting for longer texts... -def split_subs(text, threshold=40): - if text == '': - return [] - # temporarily disable this, to be able to do this in drawing code - - text = text.rstrip() - text = text.replace('\r\n', '\n') - - lines = [] - - while len(text) > threshold: - # first handle if there's an \n line ending - i_rn = text.find('\n') - if 1 < i_rn < threshold: - i = i_rn - text = text.replace('\n', '', 1) - else: - i = text.rfind(' ', 0, threshold) - i1 = text.rfind(',', 0, threshold) - i2 = text.rfind('.', 0, threshold) - i = max(i, i1, i2) - if i <= 0: - 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, width=40): # for longer texts - dlines = split_subs(input, threshold=width) - for i, l in enumerate(dlines): - t += '%s\n' % l - return t - - -def writeblockm(tooltip, mdata, key='', pretext=None, width=40): # for longer texts - if mdata.get(key) == None: - return tooltip - else: - intext = mdata[key] - if type(intext) == list: - intext = list_to_str(intext) - if type(intext) == float: - intext = round(intext, 3) - intext = str(intext) - if intext.rstrip() == '': - return tooltip - if pretext == None: - pretext = key - if pretext != '': - pretext = pretext + ': ' - text = pretext + intext - dlines = split_subs(text, threshold=width) - for i, l in enumerate(dlines): - tooltip += '%s\n' % l - - return tooltip - - -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 generate_tooltip(mdata): - col_w = 40 - if type(mdata['parameters']) == list: - mparams = utils.params_to_dict(mdata['parameters']) - else: - mparams = mdata['parameters'] - t = '' - t = writeblock(t, mdata['displayName'], width=int(col_w * .6)) - # t += '\n' - - # t = writeblockm(t, mdata, key='description', pretext='', width=col_w) - return t - - -def get_random_tip(): - t = '' - tip = 'Tip: ' + random.choice(rtips) - t = writeblock(t, tip) - return t - - -def generate_author_textblock(adata): - t = '' - - if adata not in (None, ''): - col_w = 2000 - if len(adata['firstName'] + adata['lastName']) > 0: - t = '%s %s\n' % (adata['firstName'], adata['lastName']) - t += '\n' - if adata.get('aboutMe') is not None: - t = writeblockm(t, adata, key='aboutMe', pretext='', width=col_w) - return t - - -def download_image(session, url, filepath): - r = None - try: - r = session.get(url, stream=False) - except Exception as e: - bk_logger.error('Thumbnail download failed') - bk_logger.error(str(e)) - if r and r.status_code == 200: - with open(filepath, 'wb') as f: - f.write(r.content) - - -def thumb_download_worker(queue_sml, queue_full): - # print('thumb downloader', self.url) - # utils.p('start thumbdownloader thread') - while 1: - session = None - # start a session only for single search usually, if users starts scrolling, the session might last longer if - # queue gets filled. - if not queue_sml.empty() or not queue_full.empty(): - if session is None: - session = requests.Session() - while not queue_sml.empty(): - # first empty the small thumbs queue - url, filepath = queue_sml.get() - download_image(session, url, filepath) - exit_full = False - # download full resolution image, but only if no small thumbs are waiting. If there are small - while not queue_full.empty() and queue_sml.empty(): - url, filepath = queue_full.get() - download_image(session, url, filepath) - - if queue_sml.empty() and queue_full.empty(): - if session is not None: - session.close() - session = None - time.sleep(.5) - - -def write_gravatar(a_id, gravatar_path): - ''' - Write down gravatar path, as a result of thread-based gravatar image download. - This should happen on timer in queue. - ''' - # print('write author', a_id, type(a_id)) - authors = bpy.context.window_manager['bkit authors'] - if authors.get(a_id) is not None: - adata = authors.get(a_id) - adata['gravatarImg'] = gravatar_path - - -def fetch_gravatar(adata = None): - - ''' - Gets avatars from blenderkit server - Parameters - ---------- - adata - author data from elastic search result - - ''' - # utils.p('fetch gravatar') - # print(adata) - # fetch new avatars if available already - if adata.get('avatar128') is not None: - avatar_path = paths.get_temp_dir(subdir='bkit_g/') + adata['id'] + '.jpg' - if os.path.exists(avatar_path): - tasks_queue.add_task((write_gravatar, (adata['id'], avatar_path))) - return; - - url = paths.get_bkit_url() + adata['avatar128'] - r = rerequests.get(url, stream=False) - # print(r.body) - if r.status_code == 200: - # print(url) - # print(r.headers['content-disposition']) - with open(avatar_path, 'wb') as f: - f.write(r.content) - tasks_queue.add_task((write_gravatar, (adata['id'], avatar_path))) - elif r.status_code == '404': - adata['avatar128'] = None - utils.p('avatar for author not available.') - return - - # older gravatar code - if adata.get('gravatarHash') is not None: - gravatar_path = paths.get_temp_dir(subdir='bkit_g/') + adata['gravatarHash'] + '.jpg' - - if os.path.exists(gravatar_path): - tasks_queue.add_task((write_gravatar, (adata['id'], gravatar_path))) - return; - - url = "https://www.gravatar.com/avatar/" + adata['gravatarHash'] + '?d=404' - r = rerequests.get(url, stream=False) - if r.status_code == 200: - with open(gravatar_path, 'wb') as f: - f.write(r.content) - tasks_queue.add_task((write_gravatar, (adata['id'], gravatar_path))) - elif r.status_code == '404': - adata['gravatarHash'] = None - utils.p('gravatar for author not available.') - - -fetching_gravatars = {} - - -def get_author(r): - ''' Writes author info (now from search results) and fetches gravatar if needed. - this is now tweaked to be able to get authors from - ''' - global fetching_gravatars - - a_id = str(r['author']['id']) - preferences = bpy.context.preferences.addons['blenderkit'].preferences - authors = bpy.context.window_manager.get('bkit authors', {}) - if authors == {}: - bpy.context.window_manager['bkit authors'] = authors - a = authors.get(a_id) - if a is None: # or a is '' or (a.get('gravatarHash') is not None and a.get('gravatarImg') is None): - a = r['author'] - a['id'] = a_id - a['tooltip'] = generate_author_textblock(a) - - authors[a_id] = a - if fetching_gravatars.get(a['id']) is None: - fetching_gravatars[a['id']] = True - - thread = threading.Thread(target=fetch_gravatar, args=(a.copy(),), daemon=True) - thread.start() - return a - - -def write_profile(adata): - utils.p('writing profile information') - user = adata['user'] - # we have to convert to MiB here, numbers too big for python int type - if user.get('sumAssetFilesSize') is not None: - user['sumAssetFilesSize'] /= (1024 * 1024) - if user.get('sumPrivateAssetFilesSize') is not None: - user['sumPrivateAssetFilesSize'] /= (1024 * 1024) - if user.get('remainingPrivateQuota') is not None: - user['remainingPrivateQuota'] /= (1024 * 1024) - - if adata.get('canEditAllAssets') is True: - user['exmenu'] = True - else: - user['exmenu'] = False - - bpy.context.window_manager['bkit profile'] = adata - - -def request_profile(api_key): - a_url = paths.get_api_url() + 'me/' - headers = utils.get_headers(api_key) - r = rerequests.get(a_url, headers=headers) - adata = r.json() - if adata.get('user') is None: - utils.p(adata) - utils.p('getting profile failed') - return None - return adata - - -def fetch_profile(api_key): - utils.p('fetch profile') - try: - adata = request_profile(api_key) - if adata is not None: - tasks_queue.add_task((write_profile, (adata,))) - except Exception as e: - bk_logger.error(e) - - -def get_profile(): - preferences = bpy.context.preferences.addons['blenderkit'].preferences - a = bpy.context.window_manager.get('bkit profile') - thread = threading.Thread(target=fetch_profile, args=(preferences.api_key,), daemon=True) - thread.start() - - return a - - -def query_to_url(query={}, params={}): - # build a new request - url = paths.get_api_url() + 'search/' - - # build request manually - # TODO use real queries - requeststring = '?query=' - # - if query.get('query') not in ('', None): - requeststring += query['query'].lower() - for i, q in enumerate(query): - if q != 'query': - requeststring += '+' - requeststring += q + ':' + str(query[q]).lower() - - # add dict_parameters to make results smaller - # result ordering: _score - relevance, score - BlenderKit score - order = [] - if params['free_first']: - order = ['-is_free', ] - if query.get('query') is None and query.get('category_subtree') == None: - # assumes no keywords and no category, thus an empty search that is triggered on start. - # orders by last core file upload - if query.get('verification_status') == 'uploaded': - # for validators, sort uploaded from oldest - order.append('created') - else: - order.append('-last_upload') - elif query.get('author_id') is not None and utils.profile_is_validator(): - - order.append('-created') - else: - if query.get('category_subtree') is not None: - order.append('-score,_score') - else: - order.append('_score') - if requeststring.find('+order:') == -1: - requeststring += '+order:' + ','.join(order) - requeststring += '&dict_parameters=1' - - requeststring += '&page_size=' + str(params['page_size']) - requeststring += '&addon_version=%s' % params['addon_version'] - if params.get('scene_uuid') is not None: - requeststring += '&scene_uuid=%s' % params['scene_uuid'] - # print('params', params) - urlquery = url + requeststring - return urlquery - - -def parse_html_formated_error(text): - report = text[text.find('<title>') + 7: text.find('</title>')] - - return report - - -class Searcher(threading.Thread): - query = None - - def __init__(self, query, params, tempdir='', headers=None, urlquery=''): - super(Searcher, self).__init__() - self.query = query - self.params = params - self._stop_event = threading.Event() - self.result = {} - self.tempdir = tempdir - self.headers = headers - self.urlquery = urlquery - - def stop(self): - self._stop_event.set() - - def stopped(self): - return self._stop_event.is_set() - - def run(self): - global reports_queue, thumb_sml_download_threads, thumb_full_download_threads - - maxthreads = 50 - query = self.query - params = self.params - - t = time.time() - # utils.p('start search thread') - - 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']) - - rdata = {} - rdata['results'] = [] - - try: - utils.p(self.urlquery) - r = rerequests.get(self.urlquery, headers=self.headers) # , params = rparameters) - except requests.exceptions.RequestException as e: - bk_logger.error(e) - reports_queue.put(str(e)) - # utils.p('end search thread') - - return - - mt('search response is back ') - try: - rdata = r.json() - except Exception as e: - if hasattr(r, 'text'): - error_description = parse_html_formated_error(r.text) - reports_queue.put(error_description) - tasks_queue.add_task((reports.add_report, (error_description, 10, colors.RED))) - - bk_logger.error(e) - return - mt('data parsed ') - if not rdata.get('results'): - # utils.pprint(rdata) - # if the result was converted to json and didn't return results, - # it means it's a server error that has a clear message. - # That's why it gets processed in the update timer, where it can be passed in messages to user. - self.result = rdata - # utils.p('end search thread') - - return - # print('number of results: ', len(rdata.get('results', []))) - if self.stopped(): - utils.p('stopping search : ' + str(query)) - # utils.p('end search thread') - - 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', []): - thumb_small_urls.append(d["thumbnailSmallUrl"]) - imgname = paths.extract_filename_from_url(d['thumbnailSmallUrl']) - imgpath = os.path.join(self.tempdir, imgname) - thumb_small_filepaths.append(imgpath) - - if d["assetType"] == 'hdr': - larege_thumb_url = d['thumbnailLargeUrlNonsquared'] - - else: - larege_thumb_url = d['thumbnailMiddleUrl'] - - thumb_full_urls.append(larege_thumb_url) - imgname = paths.extract_filename_from_url(larege_thumb_url) - imgpath = os.path.join(self.tempdir, imgname) - thumb_full_filepaths.append(imgpath) - - # 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(self.tempdir, imgname) - # thumb_small_filepaths.append(imgpath) - # - # imgname = paths.extract_filename_from_url(f['fileThumbnailLarge']) - # imgpath = os.path.join(self.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 downloaded thumbnails already... - - self.result = rdata - - if self.stopped(): - utils.p('stopping search : ' + str(query)) - # utils.p('end search thread') - return - - # this loop handles downloading of small thumbnails - for imgpath, url in sml_thbs: - if not os.path.exists(imgpath): - thumb_sml_download_threads.put((url, imgpath)) - - if self.stopped(): - utils.p('stopping search : ' + str(query)) - # utils.p('end search thread') - return - - if self.stopped(): - # utils.p('end search thread') - - utils.p('stopping search : ' + str(query)) - return - - # start downloading full thumbs in the end - tsession = requests.Session() - - for imgpath, url in full_thbs: - if not os.path.exists(imgpath): - thumb_full_download_threads.put((url, imgpath)) - # utils.p('end search thread') - mt('thumbnails finished') - - -def build_query_common(query, props): - '''add shared parameters to query''' - query_common = {} - if props.search_keywords != '': - # keywords = urllib.parse.urlencode(props.search_keywords) - keywords = props.search_keywords.replace('&', '%26') - query_common["query"] = keywords - - if props.search_verification_status != 'ALL' and utils.profile_is_validator(): - query_common['verification_status'] = props.search_verification_status.lower() - - if props.unrated_only and utils.profile_is_validator(): - query["quality_count"] = 0 - - if props.search_file_size: - query_common["files_size_gte"] = props.search_file_size_min * 1024 * 1024 - query_common["files_size_lte"] = props.search_file_size_max * 1024 * 1024 - - if props.quality_limit > 0: - query["quality_gte"] = props.quality_limit - - query.update(query_common) - - -def build_query_model(): - '''use all search input to request results from server''' - - props = bpy.context.window_manager.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["model_style"] = props.search_style - else: - query["model_style"] = props.search_style_other - - # the 'free_only' parametr gets moved to the search command and is used for ordering the assets as free first - # if props.free_only: - # query["is_free"] = True - - if props.search_condition != 'UNSPECIFIED': - query["condition"] = props.search_condition - - if props.search_design_year: - query["designYear_gte"] = props.search_design_year_min - query["designYear_lte"] = props.search_design_year_max - if props.search_polycount: - query["faceCount_gte"] = props.search_polycount_min - query["faceCount_lte"] = props.search_polycount_max - if props.search_texture_resolution: - query["textureResolutionMax_gte"] = props.search_texture_resolution_min - query["textureResolutionMax_lte"] = props.search_texture_resolution_max - - build_query_common(query, props) - - return query - - -def build_query_scene(): - '''use all search input to request results from server''' - - props = bpy.context.window_manager.blenderkit_scene - query = { - "asset_type": 'scene', - # "engine": props.search_engine, - # "adult": props.search_adult, - } - build_query_common(query, props) - return query - - -def build_query_HDR(): - '''use all search input to request results from server''' - - props = bpy.context.window_manager.blenderkit_HDR - query = { - "asset_type": 'hdr', - - # "engine": props.search_engine, - # "adult": props.search_adult, - } - if props.true_hdr: - query["trueHDR"] = props.true_hdr - build_query_common(query, props) - return query - - -def build_query_material(): - props = bpy.context.window_manager.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 - if props.search_procedural == 'TEXTURE_BASED': - # todo this procedural hack should be replaced with the parameter - query["textureResolutionMax_gte"] = 0 - # query["procedural"] = False - if props.search_texture_resolution: - query["textureResolutionMax_gte"] = props.search_texture_resolution_min - query["textureResolutionMax_lte"] = props.search_texture_resolution_max - - - - elif props.search_procedural == "PROCEDURAL": - # todo this procedural hack should be replaced with the parameter - query["files_size_lte"] = 1024 * 1024 - # query["procedural"] = True - - 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.window_manager.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', - - "mode": 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() - utils.p(text, alltime, since_last) - - -def add_search_process(query, params): - global search_threads, thumb_workers_sml, thumb_workers_full, all_thumbs_loaded - - while (len(search_threads) > 0): - old_thread = search_threads.pop(0) - old_thread[0].stop() - # TODO CARE HERE FOR ALSO KILLING THE Thumbnail THREADS.? - # AT LEAST NOW SEARCH DONE FIRST WON'T REWRITE AN NEWER ONE - tempdir = paths.get_temp_dir('%s_search' % query['asset_type']) - headers = utils.get_headers(params['api_key']) - - if params.get('get_next'): - urlquery = params['next'] - else: - urlquery = query_to_url(query, params) - - if thumb_workers_sml == []: - for a in range(0, 8): - thread = threading.Thread(target=thumb_download_worker, - args=(thumb_sml_download_threads, thumb_full_download_threads), - daemon=True) - thread.start() - thumb_workers_sml.append(thread) - - all_thumbs_loaded = False - - thread = Searcher(query, params, tempdir=tempdir, headers=headers, urlquery=urlquery) - thread.start() - - search_threads.append([thread, tempdir, query['asset_type'], {}]) # 4th field is for results - - mt('search thread started') - - -def get_search_simple(parameters, filepath=None, page_size=100, max_results=100000000, api_key=''): - ''' - Searches and returns the - - - Parameters - ---------- - parameters - dict of blenderkit elastic parameters - filepath - a file to save the results. If None, results are returned - page_size - page size for retrieved results - max_results - max results of the search - api_key - BlenderKit api key - - Returns - ------- - Returns search results as a list, and optionally saves to filepath - - ''' - headers = utils.get_headers(api_key) - url = paths.get_api_url() + 'search/' - requeststring = url + '?query=' - for p in parameters.keys(): - requeststring += f'+{p}:{parameters[p]}' - - requeststring += '&page_size=' + str(page_size) - requeststring += '&dict_parameters=1' - - bk_logger.debug(requeststring) - response = rerequests.get(requeststring, headers=headers) # , params = rparameters) - # print(response.json()) - search_results = response.json() - - results = [] - results.extend(search_results['results']) - page_index = 2 - page_count = math.ceil(search_results['count'] / page_size) - while search_results.get('next') and len(results) < max_results: - bk_logger.info(f'getting page {page_index} , total pages {page_count}') - response = rerequests.get(search_results['next'], headers=headers) # , params = rparameters) - search_results = response.json() - # print(search_results) - results.extend(search_results['results']) - page_index += 1 - - if not filepath: - return results - - with open(filepath, 'w', encoding='utf-8') as s: - json.dump(results, s, ensure_ascii=False, indent=4) - bk_logger.info(f'retrieved {len(results)} assets from elastic search') - return results - - -def get_single_asset(asset_base_id): - preferences = bpy.context.preferences.addons['blenderkit'].preferences - params = { - 'asset_base_id': asset_base_id - } - results = get_search_simple(params, api_key=preferences.api_key) - if len(results) > 0: - return results[0] - return None - - -def search(category='', get_next=False, author_id=''): - ''' initialize searching''' - global search_start_time - # print(category,get_next,author_id) - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - search_start_time = time.time() - # mt('start') - scene = bpy.context.scene - wm = bpy.context.window_manager - ui_props = bpy.context.window_manager.blenderkitUI - - props = utils.get_search_props() - if ui_props.asset_type == 'MODEL': - if not hasattr(wm, 'blenderkit_models'): - return; - query = build_query_model() - - if ui_props.asset_type == 'SCENE': - if not hasattr(wm, 'blenderkit_scene'): - return; - query = build_query_scene() - - if ui_props.asset_type == 'HDR': - if not hasattr(wm, 'blenderkit_HDR'): - return; - query = build_query_HDR() - - if ui_props.asset_type == 'MATERIAL': - if not hasattr(wm, 'blenderkit_mat'): - return; - - query = build_query_material() - - if ui_props.asset_type == 'TEXTURE': - if not hasattr(wm, 'blenderkit_tex'): - return; - # props = scene.blenderkit_tex - # query = build_query_texture() - - if ui_props.asset_type == 'BRUSH': - if not hasattr(wm, 'blenderkit_brush'): - return; - query = build_query_brush() - - # crop long searches - if query.get('query'): - if len(query['query']) > 50: - query['query'] = strip_accents(query['query']) - - if len(query['query']) > 150: - idx = query['query'].find(' ', 142) - query['query'] = query['query'][:idx] - - # it's possible get_next was requested more than once. - # print(category,props.is_searching, get_next) - # print(query) - if props.is_searching and get_next == True: - # print('return because of get next and searching is happening') - return; - - if category != '': - if utils.profile_is_validator() and user_preferences.categories_fix: - query['category'] = category - else: - query['category_subtree'] = category - - if author_id != '': - query['author_id'] = author_id - - elif props.own_only: - # if user searches for [another] author, 'only my assets' is invalid. that's why in elif. - profile = bpy.context.window_manager.get('bkit profile') - if profile is not None: - query['author_id'] = str(profile['user']['id']) - - # utils.p('searching') - props.is_searching = True - - page_size = min(30, ui_props.wcount * user_preferences.max_assetbar_rows) - - params = { - 'scene_uuid': bpy.context.scene.get('uuid', None), - 'addon_version': version_checker.get_addon_version(), - 'api_key': user_preferences.api_key, - 'get_next': get_next, - 'free_first': props.free_only, - 'page_size': page_size, - } - - orig_results = bpy.context.window_manager.get(f'bkit {ui_props.asset_type.lower()} search orig') - if orig_results is not None and get_next: - params['next'] = orig_results['next'] - add_search_process(query, params) - tasks_queue.add_task((reports.add_report, ('BlenderKit searching....', 2))) - - props.report = 'BlenderKit searching....' - - -def update_filters(): - sprops = utils.get_search_props() - ui_props = bpy.context.window_manager.blenderkitUI - fcommon = sprops.own_only or \ - sprops.search_texture_resolution or \ - sprops.search_file_size or \ - sprops.search_procedural != 'BOTH' or \ - sprops.free_only or \ - sprops.quality_limit > 0 - - if ui_props.asset_type == 'MODEL': - sprops.use_filters = fcommon or \ - sprops.search_style != 'ANY' or \ - sprops.search_condition != 'UNSPECIFIED' or \ - sprops.search_design_year or \ - sprops.search_polycount - elif ui_props.asset_type == 'MATERIAL': - sprops.use_filters = fcommon - elif ui_props.asset_type == 'HDR': - sprops.use_filters = sprops.true_hdr - - -def search_update(self, context): - utils.p('search updater') - # if self.search_keywords != '': - update_filters() - ui_props = bpy.context.window_manager.blenderkitUI - if ui_props.down_up != 'SEARCH': - ui_props.down_up = 'SEARCH' - - # here we tweak the input if it comes form the clipboard. we need to get rid of asset type and set it in UI - sprops = utils.get_search_props() - instr = 'asset_base_id:' - atstr = 'asset_type:' - kwds = sprops.search_keywords - idi = kwds.find(instr) - ati = kwds.find(atstr) - # if the asset type already isn't there it means this update function - # was triggered by it's last iteration and needs to cancel - if ati > -1: - at = kwds[ati:].lower() - # uncertain length of the remaining string - find as better method to check the presence of asset type - if at.find('model') > -1: - ui_props.asset_type = 'MODEL' - elif at.find('material') > -1: - ui_props.asset_type = 'MATERIAL' - elif at.find('brush') > -1: - ui_props.asset_type = 'BRUSH' - elif at.find('scene') > -1: - ui_props.asset_type = 'SCENE' - elif at.find('hdr') > -1: - ui_props.asset_type = 'HDR' - # now we trim the input copypaste by anything extra that is there, - # this is also a way for this function to recognize that it already has parsed the clipboard - # the search props can have changed and this needs to transfer the data to the other field - # this complex behaviour is here for the case where the user needs to paste manually into blender? - sprops = utils.get_search_props() - sprops.search_keywords = kwds[:ati].rstrip() - # return here since writing into search keywords triggers this update function once more. - return - - # print('search update search') - search() - - -# accented_string is of type 'unicode' -def strip_accents(s): - return ''.join(c for c in unicodedata.normalize('NFD', s) - if unicodedata.category(c) != 'Mn') - - -class SearchOperator(Operator): - """Tooltip""" - bl_idname = "view3d.blenderkit_search" - bl_label = "BlenderKit asset search" - bl_description = "Search online for assets" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - esc: BoolProperty(name="Escape window", - description="Escape window right after start", - default=False, - options={'SKIP_SAVE'} - ) - - own: BoolProperty(name="own assets only", - description="Find all own assets", - default=False, - options={'SKIP_SAVE'}) - - category: StringProperty( - name="category", - description="search only subtree of this category", - default="", - options={'SKIP_SAVE'} - ) - - author_id: StringProperty( - name="Author ID", - description="Author ID - search only assets by this author", - default="", - options={'SKIP_SAVE'} - ) - - get_next: BoolProperty(name="next page", - description="get next page from previous search", - default=False, - options={'SKIP_SAVE'} - ) - - keywords: StringProperty( - name="Keywords", - description="Keywords", - default="", - options={'SKIP_SAVE'} - ) - - # close_window: BoolProperty(name='Close window', - # description='Try to close the window below mouse before download', - # default=False) - - tooltip: bpy.props.StringProperty(default='Runs search and displays the asset bar at the same time') - - @classmethod - def description(cls, context, properties): - return properties.tooltip - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - # TODO ; this should all get transferred to properties of the search operator, so sprops don't have to be fetched here at all. - if self.esc: - bpy.ops.view3d.close_popup_button('INVOKE_DEFAULT') - sprops = utils.get_search_props() - if self.author_id != '': - sprops.search_keywords = '' - if self.keywords != '': - sprops.search_keywords = self.keywords - - search(category=self.category, get_next=self.get_next, author_id=self.author_id) - # bpy.ops.view3d.blenderkit_asset_bar_widget() - - return {'FINISHED'} - - # def invoke(self, context, event): - # if self.close_window: - # context.window.cursor_warp(event.mouse_x, event.mouse_y - 100); - # context.area.tag_redraw() - # - # context.window.cursor_warp(event.mouse_x, event.mouse_y); - # return self. execute(context) - - -class UrlOperator(Operator): - """""" - bl_idname = "wm.blenderkit_url" - bl_label = "" - bl_description = "Search online for assets" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - tooltip: bpy.props.StringProperty(default='Open a web page') - url: bpy.props.StringProperty(default='Runs search and displays the asset bar at the same time') - - @classmethod - def description(cls, context, properties): - return properties.tooltip - - def execute(self, context): - bpy.ops.wm.url_open(url=self.url) - return {'FINISHED'} - - -class TooltipLabelOperator(Operator): - """""" - bl_idname = "wm.blenderkit_tooltip" - bl_label = "" - bl_description = "Empty operator to be able to create tooltips on labels in UI" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - tooltip: bpy.props.StringProperty(default='Open a web page') - - @classmethod - def description(cls, context, properties): - return properties.tooltip - - def execute(self, context): - return {'FINISHED'} - - -classes = [ - SearchOperator, - UrlOperator, - TooltipLabelOperator -] - - -def register_search(): - bpy.app.handlers.load_post.append(scene_load) - - for c in classes: - bpy.utils.register_class(c) - - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.use_timers and not bpy.app.background: - bpy.app.timers.register(search_timer) - - categories.load_categories() - - -def unregister_search(): - bpy.app.handlers.load_post.remove(scene_load) - - for c in classes: - bpy.utils.unregister_class(c) - - if bpy.app.timers.is_registered(search_timer): - bpy.app.timers.unregister(search_timer) diff --git a/blenderkit/tasks_queue.py b/blenderkit/tasks_queue.py deleted file mode 100644 index 248e9be7..00000000 --- a/blenderkit/tasks_queue.py +++ /dev/null @@ -1,125 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import utils - -import bpy -from bpy.app.handlers import persistent - -import queue -import logging -bk_logger = logging.getLogger('blenderkit') - -@persistent -def scene_load(context): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.use_timers and not bpy.app.background: - if not (bpy.app.timers.is_registered(queue_worker)): - bpy.app.timers.register(queue_worker) - - -def get_queue(): - # we pick just a random one of blender types, to try to get a persistent queue - t = bpy.types.Scene - - if not hasattr(t, 'task_queue'): - t.task_queue = queue.Queue() - return t.task_queue - -class task_object: - def __init__(self, command = '', arguments = (), wait = 0, only_last = False, fake_context = False, fake_context_area = 'VIEW_3D'): - self.command = command - self.arguments = arguments - self.wait = wait - self.only_last = only_last - self.fake_context = fake_context - self.fake_context_area = fake_context_area - -def add_task(task, wait = 0, only_last = False, fake_context = False, fake_context_area = 'VIEW_3D'): - q = get_queue() - taskob = task_object(task[0],task[1], wait = wait, only_last = only_last, fake_context = fake_context, fake_context_area = fake_context_area) - q.put(taskob) - - -def queue_worker(): - # utils.p('start queue worker timer') - - #bk_logger.debug('timer queue worker') - time_step = 2.0 - q = get_queue() - - back_to_queue = [] #delayed events - stashed = {} - # first round we get all tasks that are supposed to be stashed and run only once (only_last option) - # stashing finds tasks with the property only_last and same command and executes only the last one. - while not q.empty(): - # print('queue while 1') - - task = q.get() - if task.only_last: - #this now makes the keys not only by task, but also first argument. - # by now stashing is only used for ratings, where the first argument is url. - # This enables fast rating of multiple assets while allowing larger delay for uploading of ratings. - # this avoids a duplicate request error on the server - stashed[str(task.command)+str(task.arguments[0])] = task - else: - back_to_queue.append(task) - if len(stashed.keys())>1: - bk_logger.debug('task queue stashed task:' +str(stashed)) - #return tasks to que except for stashed - for task in back_to_queue: - q.put(task) - #return stashed tasks to queue - for k in stashed.keys(): - q.put(stashed[k]) - #second round, execute or put back waiting tasks. - back_to_queue = [] - while not q.empty(): - # print('window manager', bpy.context.window_manager) - task = q.get() - - if task.wait>0: - task.wait-=time_step - back_to_queue.append(task) - else: - bk_logger.debug('task queue task:'+ str( task.command) +str( task.arguments)) - try: - if task.fake_context: - fc = utils.get_fake_context(bpy.context, area_type = task.fake_context_area) - task.command(fc,*task.arguments) - else: - task.command(*task.arguments) - except Exception as e: - bk_logger.error('task queue failed task:'+ str(task.command)+str(task.arguments)+ str(e)) - # bk_logger.exception('Got exception on main handler') - # raise - # print('queue while 2') - for task in back_to_queue: - q.put(task) - # utils.p('end queue worker timer') - - return 2.0 - - -def register(): - bpy.app.handlers.load_post.append(scene_load) - - -def unregister(): - bpy.app.handlers.load_post.remove(scene_load) diff --git a/blenderkit/thumbnails/arrow_left.png b/blenderkit/thumbnails/arrow_left.png Binary files differdeleted file mode 100644 index 97565169..00000000 --- a/blenderkit/thumbnails/arrow_left.png +++ /dev/null diff --git a/blenderkit/thumbnails/arrow_right.png b/blenderkit/thumbnails/arrow_right.png Binary files differdeleted file mode 100644 index fd16550b..00000000 --- a/blenderkit/thumbnails/arrow_right.png +++ /dev/null diff --git a/blenderkit/thumbnails/bar_slider.png b/blenderkit/thumbnails/bar_slider.png Binary files differdeleted file mode 100644 index ec627318..00000000 --- a/blenderkit/thumbnails/bar_slider.png +++ /dev/null diff --git a/blenderkit/thumbnails/bell.png b/blenderkit/thumbnails/bell.png Binary files differdeleted file mode 100644 index 2b724a26..00000000 --- a/blenderkit/thumbnails/bell.png +++ /dev/null diff --git a/blenderkit/thumbnails/cc0.png b/blenderkit/thumbnails/cc0.png Binary files differdeleted file mode 100644 index 3f5450f8..00000000 --- a/blenderkit/thumbnails/cc0.png +++ /dev/null diff --git a/blenderkit/thumbnails/dumbbell.png b/blenderkit/thumbnails/dumbbell.png Binary files differdeleted file mode 100644 index ffc709c6..00000000 --- a/blenderkit/thumbnails/dumbbell.png +++ /dev/null diff --git a/blenderkit/thumbnails/filter.png b/blenderkit/thumbnails/filter.png Binary files differdeleted file mode 100644 index e128c35e..00000000 --- a/blenderkit/thumbnails/filter.png +++ /dev/null diff --git a/blenderkit/thumbnails/filter_active.png b/blenderkit/thumbnails/filter_active.png Binary files differdeleted file mode 100644 index de4fb3be..00000000 --- a/blenderkit/thumbnails/filter_active.png +++ /dev/null diff --git a/blenderkit/thumbnails/flp.png b/blenderkit/thumbnails/flp.png Binary files differdeleted file mode 100644 index 7ac3c3d7..00000000 --- a/blenderkit/thumbnails/flp.png +++ /dev/null diff --git a/blenderkit/thumbnails/fp.png b/blenderkit/thumbnails/fp.png Binary files differdeleted file mode 100644 index 4e356ab1..00000000 --- a/blenderkit/thumbnails/fp.png +++ /dev/null diff --git a/blenderkit/thumbnails/intro.jpg b/blenderkit/thumbnails/intro.jpg Binary files differdeleted file mode 100644 index 52ce56e3..00000000 --- a/blenderkit/thumbnails/intro.jpg +++ /dev/null diff --git a/blenderkit/thumbnails/locked.png b/blenderkit/thumbnails/locked.png Binary files differdeleted file mode 100644 index f308392c..00000000 --- a/blenderkit/thumbnails/locked.png +++ /dev/null diff --git a/blenderkit/thumbnails/private.png b/blenderkit/thumbnails/private.png Binary files differdeleted file mode 100644 index 7ac3c3d7..00000000 --- a/blenderkit/thumbnails/private.png +++ /dev/null diff --git a/blenderkit/thumbnails/royalty_free.png b/blenderkit/thumbnails/royalty_free.png Binary files differdeleted file mode 100644 index b85fa910..00000000 --- a/blenderkit/thumbnails/royalty_free.png +++ /dev/null diff --git a/blenderkit/thumbnails/star_grey.png b/blenderkit/thumbnails/star_grey.png Binary files differdeleted file mode 100644 index 53c5bd05..00000000 --- a/blenderkit/thumbnails/star_grey.png +++ /dev/null diff --git a/blenderkit/thumbnails/star_white.png b/blenderkit/thumbnails/star_white.png Binary files differdeleted file mode 100644 index 14e030cc..00000000 --- a/blenderkit/thumbnails/star_white.png +++ /dev/null diff --git a/blenderkit/thumbnails/thumbnail_not_available.jpg b/blenderkit/thumbnails/thumbnail_not_available.jpg Binary files differdeleted file mode 100644 index c5b5172a..00000000 --- a/blenderkit/thumbnails/thumbnail_not_available.jpg +++ /dev/null diff --git a/blenderkit/thumbnails/thumbnail_notready.jpg b/blenderkit/thumbnails/thumbnail_notready.jpg Binary files differdeleted file mode 100644 index 5be976b9..00000000 --- a/blenderkit/thumbnails/thumbnail_notready.jpg +++ /dev/null diff --git a/blenderkit/thumbnails/trophy.png b/blenderkit/thumbnails/trophy.png Binary files differdeleted file mode 100644 index 01d77a22..00000000 --- a/blenderkit/thumbnails/trophy.png +++ /dev/null diff --git a/blenderkit/thumbnails/vs_deleted.png b/blenderkit/thumbnails/vs_deleted.png Binary files differdeleted file mode 100644 index a7f4e134..00000000 --- a/blenderkit/thumbnails/vs_deleted.png +++ /dev/null diff --git a/blenderkit/thumbnails/vs_on_hold.png b/blenderkit/thumbnails/vs_on_hold.png Binary files differdeleted file mode 100644 index eb797517..00000000 --- a/blenderkit/thumbnails/vs_on_hold.png +++ /dev/null diff --git a/blenderkit/thumbnails/vs_ready.png b/blenderkit/thumbnails/vs_ready.png Binary files differdeleted file mode 100644 index ac52a3cd..00000000 --- a/blenderkit/thumbnails/vs_ready.png +++ /dev/null diff --git a/blenderkit/thumbnails/vs_rejected.png b/blenderkit/thumbnails/vs_rejected.png Binary files differdeleted file mode 100644 index 6ff663cf..00000000 --- a/blenderkit/thumbnails/vs_rejected.png +++ /dev/null diff --git a/blenderkit/thumbnails/vs_uploaded.png b/blenderkit/thumbnails/vs_uploaded.png Binary files differdeleted file mode 100644 index 6ef39cb4..00000000 --- a/blenderkit/thumbnails/vs_uploaded.png +++ /dev/null diff --git a/blenderkit/thumbnails/vs_uploading.png b/blenderkit/thumbnails/vs_uploading.png Binary files differdeleted file mode 100644 index e7276e4d..00000000 --- a/blenderkit/thumbnails/vs_uploading.png +++ /dev/null diff --git a/blenderkit/thumbnails/vs_validated.png b/blenderkit/thumbnails/vs_validated.png Binary files differdeleted file mode 100644 index b2d8fdd2..00000000 --- a/blenderkit/thumbnails/vs_validated.png +++ /dev/null diff --git a/blenderkit/ui.py b/blenderkit/ui.py deleted file mode 100644 index 41c4feee..00000000 --- a/blenderkit/ui.py +++ /dev/null @@ -1,1934 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import paths, ratings, utils, search, upload, ui_bgl, download, bg_blender, colors, tasks_queue, \ - ui_panels, icons, ratings_utils, reports - -import bpy - -import math, random - -from bpy.props import ( - BoolProperty, - StringProperty, - IntProperty, - FloatVectorProperty -) - -from bpy_extras import view3d_utils -import mathutils -from mathutils import Vector -import time -import datetime -import os - -import logging - -draw_time = 0 -eval_time = 0 - -bk_logger = logging.getLogger('blenderkit') - -handler_2d = None -handler_3d = None -active_area_pointer = None -active_window_pointer = None -active_region_pointer = None - -mappingdict = { - 'MODEL': 'model', - 'SCENE': 'scene', - 'HDR': 'hdr', - 'MATERIAL': 'material', - 'TEXTURE': 'texture', - 'BRUSH': 'brush' -} - -verification_icons = { - 'ready': 'vs_ready.png', - 'deleted': 'vs_deleted.png', - 'uploaded': 'vs_uploaded.png', - 'uploading': 'vs_uploading.png', - 'on_hold': 'vs_on_hold.png', - 'validated': None, - 'rejected': 'vs_rejected.png' - -} - - -# 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 - wm = bpy.context.window_manager - ui_props = bpy.context.window_manager.blenderkitUI - r = bpy.context.region - - search_results = wm.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.scroll_offset) - 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.scroll_offset - - # 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.window_manager.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.window_manager.blenderkitUI - - rating_possible, rated, asset, asset_data = is_rating_possible() - if rating_possible: # (not rated or ui_props.rating_menu_on): - # print('rating is pssible', asset_data['name']) - bkit_ratings = asset.bkit_ratings - - if ui.rating_button_on: - # print('should draw button') - 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['assetType']) - 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) - return - - -def draw_text_block(x=0, y=0, width=40, font_size=10, line_height=15, text='', color=colors.TEXT): - lines = text.split('\n') - nlines = [] - for l in lines: - nlines.extend(search.split_subs(l, )) - - column_lines = 0 - for l in nlines: - ytext = y - column_lines * line_height - column_lines += 1 - ui_bgl.draw_text(l, x, ytext, font_size, color) - - -def draw_tooltip(x, y, name='', author='', quality='-', img=None, gravatar=None): - region = bpy.context.region - scale = bpy.context.preferences.view.ui_scale - t = time.time() - - if not img or max(img.size[0], img.size[1]) == 0: - return; - - x += 20 - y -= 20 - # first get image size scaled - isizex = int(512 * scale * img.size[0] / min(img.size[0], img.size[1])) - isizey = int(512 * scale * img.size[1] / min(img.size[0], img.size[1])) - - ttipmargin = 5 * scale - # then do recurrent re-scaling, to know where to fit the tooltip - estimated_height = 2 * ttipmargin + isizey - if estimated_height > y: - scaledown = y / (estimated_height) - scale *= scaledown - - isizex = int(512 * scale * img.size[0] / min(img.size[0], img.size[1])) - isizey = int(512 * scale * img.size[1] / min(img.size[0], img.size[1])) - - ttipmargin = 5 * scale - textmargin = 12 * scale - - if gravatar is not None: - overlay_height_base = 90 - else: - overlay_height_base = 70 - - overlay_height = overlay_height_base * scale - name_height = int(20 * scale) - - width = isizex + 2 * ttipmargin - - properties_width = 0 - for r in bpy.context.area.regions: - if r.type == 'UI': - properties_width = r.width - - # limit to area borders - x = min(x + width, region.width - properties_width) - width - - # define_colors - background_color = bpy.context.preferences.themes[0].user_interface.wcol_tooltip.inner - background_overlay = (background_color[0], background_color[1], background_color[2], .8) - textcol = bpy.context.preferences.themes[0].user_interface.wcol_tooltip.text - textcol = (textcol[0], textcol[1], textcol[2], 1) - - # background - ui_bgl.draw_rect(x - ttipmargin, - y - 2 * ttipmargin - isizey, - isizex + ttipmargin * 2, - 2 * ttipmargin + isizey, - background_color) - - # main preview image - ui_bgl.draw_image(x, y - isizey - ttipmargin, isizex, isizey, img, 1) - - # text overlay background - ui_bgl.draw_rect(x - ttipmargin, - y - 2 * ttipmargin - isizey, - isizex + ttipmargin * 2, - ttipmargin + overlay_height, - background_overlay) - - # draw name - name_x = x + textmargin - name_y = y - isizey + overlay_height - textmargin - name_height - ui_bgl.draw_text(name, name_x, name_y, name_height, textcol) - - # draw gravatar - author_x_text = x + isizex - textmargin - gravatar_size = overlay_height - 2 * textmargin - gravatar_y = y - isizey - ttipmargin + textmargin - if gravatar is not None: - author_x_text -= gravatar_size + textmargin - ui_bgl.draw_image(x + isizex - gravatar_size - textmargin, - gravatar_y, # + textmargin, - gravatar_size, gravatar_size, gravatar, 1) - - # draw author's name - author_text_size = int(name_height * .7) - ui_bgl.draw_text(author, author_x_text, gravatar_y, author_text_size, textcol, halign='RIGHT') - - # draw quality - quality_text_size = int(name_height * 1) - img = utils.get_thumbnail('star_grey.png') - ui_bgl.draw_image(name_x, gravatar_y, quality_text_size, quality_text_size, img, .6) - ui_bgl.draw_text(str(quality), name_x + quality_text_size + 5, gravatar_y, quality_text_size, textcol) - - -def draw_tooltip_with_author(asset_data, x, y): - # TODO move this lazy loading into a function and don't duplicate through the code - - img = get_large_thumbnail_image(asset_data) - gimg = None - tooltip_data = asset_data.get('tooltip_data') - if tooltip_data is None: - author_text = '' - - if bpy.context.window_manager.get('bkit authors') is not None: - a = bpy.context.window_manager['bkit authors'].get(asset_data['author']['id']) - if a is not None and a != '': - if a.get('gravatarImg') is not None: - gimg = utils.get_hidden_image(a['gravatarImg'], a['gravatarHash']).name - - if len(a['firstName']) > 0 or len(a['lastName']) > 0: - author_text = f"by {a['firstName']} {a['lastName']}" - - aname = asset_data['displayName'] - aname = aname[0].upper() + aname[1:] - if len(aname) > 36: - aname = f"{aname[:33]}..." - - rc = asset_data.get('ratingsCount') - show_rating_threshold = 0 - rcount = 0 - quality = '-' - if rc: - rcount = min(rc.get('quality', 0), rc.get('workingHours', 0)) - if rcount > show_rating_threshold: - quality = round(asset_data['ratingsAverage'].get('quality')) - tooltip_data = { - 'aname': aname, - 'author_text': author_text, - 'quality': quality, - 'gimg': gimg - } - asset_data['tooltip_data'] = tooltip_data - gimg = tooltip_data['gimg'] - if gimg is not None: - gimg = bpy.data.images[gimg] - - draw_tooltip(x, y, name=tooltip_data['aname'], - author=tooltip_data['author_text'], - quality=tooltip_data['quality'], - img=img, - gravatar=gimg) - - -def draw_callback_2d(self, context): - if not utils.guard_from_crash(): - return - - a = context.area - w = context.window - try: - # self.area might throw error just by itself. - a1 = self.area - w1 = self.window - go = True - if len(a.spaces[0].region_quadviews) > 0: - # print(dir(bpy.context.region_data)) - # print('quad', a.spaces[0].region_3d, a.spaces[0].region_quadviews[0]) - if a.spaces[0].region_3d != context.region_data: - go = False - 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 and w == w1: - - props = context.window_manager.blenderkitUI - if props.down_up == 'SEARCH': - draw_ratings_bgl() - draw_asset_bar(self, context) - elif props.down_up == 'UPLOAD': - draw_callback_2d_upload_preview(self, context) - - -def draw_downloader(x, y, percent=0, img=None, text=''): - 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)) - # if asset_data is not None: - # ui_bgl.draw_text(asset_data['name'], x, y, colors.TEXT) - # ui_bgl.draw_text(asset_data['filesSize']) - if text: - ui_bgl.draw_text(text, x, y - 15, 12, colors.TEXT) - - -def draw_progress(x, y, text='', percent=None, color=colors.GREEN): - ui_bgl.draw_rect(x, y, percent, 5, color) - ui_bgl.draw_text(text, x, y + 8, 16, color) - - -def draw_callback_3d_progress(self, context): - # 'star trek' mode is here - - if not utils.guard_from_crash(): - return - 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['assetType'] == '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): - if not utils.guard_from_crash(): - return - - green = (.2, 1, .2, .3) - offset = 0 - row_height = 35 - - ui = bpy.context.window_manager.blenderkitUI - - x = ui.reports_x - y = ui.reports_y - index = 0 - for threaddata in download.download_threads: - asset_data = threaddata[1] - tcom = threaddata[2] - - directory = paths.get_temp_dir('%s_search' % asset_data['assetType']) - tpath = os.path.join(directory, asset_data['thumbnail_small']) - img = utils.get_hidden_image(tpath, asset_data['id']) - - if tcom.passargs.get('downloaders'): - for d in tcom.passargs['downloaders']: - - loc = view3d_utils.location_3d_to_region_2d(bpy.context.region, bpy.context.space_data.region_3d, - d['location']) - # print('drawing downloader') - if loc is not None: - if asset_data['assetType'] == '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, text=tcom.report) - else: - draw_downloader(loc[0], loc[1], percent=tcom.progress, img=img, text=tcom.report) - # utils.p('end drawing downlaoders downloader') - 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] - n = '' - if tcom.name is not None: - n = tcom.name + ': ' - draw_progress(x, y - index * 30, '%s' % n + tcom.lasttext, - tcom.progress) - index += 1 - for report in reports.reports: - # print('drawing reports', x, y, report.text) - report.draw(x, y - index * 30) - index += 1 - report.fade() - - -def draw_callback_2d_upload_preview(self, context): - ui_props = context.window_manager.blenderkitUI - - props = utils.get_upload_props() - - # assets which don't need asset preview - if ui_props.asset_type == 'HDR': - return - - 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, name=props.name, img=img) - - - -def get_large_thumbnail_image(asset_data): - '''Get thumbnail image from asset data''' - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - iname = utils.previmg_name(ui_props.active_index, fullsize=True) - directory = paths.get_temp_dir('%s_search' % mappingdict[ui_props.asset_type]) - tpath = os.path.join(directory, asset_data['thumbnail']) - # if asset_data['assetType'] == 'hdr': - # tpath = os.path.join(directory, asset_data['thumbnail']) - if not asset_data['thumbnail']: - tpath = paths.get_addon_thumbnail_path('thumbnail_not_available.jpg') - - if asset_data['assetType'] == 'hdr': - colorspace = 'Non-Color' - else: - colorspace = 'sRGB' - img = utils.get_hidden_image(tpath, iname, colorspace=colorspace) - return img - - -def draw_asset_bar(self, context): - s = bpy.context.scene - ui_props = context.window_manager.blenderkitUI - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - is_validator = utils.profile_is_validator() - 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 ui_props.hcount>0: - # #this fixes a draw issue introduced in blender 2.91. draws a very small version of the image to avoid problems - # # with alpha. Not sure why this works. - # img = utils.get_thumbnail('arrow_left.png') - # ui_bgl.draw_image(0, 0, 1, - # 1, - # img, - # 1) - if ui_props.hcount > 0 and ui_props.wcount > 0: - search_results = bpy.context.window_manager.get('search results') - search_results_orig = bpy.context.window_manager.get('search results orig') - 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.scroll_offset > 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.scroll_offset > 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 search_results_orig['count'] - ui_props.scroll_offset > (ui_props.wcount * ui_props.hcount) + 1: - 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) - ar = context.window_manager.get('asset ratings') - for b in range(0, h_draw): - w_draw = min(ui_props.wcount, len(search_results) - b * ui_props.wcount - ui_props.scroll_offset) - - 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.scroll_offset + b * ui_props.wcount - iname = utils.previmg_name(index) - img = bpy.data.images.get(iname) - if img is not None and img.size[0] > 0 and img.size[1] > 0: - 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) - - 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, ui_props.thumb_size, ui_props.thumb_size, grey2) - ui_bgl.draw_text('loading', x + ui_props.thumb_size // 2, y + ui_props.thumb_size // 2, - ui_props.thumb_size // 6, white, halign='CENTER', valign='CENTER') - - result = search_results[index] - # code to inform validators that the validation is waiting too long and should be done asap - if result['verificationStatus'] == 'uploaded': - if is_validator: - over_limit = utils.is_upload_old(result) - if over_limit: - redness = min(over_limit * .05, 0.5) - red = (1, 0, 0, redness) - ui_bgl.draw_rect(x, y, ui_props.thumb_size, ui_props.thumb_size, red) - - if result['downloaded'] > 0: - ui_bgl.draw_rect(x, y, int(ui_props.thumb_size * 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 (result.get('canDownload', True)) == 0: - img = utils.get_thumbnail('locked.png') - ui_bgl.draw_image(x + 2, y + 2, 24, 24, img, 1) - - # pcoll = icons.icon_collections["main"] - # v_icon = pcoll['rejected'] - v_icon = verification_icons[result.get('verificationStatus', 'validated')] - - if v_icon is None and is_validator: - # poke for validators to rate - rating = ar.get(result['id']) - if rating is not None: - rating = rating.to_dict() - if rating in (None, {}): - v_icon = 'star_grey.png' - - 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 = 'Register on BlenderKit website to upload your own assets.' - # 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) - if ui_props.draw_tooltip and len(search_results) > ui_props.active_index: - r = search_results[ui_props.active_index] - draw_tooltip_with_author(r, ui_props.mouse_x, ui_props.mouse_y) - 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) - - -def object_in_particle_collection(o): - '''checks if an object is in a particle system as instance, to not snap to it and not to try to attach material.''' - for p in bpy.data.particles: - if p.render_type == 'COLLECTION': - if p.instance_collection: - for o1 in p.instance_collection.objects: - if o1 == o: - return True - if p.render_type == 'COLLECTION': - if p.instance_object == o: - return True - return False - - -def deep_ray_cast(depsgraph, ray_origin, vec): - # this allows to ignore some objects, like objects with bounding box draw style or particle objects - object = None - # while object is None or object.draw - has_hit, snapped_location, snapped_normal, face_index, object, matrix = bpy.context.scene.ray_cast( - depsgraph, ray_origin, vec) - empty_set = False, Vector((0, 0, 0)), Vector((0, 0, 1)), None, None, None - if not object: - return empty_set - try_object = object - while try_object and (try_object.display_type == 'BOUNDS' or object_in_particle_collection(try_object)): - ray_origin = snapped_location + vec.normalized() * 0.0003 - try_has_hit, try_snapped_location, try_snapped_normal, try_face_index, try_object, try_matrix = bpy.context.scene.ray_cast( - depsgraph, ray_origin, vec) - if try_has_hit: - # this way only good hits are returned, otherwise - has_hit, snapped_location, snapped_normal, face_index, object, matrix = try_has_hit, try_snapped_location, try_snapped_normal, try_face_index, try_object, try_matrix - if not (object.display_type == 'BOUNDS' or object_in_particle_collection( - try_object)): # or not object.visible_get()): - return has_hit, snapped_location, snapped_normal, face_index, object, matrix - return empty_set - - -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) - if rv3d.view_perspective == 'CAMERA' and rv3d.is_perspective == False: - # ortographic cameras don'w work with region_2d_to_origin_3d - view_position = rv3d.view_matrix.inverted().translation - ray_origin = view3d_utils.region_2d_to_location_3d(r, rv3d, coord, depth_location=view_position) - else: - ray_origin = view3d_utils.region_2d_to_origin_3d(r, rv3d, coord, clamp=1.0) - - ray_target = ray_origin + (view_vector * 1000000000) - - vec = ray_target - ray_origin - - has_hit, snapped_location, snapped_normal, face_index, object, matrix = deep_ray_cast( - bpy.context.view_layer.depsgraph, ray_origin, vec) - - # backface snapping inversion - if view_vector.angle(snapped_normal) < math.pi / 2: - snapped_normal = -snapped_normal - # print(has_hit, snapped_location, snapped_normal, face_index, object, matrix) - # rote = mathutils.Euler((0, 0, math.pi)) - randoffset = math.pi - if has_hit: - props = bpy.context.window_manager.blenderkit_models - up = Vector((0, 0, 1)) - - if props.perpendicular_snap: - if snapped_normal.z > 1 - props.perpendicular_snap_threshold: - snapped_normal = Vector((0, 0, 1)) - elif snapped_normal.z < -1 + props.perpendicular_snap_threshold: - snapped_normal = Vector((0, 0, -1)) - elif abs(snapped_normal.z) < props.perpendicular_snap_threshold: - snapped_normal.z = 0 - snapped_normal.normalize() - - snapped_rotation = snapped_normal.to_track_quat('Z', 'Y').to_euler() - - 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.window_manager.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.window_manager.blenderkitUI - preferences = bpy.context.preferences.addons['blenderkit'].preferences - # first test if user is logged in. - if preferences.api_key == '': - return False, False, None, None - 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['assetBaseId']) - return True, rated, b, ad - if ao is not None: - ad = None - # crawl parents to reach active asset. there could have been parenting so we need to find the first onw - ao_check = ao - while ad is None or (ad is None and ao_check.parent is not None): - s = bpy.context.scene - ad = ao_check.get('asset_data') - if ad is not None and ad.get('assetBaseId') is not None: - - s['assets rated'] = s.get('assets rated', {}) - rated = s['assets rated'].get(ad['assetBaseId']) - # originally hidden for already rated assets - return True, rated, ao_check, ad - elif ao_check.parent is not None: - ao_check = ao_check.parent - else: - break - # check also materials - m = ao.active_material - if m is not None: - ad = m.get('asset_data') - - if ad is not None and ad.get('assetBaseId'): - rated = bpy.context.scene['assets rated'].get(ad['assetBaseId']) - 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.window_manager.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 bpy.context.mode in ('SCULPT', 'PAINT_TEXTURE'): - accept_value = 'PRESS' - else: - accept_value = 'RELEASE' - - if ui.rating_button_on and event.type == 'LEFTMOUSE' and event.value == accept_value: - 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 - ctx = utils.get_fake_context(bpy.context, area_type='VIEW_3D') - bpy.ops.wm.blenderkit_menu_rating_upload(ctx, 'INVOKE_DEFAULT', asset_name=asset_data['name'], - asset_id=asset_data['id'], - asset_type=asset_data['assetType']) - return True - 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.window_manager.blenderkitUI - # search_results = bpy.context.window_manager.get('search results') - # if search_results == None: - # return False - # - # w_draw1 = min(ui_props.wcount + 1, len(search_results) - b * ui_props.wcount - ui_props.scroll_offset) - # end = ui_props.bar_x + (w_draw1) * ( - # ui_props.margin + ui_props.thumb_size) + ui_props.margin + ui_props.drawoffset + 25 - - 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): - if bpy.app.background or not area: - return - ui = bpy.context.window_manager.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 = user_preferences.thumb_size * 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.window_manager.get('search results') - if search_results != None and ui.wcount > 0: - 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 - 600 - ui.reports_x = ui.bar_x - else: - ui.reports_y = ui.bar_y - ui.bar_height - 100 - ui.reports_x = ui.bar_x - - ui.rating_x = ui.bar_x - ui.rating_y = ui.bar_y - ui.bar_height - - -class ParticlesDropDialog(bpy.types.Operator): - """Tooltip""" - bl_idname = "object.blenderkit_particles_drop" - bl_label = "BlenderKit particle plants object drop" - bl_options = {'REGISTER', 'INTERNAL'} - - asset_search_index: IntProperty(name="Asset index", - description="Index of the asset in asset bar", - default=0, - ) - - model_location: FloatVectorProperty(name="Location", - default=(0, 0, 0)) - - model_rotation: FloatVectorProperty(name="Rotation", - default=(0, 0, 0), - subtype='QUATERNION') - - target_object: StringProperty( - name="Target object", - description="The object to which the particles will get applied", - default="", options={'SKIP_SAVE'}) - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout - message = 'This asset is a particle setup. BlenderKit can apply particles to the active/drag-drop object.' \ - 'The number of particles is caluclated automatically, but if there are 2 many particles,' \ - ' BlenderKit can do the following steps to make sure Blender continues to run:' \ - '\n1.Switch to bounding box view of the particles.' \ - '\n2.Turn down number of particles that are shown in the view.' \ - '\n3.Hide the particle system completely from the 3D view.' \ - "as a result of this, it's possible you'll see the particle setup only in render view or " \ - "rendered images. You should still be careful and test particle systems on smaller objects first." - utils.label_multiline(layout, text=message, width=400) - - def execute(self, context): - bpy.ops.scene.blenderkit_download(True, - # asset_type=ui_props.asset_type, - asset_index=self.asset_search_index, - model_location=self.model_rotation, - model_rotation=self.model_rotation, - target_object=self.target_object) - return {'FINISHED'} - - def invoke(self, context, event): - wm = context.window_manager - return wm.invoke_props_dialog(self, width=400) - - -# class MaterialDropDialog(bpy.types.Operator): -# """Tooltip""" -# bl_idname = "object.blenderkit_material_drop" -# bl_label = "BlenderKit material drop on linked objects" -# bl_options = {'REGISTER', 'INTERNAL'} -# -# asset_search_index: IntProperty(name="Asset index", -# description="Index of the asset in asset bar", -# default=0, -# ) -# -# model_location: FloatVectorProperty(name="Location", -# default=(0, 0, 0)) -# -# model_rotation: FloatVectorProperty(name="Rotation", -# default=(0, 0, 0), -# subtype='QUATERNION') -# -# target_object: StringProperty( -# name="Target object", -# description="The object to which the particles will get applied", -# default="", options={'SKIP_SAVE'}) -# -# target_material_slot: IntProperty(name="Target material slot", -# description="Index of the material on the object to be changed", -# default=0, -# ) -# -# @classmethod -# def poll(cls, context): -# return True -# -# def draw(self, context): -# layout = self.layout -# message = "This asset is linked to the scene from an external file and cannot have material appended." \ -# " Do you want to bring it into Blender Scene?" -# utils.label_multiline(layout, text=message, width=400) -# -# def execute(self, context): -# for c in bpy.data.collections: -# for o in c.objects: -# if o.name != self.target_object: -# continue; -# for empty in bpy.context.visible_objects: -# if not(empty.instance_type == 'COLLECTION' and empty.instance_collection == c): -# continue; -# utils.activate(empty) -# break; -# bpy.ops.object.blenderkit_bring_to_scene() -# bpy.ops.scene.blenderkit_download(True, -# # asset_type=ui_props.asset_type, -# asset_index=self.asset_search_index, -# model_location=self.model_rotation, -# model_rotation=self.model_rotation, -# target_object=self.target_object, -# material_target_slot = self.target_slot) -# return {'FINISHED'} -# -# def invoke(self, context, event): -# wm = context.window_manager -# return wm.invoke_props_dialog(self, width=400) - -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', 'INTERNAL'} - - 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 first", description='', default=False, options={'SKIP_SAVE'}) - - category: StringProperty( - name="Category", - description="search only subtree of this category", - default="", options={'SKIP_SAVE'}) - - tooltip: bpy.props.StringProperty(default='runs search and displays the asset bar at the same time') - - @classmethod - def description(cls, context, properties): - return properties.tooltip - - def search_more(self): - sro = bpy.context.window_manager.get('search results orig') - if sro is None: - return; - if sro.get('next') is None: - return - search_props = utils.get_search_props() - if search_props.is_searching: - return - - search.search(get_next=True) - - def exit_modal(self): - try: - bpy.types.SpaceView3D.draw_handler_remove(self._handle_2d, 'WINDOW') - except: - pass; - ui_props = bpy.context.window_manager.blenderkitUI - - # 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.window_manager.blenderkitUI - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - - areas = [] - - # timers testing - seems timers might be causing crashes. testing it this way now. - if not user_preferences.use_timers: - search.search_timer() - download.download_timer() - tasks_queue.queue_worker() - bg_blender.bg_update() - - if bpy.context.scene != self.scene: - self.exit_modal() - return {'CANCELLED'} - - for w in context.window_manager.windows: - areas.extend(w.screen.areas) - - if self.area not in areas or self.area.type != 'VIEW_3D' or self.has_quad_views != ( - len(self.area.spaces[0].region_quadviews) > 0): - # print('search areas') bpy.context.area.spaces[0].region_quadviews - # 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() - 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() - return {'CANCELLED'} - - update_ui_size(self.area, self.region) - - # 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.turn_off = False - self.exit_modal() - ui_props.draw_tooltip = False - return {'CANCELLED'} - - if context.region != self.region: - # print(time.time(), 'pass through because of region') - # print(context.region.type, self.region.type) - return {'PASS_THROUGH'} - - if ui_props.down_up == 'UPLOAD': - - ui_props.mouse_x = 0 - ui_props.mouse_y = self.region.height - - 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': - 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' and utils.get_active_brush() is not None \ - or ui_props.asset_type == 'SCENE' or ui_props.asset_type == 'HDR': - export_data, upload_data = upload.get_upload_data(context=context, asset_type=ui_props.asset_type) - # if upload_data: - # # print(upload_data) - # ui_props.tooltip = upload_data['displayName'] # 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 = bpy.context.window_manager.get('search results') - search_results_orig = bpy.context.window_manager.get('search results orig') - # If there aren't any results, we need no interaction(yet) - if sr is None: - return {'PASS_THROUGH'} - if len(sr) - ui_props.scroll_offset < (ui_props.wcount * user_preferences.max_assetbar_rows) + 15: - 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 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.scroll_offset > ( - ui_props.wcount * ui_props.hcount): - if ui_props.hcount > 1: - ui_props.scroll_offset += ui_props.wcount - else: - ui_props.scroll_offset += 1 - if len(sr) - ui_props.scroll_offset < (ui_props.wcount * ui_props.hcount): - ui_props.scroll_offset = len(sr) - (ui_props.wcount * ui_props.hcount) - - if event.type == 'WHEELUPMOUSE' and ui_props.scroll_offset > 0: - if ui_props.hcount > 1: - ui_props.scroll_offset -= ui_props.wcount - else: - ui_props.scroll_offset -= 1 - if ui_props.scroll_offset < 0: - ui_props.scroll_offset = 0 - - 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 not mouse_in_asset_bar(mx, my): # - - ui_props.active_index = -3 - ui_props.draw_drag_image = False - ui_props.draw_snapped_bounds = False - ui_props.draw_tooltip = False - bpy.context.window.cursor_set("DEFAULT") - return {'PASS_THROUGH'} - - sr = bpy.context.window_manager['search results'] - - bpy.context.window.cursor_set("HAND") - - if sr != None and ui_props.wcount * ui_props.hcount > len(sr) and ui_props.scroll_offset > 0: - ui_props.scroll_offset = 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'] - # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_asset_menu') - - else: - ui_props.draw_tooltip = False - - if mx > ui_props.bar_x + ui_props.bar_width - 50 and search_results_orig[ - 'count'] - ui_props.scroll_offset > ( - ui_props.wcount * ui_props.hcount) + 1: - ui_props.active_index = -1 - return {'RUNNING_MODAL'} - if mx < ui_props.bar_x + 50 and ui_props.scroll_offset > 0: - ui_props.active_index = -2 - return {'RUNNING_MODAL'} - - return {'RUNNING_MODAL'} - - if event.type == 'RIGHTMOUSE': - mx = event.mouse_x - r.x - my = event.mouse_y - r.y - - if event.value == 'PRESS' and mouse_in_asset_bar(mx, my) and ui_props.active_index > -1: - # context.window.cursor_warp(event.mouse_x - 300, event.mouse_y - 10); - - bpy.ops.wm.blenderkit_asset_popup('INVOKE_DEFAULT') - # context.window.cursor_warp(event.mouse_x, event.mouse_y); - - # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_asset_menu') - return {'RUNNING_MODAL'} - - if event.type == 'LEFTMOUSE': - - r = self.region - mx = event.mouse_region_x - my = event.mouse_region_y - - ui_props = context.window_manager.blenderkitUI - if event.value == 'PRESS' and ui_props.active_index > -1: - # start dragging models and materials - bpy.ops.view3d.asset_drag_drop('INVOKE_DEFAULT', - asset_search_index=ui_props.active_index) - # ui_props.draw_tooltip = False - - if ui_props.rating_on: - res = interact_rating(r, mx, my, event) - if res: - return {'RUNNING_MODAL'} - - if 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.scroll_offset > 0 and (ui_props.wcount * ui_props.hcount) > len(sr) - ui_props.scroll_offset: - ui_props.scroll_offset = len(sr) - (ui_props.wcount * ui_props.hcount) - - if event.value == 'RELEASE': # Confirm - # ui_props.drag_init = False - - # scroll with buttons by a whole page - if mx > ui_props.bar_x + ui_props.bar_width - 50 and len( - sr) - ui_props.scroll_offset > ui_props.wcount * ui_props.hcount: - ui_props.scroll_offset = min( - ui_props.scroll_offset + (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.scroll_offset > 0: - ui_props.scroll_offset = max(0, ui_props.scroll_offset - ui_props.wcount * ui_props.hcount) - return {'RUNNING_MODAL'} - - if ui_props.active_index == -3: - return {'RUNNING_MODAL'} - else: - return {'RUNNING_MODAL'} - - if event.type == 'W' and ui_props.active_index > -1: - sr = bpy.context.window_manager['search results'] - asset_data = sr[ui_props.active_index] - a = bpy.context.window_manager['bkit authors'].get(asset_data['author']['id']) - if a is not None: - utils.p('author:', a) - if a.get('aboutMeUrl') is not None: - bpy.ops.wm.url_open(url=a['aboutMeUrl']) - return {'RUNNING_MODAL'} - if event.type == 'A' and ui_props.active_index > -1: - sr = bpy.context.window_manager['search results'] - asset_data = sr[ui_props.active_index] - a = asset_data['author']['id'] - if a is not None: - sprops = utils.get_search_props() - sprops.search_keywords = '' - sprops.search_verification_status = 'ALL' - utils.p('author:', a) - search.search(author_id=a) - return {'RUNNING_MODAL'} - - if event.type == 'X' and ui_props.active_index > -1: - # delete downloaded files for this asset - sr = bpy.context.window_manager['search results'] - asset_data = sr[ui_props.active_index] - bk_logger.info('delete asset from local drive:' + asset_data['name']) - 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.window_manager.blenderkitUI - sr = bpy.context.window_manager.get('search results') - - 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) - - if ui_props.assetbar_on: - # we don't want to run the assetbar more than once, 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 already 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 - - if sr is None: - bpy.context.window_manager['search results'] = [] - - if context.area.type != 'VIEW_3D': - self.report({'WARNING'}, "View3D not found, cannot run operator") - return {'CANCELLED'} - - # the arguments we pass the the callback - args = (self, context) - - self.window = context.window - self.area = context.area - self.scene = bpy.context.scene - - self.has_quad_views = len(bpy.context.area.spaces[0].region_quadviews) > 0 - - for r in self.area.regions: - if r.type == 'WINDOW': - self.region = r - - global active_window_pointer, active_area_pointer, active_region_pointer - active_window_pointer = self.window.as_pointer() - active_area_pointer = self.area.as_pointer() - active_region_pointer = self.region.as_pointer() - - update_ui_size(self.area, self.region) - - self._handle_2d = bpy.types.SpaceView3D.draw_handler_add(draw_callback_2d, args, 'WINDOW', 'POST_PIXEL') - - ui_props.assetbar_on = True - - # in an exceptional case these were accessed before drag start. - self.drag_start_x = 0 - self.drag_start_y = 0 - - context.window_manager.modal_handler_add(self) - return {'RUNNING_MODAL'} - - def execute(self, context): - return {'RUNNING_MODAL'} - - -class TransferBlenderkitData(bpy.types.Operator): - """Regenerate cobweb""" - bl_idname = "object.blenderkit_data_trasnfer" - bl_label = "Transfer BlenderKit data" - bl_description = "Transfer blenderKit metadata from one object to another when fixing uploads with wrong parenting" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - source_ob = bpy.context.active_object - for target_ob in bpy.context.selected_objects: - if target_ob != source_ob: - target_ob.property_unset('blenderkit') - for k in source_ob.keys(): - target_ob[k] = source_ob[k] - source_ob.property_unset('blenderkit') - return {'FINISHED'} - - -class UndoWithContext(bpy.types.Operator): - """Regenerate cobweb""" - bl_idname = "wm.undo_push_context" - bl_label = "BlnenderKit undo push" - bl_description = "BlenderKit undo push with fixed context" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - # def modal(self, context, event): - # return {'RUNNING_MODAL'} - - message: StringProperty('Undo Message', default='BlenderKit operation') - - def execute(self, context): - # C_dict = utils.get_fake_context(context) - # w, a, r = get_largest_area(area_type=area_type) - # wm = bpy.context.window_manager#bpy.data.window_managers[0] - # w = wm.windows[0] - # - # C_dict = {'window': w, 'screen': w.screen} - # bpy.ops.ed.undo_push(C_dict, 'INVOKE_REGION_WIN', message=self.message) - # bpy.ops.ed.undo_push('INVOKE_REGION_WIN', message=self.message) - - return {'FINISHED'} - - -def draw_callback_dragging(self, context): - try: - img = bpy.data.images.get(self.iname) - except: - # self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_dragging, args, 'WINDOW', 'POST_PIXEL') - # self._handle_3d = bpy.types.SpaceView3D.draw_handler_add(draw_callback_3d_dragging, args, 'WINDOW', - # bpy.types.SpaceView3D.draw_handler_remove(self._handle, - # bpy.types.SpaceView3D.draw_handler_remove(self._handle_3d, 'WINDOW') - - return - linelength = 35 - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - ui_bgl.draw_image(self.mouse_x + linelength, self.mouse_y - linelength - ui_props.thumb_size, - ui_props.thumb_size, ui_props.thumb_size, img, 1) - ui_bgl.draw_line2d(self.mouse_x, self.mouse_y, self.mouse_x + linelength, - self.mouse_y - linelength, 2, colors.WHITE) - - -def draw_callback_3d_dragging(self, context): - ''' Draw snapped bbox while dragging. ''' - if not utils.guard_from_crash(): - return - ui_props = context.window_manager.blenderkitUI - # print(ui_props.asset_type, self.has_hit, self.snapped_location) - if ui_props.asset_type == 'MODEL': - if self.has_hit: - draw_bbox(self.snapped_location, self.snapped_rotation, self.snapped_bbox_min, self.snapped_bbox_max) - - -def find_and_activate_instancers(object): - for ob in bpy.context.visible_objects: - if ob.instance_type == 'COLLECTION' and ob.instance_collection and object.name in ob.instance_collection.objects: - utils.activate(ob) - return ob - - -class AssetDragOperator(bpy.types.Operator): - """Drag & drop assets into scene""" - bl_idname = "view3d.asset_drag_drop" - bl_label = "BlenderKit asset drag drop" - - asset_search_index: IntProperty(name="Active Index", default=0) - drag_length: IntProperty(name="Drag_length", default=0) - - object_name = None - - def handlers_remove(self): - bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') - bpy.types.SpaceView3D.draw_handler_remove(self._handle_3d, 'WINDOW') - - def mouse_release(self): - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - - if ui_props.asset_type == 'MODEL': - if not self.drag: - self.snapped_location = scene.cursor.location - self.snapped_rotation = (0, 0, 0) - - target_object = '' - if self.object_name is not None: - target_object = self.object_name - target_slot = '' - - if 'particle_plants' in self.asset_data['tags']: - bpy.ops.object.blenderkit_particles_drop("INVOKE_DEFAULT", - asset_search_index=self.asset_search_index, - model_location=self.snapped_location, - model_rotation=self.snapped_rotation, - target_object=target_object) - else: - bpy.ops.scene.blenderkit_download(True, - # asset_type=ui_props.asset_type, - asset_index=self.asset_search_index, - model_location=self.snapped_location, - model_rotation=self.snapped_rotation, - target_object=target_object) - if ui_props.asset_type == 'MATERIAL': - object = None - target_object = '' - target_slot = '' - if not self.drag: - # click interaction - object = bpy.context.active_object - if object is None: - ui_panels.ui_message(title='Nothing selected', - message=f"Select something to assign materials by clicking.") - return - target_object = object.name - target_slot = object.active_material_index - self.snapped_location = object.location - elif self.object_name is not None and self.has_hit: - - # first, test if object can have material applied. - object = bpy.data.objects[self.object_name] - # this enables to run Bring to scene automatically when dropping on a linked objects. - # it's however quite a slow operation, that's why not enabled (and finished) now. - # if object is not None and object.is_library_indirect: - # find_and_activate_instancers(object) - # bpy.ops.object.blenderkit_bring_to_scene() - if object is not None and \ - not object.is_library_indirect and \ - object.type in utils.supported_material_drag: - - target_object = object.name - # create final mesh to extract correct material slot - depsgraph = bpy.context.evaluated_depsgraph_get() - object_eval = object.evaluated_get(depsgraph) - - if object.type == 'MESH': - temp_mesh = object_eval.to_mesh() - target_slot = temp_mesh.polygons[self.face_index].material_index - object_eval.to_mesh_clear() - else: - ui_props.snapped_location = object.location - target_slot = object.active_material_index - - if not object: - return - if object.is_library_indirect: - ui_panels.ui_message(title='This object is linked from outer file', - message="Please select the model," - "go to the 'Selected Model' panel " - "in BlenderKit and hit 'Bring to Scene' first.") - return - if object.type not in utils.supported_material_drag: - if object.type in utils.supported_material_click: - ui_panels.ui_message(title='Unsupported object type', - message=f"Use click interaction for {object.type.lower()} object.") - return - else: - ui_panels.ui_message(title='Unsupported object type', - message=f"Can't assign materials to {object.type.lower()} object.") - return - - if target_object != '': - # position is for downloader: - loc = self.snapped_location - rotation = (0, 0, 0) - - utils.automap(target_object, target_slot=target_slot, - tex_size=self.asset_data.get('texture_size_meters', 1.0)) - bpy.ops.scene.blenderkit_download(True, - # asset_type=ui_props.asset_type, - asset_index=self.asset_search_index, - model_location=loc, - model_rotation=rotation, - target_object=target_object, - material_target_slot=target_slot) - - if ui_props.asset_type == 'HDR': - bpy.ops.scene.blenderkit_download('INVOKE_DEFAULT', - asset_index=self.asset_search_index, - # replace_resolution=True, - invoke_resolution=True, - max_resolution=self.asset_data.get('max_resolution', 0) - ) - - if ui_props.asset_type == 'SCENE': - bpy.ops.scene.blenderkit_download('INVOKE_DEFAULT', - asset_index=self.asset_search_index, - # replace_resolution=True, - invoke_resolution=False, - invoke_scene_settings=True - ) - - if ui_props.asset_type == 'BRUSH': - bpy.ops.scene.blenderkit_download( # asset_type=ui_props.asset_type, - asset_index=self.asset_search_index, - ) - - def modal(self, context, event): - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - context.area.tag_redraw() - - # if event.type == 'MOUSEMOVE': - if not hasattr(self, 'start_mouse_x'): - self.start_mouse_x = event.mouse_region_x - self.start_mouse_y = event.mouse_region_y - - self.mouse_x = event.mouse_region_x - self.mouse_y = event.mouse_region_y - - # are we dragging already? - drag_threshold = 10 - if not self.drag and \ - (abs(self.start_mouse_x - self.mouse_x) > drag_threshold or \ - abs(self.start_mouse_y - self.mouse_y) > drag_threshold): - self.drag = True - - if self.drag and ui_props.assetbar_on: - # turn off asset bar here, shout start again after finishing drag drop. - ui_props.turn_off = True - - if (event.type == 'ESC' or \ - not mouse_in_region(context.region, self.mouse_x, self.mouse_y)) and \ - (not self.drag or self.steps < 5): - # this case is for canceling from inside popup card when there's an escape attempt to close the window - return {'PASS_THROUGH'} - - if event.type in {'RIGHTMOUSE', 'ESC'} or \ - not mouse_in_region(context.region, self.mouse_x, self.mouse_y): - self.handlers_remove() - bpy.context.window.cursor_set("DEFAULT") - ui_props.dragging = False - return {'CANCELLED'} - - sprops = bpy.context.window_manager.blenderkit_models - if event.type == 'WHEELUPMOUSE': - sprops.offset_rotation_amount += sprops.offset_rotation_step - return {'RUNNING_MODAL'} - elif event.type == 'WHEELDOWNMOUSE': - sprops.offset_rotation_amount -= sprops.offset_rotation_step - return {'RUNNING_MODAL'} - - if event.type == 'MOUSEMOVE': - - #### TODO - this snapping code below is 3x in this file.... refactor it. - self.has_hit, self.snapped_location, self.snapped_normal, self.snapped_rotation, self.face_index, object, self.matrix = mouse_raycast( - context, event.mouse_region_x, event.mouse_region_y) - if object is not None: - self.object_name = object.name - - # MODELS can be dragged on scene floor - if not self.has_hit and ui_props.asset_type == 'MODEL': - self.has_hit, self.snapped_location, self.snapped_normal, self.snapped_rotation, self.face_index, object, self.matrix = floor_raycast( - context, - event.mouse_region_x, event.mouse_region_y) - if object is not None: - self.object_name = object.name - - if ui_props.asset_type == 'MODEL': - self.snapped_bbox_min = Vector(self.asset_data['bbox_min']) - self.snapped_bbox_max = Vector(self.asset_data['bbox_max']) - #return {'RUNNING_MODAL'} - - if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': - self.mouse_release() # does the main job with assets - self.handlers_remove() - bpy.context.window.cursor_set("DEFAULT") - - bpy.ops.object.run_assetbar_fix_context(keep_running=True, do_search=False) - ui_props.dragging = False - return {'FINISHED'} - - self.steps += 1 - - #pass event to assetbar so it can close itself - if ui_props.assetbar_on and ui_props.turn_off: - return {'PASS_THROUGH'} - - return {'RUNNING_MODAL'} - - def invoke(self, context, event): - if context.area.type == 'VIEW_3D': - # the arguments we pass the the callback - args = (self, context) - # Add the region OpenGL drawing callback - # draw in view space with 'POST_VIEW' and 'PRE_VIEW' - self.iname = utils.previmg_name(self.asset_search_index) - - self.mouse_x = 0 - self.mouse_y = 0 - self.steps = 0 - - self.has_hit = False - self.snapped_location = (0, 0, 0) - self.snapped_normal = (0, 0, 1) - self.snapped_rotation = (0, 0, 0) - self.face_index = 0 - object = None - self.matrix = None - - sr = bpy.context.window_manager['search results'] - self.asset_data = sr[self.asset_search_index] - - if not self.asset_data.get('canDownload'): - message = "Let's support asset creators and Open source." - link_text = 'Unlock the asset.' - url = paths.get_bkit_url() + '/get-blenderkit/' + self.asset_data['id'] + '/?from_addon=True' - bpy.ops.wm.blenderkit_url_dialog('INVOKE_REGION_WIN', url=url, message=message, - link_text=link_text) - - return {'CANCELLED'} - - self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_dragging, args, 'WINDOW', 'POST_PIXEL') - self._handle_3d = bpy.types.SpaceView3D.draw_handler_add(draw_callback_3d_dragging, args, 'WINDOW', - 'POST_VIEW') - - bpy.context.window.cursor_set("NONE") - ui_props = bpy.context.window_manager.blenderkitUI - ui_props.dragging = True - self.drag = False - context.window_manager.modal_handler_add(self) - return {'RUNNING_MODAL'} - else: - self.report({'WARNING'}, "View3D not found, cannot run operator") - return {'CANCELLED'} - - -class RunAssetBarWithContext(bpy.types.Operator): - """Regenerate cobweb""" - bl_idname = "object.run_assetbar_fix_context" - bl_label = "BlnenderKit assetbar with fixed context" - bl_description = "Run assetbar with fixed context" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - keep_running: BoolProperty(name="Keep Running", description='', default=True, options={'SKIP_SAVE'}) - do_search: BoolProperty(name="Run Search", description='', default=False, options={'SKIP_SAVE'}) - - # def modal(self, context, event): - # return {'RUNNING_MODAL'} - - def execute(self, context): - C_dict = utils.get_fake_context(context) - if C_dict.get('window'): # no 3d view, no asset bar. - preferences = bpy.context.preferences.addons['blenderkit'].preferences - if 1:#preferences.experimental_features: - bpy.ops.view3d.blenderkit_asset_bar_widget(C_dict, 'INVOKE_REGION_WIN', keep_running=self.keep_running, - do_search=self.do_search) - - else: - bpy.ops.view3d.blenderkit_asset_bar(C_dict, 'INVOKE_REGION_WIN', keep_running=self.keep_running, - do_search=self.do_search) - return {'FINISHED'} - - -classes = ( - AssetBarOperator, - # AssetBarExperiment, - AssetDragOperator, - RunAssetBarWithContext, - TransferBlenderkitData, - UndoWithContext, - ParticlesDropDialog -) - -# store keymap items here to access after registration -addon_keymapitems = [] - - -# @persistent -def pre_load(context): - ui_props = bpy.context.window_manager.blenderkitUI - ui_props.assetbar_on = False - ui_props.turn_off = True - preferences = bpy.context.preferences.addons['blenderkit'].preferences - preferences.login_attempt = False - - -def register_ui(): - global handler_2d, handler_3d - - for c in classes: - bpy.utils.register_class(c) - - args = (None, bpy.context) - - handler_2d = bpy.types.SpaceView3D.draw_handler_add(draw_callback_2d_progress, args, 'WINDOW', 'POST_PIXEL') - handler_3d = 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') - # asset bar shortcut - kmi = km.keymap_items.new("object.run_assetbar_fix_context", 'SEMI_COLON', 'PRESS', ctrl=False, shift=False) - kmi.properties.keep_running = False - kmi.properties.do_search = False - addon_keymapitems.append(kmi) - # fast rating shortcut - wm = bpy.context.window_manager - km = wm.keyconfigs.addon.keymaps['Window'] - kmi = km.keymap_items.new(ratings.FastRateMenu.bl_idname, 'F', 'PRESS', ctrl=False, shift=False) - addon_keymapitems.append(kmi) - kmi = km.keymap_items.new(upload.FastMetadata.bl_idname, 'F', 'PRESS', ctrl=True, shift=False) - addon_keymapitems.append(kmi) - - -def unregister_ui(): - global handler_2d, handler_3d - pre_load(bpy.context) - - bpy.types.SpaceView3D.draw_handler_remove(handler_2d, 'WINDOW') - bpy.types.SpaceView3D.draw_handler_remove(handler_3d, 'WINDOW') - - for c in classes: - bpy.utils.unregister_class(c) - - wm = bpy.context.window_manager - if not wm.keyconfigs.addon: - return - - km = wm.keyconfigs.addon.keymaps.get('Window') - if km: - for kmi in addon_keymapitems: - km.keymap_items.remove(kmi) - del addon_keymapitems[:] diff --git a/blenderkit/ui_bgl.py b/blenderkit/ui_bgl.py deleted file mode 100644 index 339f5983..00000000 --- a/blenderkit/ui_bgl.py +++ /dev/null @@ -1,153 +0,0 @@ -# ##### 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) - -cached_images = {} -def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1), batch = None): - # draw_rect(x,y, width, height, (.5,0,0,.5)) - if not image: - return; - ci = cached_images.get(image.filepath) - if ci is not None: - if ci['x'] == x and ci['y'] ==y: - batch = ci['batch'] - image_shader = ci['image_shader'] - if not batch: - - 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)] - - image_shader = shader = gpu.shader.from_builtin('2D_IMAGE') - batch = batch_for_shader(image_shader, 'TRIS', - {"pos": coords, - "texCoord": uvs}, - indices=indices) - - - # tell shader to use the image that is bound to image unit 0 - image_shader.uniform_int("image", 0) - cached_images[image.filepath] = { - 'x': x, - 'y': y, - 'batch': batch, - 'image_shader': image_shader - } - # 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) - - image_shader.bind() - - batch.draw(image_shader) - - # bgl.glDisable(bgl.GL_TEXTURE_2D) - return batch - - -def draw_text(text, x, y, size, color=(1, 1, 1, 0.5), halign = 'LEFT', valign = 'TOP'): - font_id = 1 - # bgl.glColor4f(*color) - blf.color(font_id, color[0], color[1], color[2], color[3]) - blf.size(font_id, size, 72) - if halign != 'LEFT': - width,height = blf.dimensions(font_id, text) - if halign == 'RIGHT': - x-=width - elif halign == 'CENTER': - x-=width//2 - if valign=='CENTER': - y-=height//2 - #bottom could be here but there's no reason for it - blf.position(font_id, x, y, 0) - - blf.draw(font_id, text) diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py deleted file mode 100644 index d976fbdd..00000000 --- a/blenderkit/ui_panels.py +++ /dev/null @@ -1,2680 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import paths, comments_utils, ratings, ratings_utils, utils, download, categories, icons, search, \ - resolutions, ui, \ - tasks_queue, \ - autothumb, upload - -from bpy.types import ( - Panel -) -from bpy.props import ( - IntProperty, - FloatProperty, - FloatVectorProperty, - StringProperty, - EnumProperty, - BoolProperty, - PointerProperty, -) - -import bpy -import os -import random -import logging -import platform -import ctypes - -bk_logger = logging.getLogger('blenderkit') - - -# this was moved to separate interface: - -def draw_ratings(layout, context, asset): - # layout.operator("wm.url_open", text="Read rating instructions", icon='QUESTION').url = 'https://support.google.com/?hl=en' - # the following shouldn't happen at all in an optimal case, - # this function should run only when asset was already checked to be existing - if asset == None: - return; - - col = layout.column() - bkit_ratings = asset.bkit_ratings - - # layout.template_icon_view(bkit_ratings, property, show_labels=False, scale=6.0, scale_popup=5.0) - - row = col.row() - row.prop(bkit_ratings, 'rating_quality_ui', expand=True, icon_only=True, emboss=False) - if bkit_ratings.rating_quality > 0: - col.separator() - col.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 - # re-enable layout if included in longer panel - - -def draw_not_logged_in(source, message='Please Login/Signup to use this feature'): - title = "You aren't logged in" - - def draw_message(source, context): - layout = source.layout - utils.label_multiline(layout, text=message) - draw_login_buttons(layout) - - bpy.context.window_manager.popup_menu(draw_message, title=title, icon='INFO') - - -def draw_upload_common(layout, props, asset_type, context): - op = layout.operator("wm.url_open", text=f"Read {asset_type.lower()} 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 - if asset_type == 'BRUSH': - op.url = paths.BLENDERKIT_BRUSH_UPLOAD_INSTRUCTIONS_URL - if asset_type == 'SCENE': - op.url = paths.BLENDERKIT_SCENE_UPLOAD_INSTRUCTIONS_URL - if asset_type == 'HDR': - op.url = paths.BLENDERKIT_HDR_UPLOAD_INSTRUCTIONS_URL - - row = layout.row(align=True) - if props.upload_state != '': - utils.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() - op = layout.operator("object.blenderkit_upload", text=optext, icon='EXPORT') - op.asset_type = asset_type - op.reupload = False - # make sure everything gets uploaded. - op.main_file = True - op.metadata = True - op.thumbnail = True - - if props.asset_base_id != '': - op = layout.operator("object.blenderkit_upload", text='Reupload asset', icon='EXPORT') - op.asset_type = asset_type - op.reupload = True - - op = layout.operator("object.blenderkit_upload", text='Upload as new asset', icon='EXPORT') - op.asset_type = asset_type - op.reupload = False - - # layout.label(text = 'asset id, overwrite only for reuploading') - layout.label(text='asset has a version online.') - # row = layout.row() - # row.enabled = False - # row.prop(props, 'asset_base_id', icon='FILE_TICK') - # row = layout.row() - # row.enabled = False - # row.prop(props, 'id', icon='FILE_TICK') - layout.prop(props, 'category') - if props.category != 'NONE' and props.subcategory != 'NONE': - layout.prop(props, 'subcategory') - if props.subcategory != 'NONE' and props.subcategory1 != 'NONE': - layout.prop(props, 'subcategory1') - - layout.prop(props, 'is_private', expand=True) - if props.is_private == 'PUBLIC': - layout.prop(props, 'license') - layout.prop(props, 'is_free', expand=True) - - prop_needed(layout, props, 'name', props.name) - if props.is_private == 'PUBLIC': - prop_needed(layout, props, 'description', props.description) - prop_needed(layout, props, 'tags', props.tags) - else: - layout.prop(props, 'description') - layout.prop(props, 'tags') - - -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.alert = True - row.prop(props, name) # , icon=icon) - row.alert = False - else: - # row.label(text='', icon = 'FILE_TICK') - icon = None - row.prop(props, name) - - -def draw_panel_hdr_upload(self, context): - layout = self.layout - ui_props = bpy.context.window_manager.blenderkitUI - - # layout.prop_search(ui_props, "hdr_upload_image", bpy.data, "images") - layout.prop(ui_props, "hdr_upload_image") - - hdr = utils.get_active_HDR() - - if hdr is not None: - props = hdr.blenderkit - - layout = self.layout - - draw_upload_common(layout, props, 'HDR', context) - - -def draw_panel_hdr_search(self, context): - s = context.scene - wm = context.window_manager - props = wm.blenderkit_HDR - - layout = self.layout - row = layout.row() - row.prop(props, "search_keywords", text="", icon='VIEWZOOM') - draw_assetbar_show_hide(row, props) - layout.prop(props, "own_only") - - utils.label_multiline(layout, text=props.report) - - -def draw_thumbnail_upload_panel(layout, props): - update = False - tex = autothumb.get_texture_ui(props.thumbnail, 'upload_preview') - if not tex or not tex.image: - return - box = layout.box() - box.template_icon(icon_value=tex.image.preview.icon_id, scale=6.0) - - -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) - - col = layout.column() - if props.is_generating_thumbnail: - col.enabled = False - - draw_thumbnail_upload_panel(col, props) - - prop_needed(col, props, 'thumbnail', props.thumbnail) - 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 != '': - utils.label_multiline(layout, text=props.thumbnail_generating_state) - - # 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, '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() - 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') - - col = layout.column() - # if props.is_generating_thumbnail: - # col.enabled = False - draw_thumbnail_upload_panel(col, props) - - 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 != '': - # utils.label_multiline(layout, text = props.thumbnail_generating_state) - - 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() - row.prop(props, 'work_hours') - layout.prop(props, 'adult') - - -def draw_assetbar_show_hide(layout, props): - s = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - - if ui_props.assetbar_on: - icon = 'HIDE_OFF' - ttip = 'Click to Hide Asset Bar' - else: - icon = 'HIDE_ON' - ttip = 'Click to Show Asset Bar' - - preferences = bpy.context.preferences.addons['blenderkit'].preferences - if 1:#preferences.experimental_features: - op = layout.operator('view3d.blenderkit_asset_bar_widget', text='', icon=icon) - else: - op = layout.operator('view3d.blenderkit_asset_bar', text='', icon=icon) - op.keep_running = False - op.do_search = False - op.tooltip = ttip - - -def draw_panel_model_search(self, context): - wm = bpy.context.window_manager - props = wm.blenderkit_models - - layout = self.layout - - row = layout.row() - row.prop(props, "search_keywords", text="", icon='VIEWZOOM') - draw_assetbar_show_hide(row, props) - - icon = 'NONE' - if props.report == 'You need Full plan to get this item.': - icon = 'ERROR' - utils.label_multiline(layout, text=props.report, icon=icon) - if props.report == 'You need Full plan to get this item.': - layout.operator("wm.url_open", text="Get Full plan", icon='URL').url = paths.BLENDERKIT_PLANS - - # layout.prop(props, "search_style") - # layout.prop(props, "own_only") - # layout.prop(props, "free_only") - - # 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) - - # draw_panel_categories(self, context) - - -def draw_panel_scene_search(self, context): - wm = bpy.context.window_manager - props = wm.blenderkit_scene - layout = self.layout - # layout.label(text = "common search properties:") - row = layout.row() - row.prop(props, "search_keywords", text="", icon='VIEWZOOM') - draw_assetbar_show_hide(row, props) - layout.prop(props, "own_only") - utils.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 = "Selected Model" - bl_context = "objectmode" - - @classmethod - def poll(cls, context): - p = bpy.context.view_layer.objects.active is not None - return p - - def draw(self, context): - # draw asset properties here - layout = self.layout - - o = utils.get_active_model() - # o = bpy.context.active_object - if o.get('asset_data') is None: - utils.label_multiline(layout, - text='To upload this asset to BlenderKit, go to the Find and Upload Assets panel.') - layout.prop(o, 'name') - - if o.get('asset_data') is not None: - ad = o['asset_data'] - layout.label(text=str(ad['name'])) - if o.instance_type == 'COLLECTION' and o.instance_collection is not None: - layout.operator('object.blenderkit_bring_to_scene', text='Bring to scene') - - layout.label(text='Asset tools:') - draw_asset_context_menu(self.layout, context, ad, from_panel=True) - # if 'rig' in ad['tags']: - # # layout.label(text = 'can make proxy') - # layout.operator('object.blenderkit_make_proxy', text = 'Make Armature proxy') - # fast upload, blocked by now - # else: - # op = layout.operator("object.blenderkit_upload", text='Store as private', icon='EXPORT') - # op.asset_type = 'MODEL' - # op.fast = True - # fun override project, not finished - # layout.operator('object.blenderkit_color_corrector') - - -class NODE_PT_blenderkit_material_properties(Panel): - bl_category = "BlenderKit" - bl_idname = "NODE_PT_blenderkit_material_properties" - bl_space_type = 'NODE_EDITOR' - bl_region_type = 'UI' - bl_label = "Selected Material" - bl_context = "objectmode" - - @classmethod - def poll(cls, context): - p = bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None - return p - - def draw(self, context): - # draw asset properties here - layout = self.layout - - m = bpy.context.active_object.active_material - # o = bpy.context.active_object - if m.get('asset_data') is None and m.blenderkit.id == '': - utils.label_multiline(layout, - text='To upload this asset to BlenderKit, go to the Find and Upload Assets panel.') - layout.prop(m, 'name') - - if m.get('asset_data') is not None: - ad = m['asset_data'] - layout.label(text=str(ad['name'])) - - layout.label(text='Asset tools:') - draw_asset_context_menu(self.layout, context, ad, from_panel=True) - # if 'rig' in ad['tags']: - # # layout.label(text = 'can make proxy') - # layout.operator('object.blenderkit_make_proxy', text = 'Make Armature proxy') - # fast upload, blocked by now - # else: - # op = layout.operator("object.blenderkit_upload", text='Store as private', icon='EXPORT') - # op.asset_type = 'MODEL' - # op.fast = True - # fun override project, not finished - # layout.operator('object.blenderkit_color_corrector') - - -def draw_rating_asset(self, context, asset): - layout = self.layout - col = layout.box() - # split = layout.split(factor=0.5) - # col1 = split.column() - # col2 = split.column() - # print('%s_search' % asset['asset_data']['assetType']) - directory = paths.get_temp_dir('%s_search' % asset['asset_data']['assetType']) - tpath = os.path.join(directory, asset['asset_data']['thumbnail_small']) - for image in bpy.data.images: - if image.filepath == tpath: - # split = row.split(factor=1.0, align=False) - col.template_icon(icon_value=image.preview.icon_id, scale=6.0) - break; - # layout.label(text = '', icon_value=image.preview.icon_id, scale = 10) - col.label(text=asset.name) - draw_ratings(col, context, asset=asset) - - -class VIEW3D_PT_blenderkit_ratings(Panel): - bl_category = "BlenderKit" - bl_idname = "VIEW3D_PT_blenderkit_ratings" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_label = "Please rate" - bl_context = "objectmode" - - @classmethod - def poll(cls, context): - # - p = bpy.context.view_layer.objects.active is not None - return p - - def draw(self, context): - # TODO make a list of assets inside asset appending code, to happen only when assets are added to the scene. - # draw asset properties here - layout = self.layout - assets = ratings.get_assets_for_rating() - if len(assets) > 0: - utils.label_multiline(layout, text='Please help BlenderKit community by rating these assets:') - - for a in assets: - if a.bkit_ratings.rating_work_hours == 0: - draw_rating_asset(self, context, asset=a) - - -def draw_login_progress(layout): - layout.label(text='Login through browser') - layout.label(text='in progress.') - layout.operator("wm.blenderkit_login_cancel", text="Cancel", icon='CANCEL') - - -class VIEW3D_PT_blenderkit_profile(Panel): - bl_category = "BlenderKit" - bl_idname = "VIEW3D_PT_blenderkit_profile" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_label = "BlenderKit Profile" - - @classmethod - def poll(cls, context): - - return True - - def draw(self, context): - # draw asset properties here - layout = self.layout - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - - if user_preferences.login_attempt: - draw_login_progress(layout) - return - - if user_preferences.api_key != '': - me = bpy.context.window_manager.get('bkit profile') - if me is not None: - me = me['user'] - # user name - if len(me['firstName']) > 0 or len(me['lastName']) > 0: - layout.label(text=f"Me: {me['firstName']} {me['lastName']}") - else: - layout.label(text=f"Me: {me['email']}") - # layout.label(text='Email: %s' % (me['email'])) - - # plan information - - if me.get('currentPlanName') is not None: - pn = me['currentPlanName'] - pcoll = icons.icon_collections["main"] - if pn == 'Free': - my_icon = pcoll['free'] - else: - my_icon = pcoll['full'] - - row = layout.row() - row.label(text='My plan:') - row.label(text='%s plan' % pn, icon_value=my_icon.icon_id) - if pn == 'Free': - layout.operator("wm.url_open", text="Change plan", - icon='URL').url = paths.get_bkit_url() + paths.BLENDERKIT_PLANS - - # storage statistics - # if me.get('sumAssetFilesSize') is not None: # TODO remove this when production server has these too. - # layout.label(text='My public assets: %i MiB' % (me['sumAssetFilesSize'])) - # if me.get('sumPrivateAssetFilesSize') is not None: - # layout.label(text='My private assets: %i MiB' % (me['sumPrivateAssetFilesSize'])) - if me.get('remainingPrivateQuota') is not None: - layout.label(text='My free storage: %i MiB' % (me['remainingPrivateQuota'])) - - layout.operator("wm.url_open", text="See my uploads", - icon='URL').url = paths.get_bkit_url() + paths.BLENDERKIT_USER_ASSETS - - -class MarkNotificationRead(bpy.types.Operator): - """Mark notification as read here and also on BlenderKit server""" - bl_idname = "wm.blenderkit_mark_notification_read" - bl_label = "Mark notification as read" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - notification_id: bpy.props.IntProperty( - name="Id", - description="notification id", - default=-1) - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - notifications = bpy.context.window_manager['bkit notifications'] - for n in notifications['results']: - if n['id'] == self.notification_id: - n['unread'] = 0 - comments_utils.check_notifications_read() - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - api_key = user_preferences.api_key - comments_utils.mark_notification_read_thread(api_key, self.notification_id) - - return {'FINISHED'} - -class MarkAllNotificationsRead(bpy.types.Operator): - """Mark notification as read here and also on BlenderKit server""" - bl_idname = "wm.blenderkit_mark_notifications_read_all" - bl_label = "Mark all notifications as read" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - api_key = user_preferences.api_key - notifications = bpy.context.window_manager['bkit notifications'] - for n in notifications.get('results'): - if n['unread'] == 1: - n['unread'] = 0 - comments_utils.mark_notification_read_thread(api_key, n['id']) - - comments_utils.check_notifications_read() - return {'FINISHED'} - -class NotificationOpenTarget(bpy.types.Operator): - """""" - bl_idname = "wm.blenderkit_open_notification_target" - bl_label = "" - bl_description = "Open notification target and mark notification as read" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - tooltip: bpy.props.StringProperty(default='Open a web page') - url: bpy.props.StringProperty(default='Runs search and displays the asset bar at the same time') - notification_id: bpy.props.IntProperty( - name="Id", - description="notification id", - default=-1) - - @classmethod - def description(cls, context, properties): - return properties.tooltip - - def execute(self, context): - bpy.ops.wm.blenderkit_mark_notification_read(notification_id=self.notification_id) - bpy.ops.wm.url_open(url=self.url) - return {'FINISHED'} - - -class LikeComment(bpy.types.Operator): - """Mark notification as read here and also on BlenderKit server""" - bl_idname = "wm.blenderkit_like_comment" - bl_label = "BlenderKit like/dislike comment" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - asset_id: StringProperty( - name="Asset Base Id", - description="Unique id of the asset (hidden)", - default="", - options={'SKIP_SAVE'}) - - comment_id: bpy.props.IntProperty( - name="Id", - description="comment id", - default=-1) - - flag: bpy.props.StringProperty( - name="flag", - description="Like/dislike comment", - default="like") - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - api_key = user_preferences.api_key - comments_utils.send_comment_flag_to_thread(asset_id=self.asset_id, comment_id=self.comment_id, flag=self.flag, - api_key=api_key) - return {'FINISHED'} - - -def draw_notification(self, notification, width=600): - layout = self.layout - box = layout.box() - firstline = f"{notification['actor']['string']} {notification['verb']} {notification['target']['string']}" - box1 = box.box() - # row = box1.row() - - split_last = 0.7 - if notification['description']: - split_last = 0 - - - rows = utils.label_multiline(box1, text=firstline, width=width, split_last = split_last) - - - if notification['description']: - rows = utils.label_multiline(box, text=notification['description'], width=width, split_last = 0.7) - - - if notification['target']: - # row = layout.row() - # split = row.split(factor=.8) - # split.label(text='') - # split = split.split() - # split = rows[-1].split(factor=0.8) - # split = split.split() - # split.alignment = 'RIGHT' - # row = split.row(align = True) - row = rows[-1] - row = row.row(align=False) - - # row = row.split(factor = 0.7) - - op = row.operator('wm.blenderkit_open_notification_target', text='Open page', icon='HIDE_OFF') - op.tooltip = 'Open the browser on the asset page to comment' - op.url = paths.get_bkit_url() + notification['target']['url'] - op.notification_id = notification['id'] - # split = - op = row.operator("wm.blenderkit_mark_notification_read", text="", icon='CANCEL') - op.notification_id = notification['id'] - - -def draw_notifications(self, context, width=600): - layout = self.layout - notifications = bpy.context.window_manager.get('bkit notifications') - if notifications is not None and notifications.get('count')>0: - row = layout.row() - # row.alert = True - split = row.split(factor = 0.7) - split.label(text='') - split = split.split() - split.operator('wm.blenderkit_mark_notifications_read_all', text = 'Mark All Read', icon = 'CANCEL') - for notification in notifications['results']: - if notification['unread'] == 1: - draw_notification(self, notification, width=width) - - -class ShowNotifications(bpy.types.Operator): - """Show notifications""" - bl_idname = "wm.show_notifications" - bl_label = "Show BlenderKit notifications" - bl_options = {'REGISTER', 'UNDO'} - - notification_id: bpy.props.IntProperty( - name="Id", - description="notification id", - default=-1) - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - draw_notifications(self, context, width=600) - - def execute(self, context): - wm = bpy.context.window_manager - return wm.invoke_popup(self, width=600) - - -class VIEW3D_PT_blenderkit_notifications(Panel): - bl_category = "BlenderKit" - bl_idname = "VIEW3D_PT_blenderkit_notifications" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_label = "BlenderKit Notifications" - - @classmethod - def poll(cls, context): - notifications = bpy.context.window_manager.get('bkit notifications') - if notifications is not None and len(notifications['results']) > 0: - return True - return False - - def draw(self, context): - draw_notifications(self, context) - - -class VIEW3D_PT_blenderkit_login(Panel): - bl_category = "BlenderKit" - bl_idname = "VIEW3D_PT_blenderkit_login" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_label = "BlenderKit Login" - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - - if user_preferences.login_attempt: - draw_login_progress(layout) - return - - if user_preferences.enable_oauth: - draw_login_buttons(layout) - - -def draw_panel_model_rating(self, context): - # o = bpy.context.active_object - o = utils.get_active_model() - # print('ratings active',o) - draw_ratings(self.layout, context, asset=o) # , 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) - - # THUMBNAIL - row = layout.column() - if props.is_generating_thumbnail: - row.enabled = False - - draw_thumbnail_upload_panel(row, props) - - prop_needed(row, props, 'thumbnail', props.has_thumbnail, False) - - if bpy.context.scene.render.engine in ('CYCLES', 'BLENDER_EEVEE'): - layout.operator("object.blenderkit_generate_material_thumbnail", text='Render thumbnail with Cycles', - icon='EXPORT') - 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 != '': - utils.label_multiline(layout, text=props.thumbnail_generating_state) - - 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') - # 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') - - # 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.window_manager - props = wm.blenderkit_mat - - layout = self.layout - row = layout.row() - row.prop(props, "search_keywords", text="", icon='VIEWZOOM') - draw_assetbar_show_hide(row, props) - utils.label_multiline(layout, text=props.report) - - # layout.prop(props, 'search_style')F - # 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') - - # draw_panel_categories(self, context) - - -def draw_panel_material_ratings(self, context): - asset = bpy.context.active_object.active_material - draw_ratings(self.layout, context, asset) # , 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) - - -def draw_panel_brush_search(self, context): - wm = context.window_manager - props = wm.blenderkit_brush - - layout = self.layout - row = layout.row() - row.prop(props, "search_keywords", text="", icon='VIEWZOOM') - draw_assetbar_show_hide(row, props) - layout.prop(props, "own_only") - - utils.label_multiline(layout, text=props.report) - # draw_panel_categories(self, context) - - -def draw_panel_brush_ratings(self, context): - # props = utils.get_brush_props(context) - brush = utils.get_active_brush() - draw_ratings(self.layout, context, asset=brush) # , props) - # - # op.asset_type = 'BRUSH' - - -def draw_login_buttons(layout, invoke=False): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - - if user_preferences.login_attempt: - draw_login_progress(layout) - else: - if invoke: - layout.operator_context = 'INVOKE_DEFAULT' - else: - layout.operator_context = 'EXEC_DEFAULT' - if user_preferences.api_key == '': - layout.operator("wm.blenderkit_login", text="Login", - icon='URL').signup = False - layout.operator("wm.blenderkit_login", text="Sign up", - icon='URL').signup = True - - else: - layout.operator("wm.blenderkit_login", text="Login as someone else", - icon='URL').signup = False - layout.operator("wm.blenderkit_logout", text="Logout", - icon='URL') - - -class VIEW3D_PT_blenderkit_advanced_model_search(Panel): - bl_category = "BlenderKit" - bl_idname = "VIEW3D_PT_blenderkit_advanced_model_search" - bl_parent_id = "VIEW3D_PT_blenderkit_unified" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_label = "Search filters" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - s = context.scene - ui_props = bpy.context.window_manager.blenderkitUI - return ui_props.down_up == 'SEARCH' and ui_props.asset_type == 'MODEL' - - def draw(self, context): - wm = bpy.context.window_manager - - props = wm.blenderkit_models - layout = self.layout - 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") - - layout.prop(props, "own_only") - layout.prop(props, "free_only") - layout.prop(props, "search_style") - - # DESIGN YEAR - layout.prop(props, "search_design_year", text='Designed in Year') - if props.search_design_year: - row = layout.row(align=True) - 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='Poly Count ') - if props.search_polycount: - row = layout.row(align=True) - 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 Resolutions') - if props.search_texture_resolution: - row = layout.row(align=True) - row.prop(props, "search_texture_resolution_min", text='Min') - row.prop(props, "search_texture_resolution_max", text='Max') - - # FILE SIZE - layout.prop(props, "search_file_size", text='File Size (MB)') - if props.search_file_size: - row = layout.row(align=True) - row.prop(props, "search_file_size_min", text='Min') - row.prop(props, "search_file_size_max", text='Max') - - # AGE - layout.prop(props, "search_condition", text='Condition') # , text ='condition of object new/old e.t.c.') - layout.prop(props, "quality_limit", slider=True) # , text ='condition of object new/old e.t.c.') - - # layout.prop(props, "search_procedural", expand=True) - # ADULT - # layout.prop(props, "search_adult") # , text ='condition of object new/old e.t.c.') - - -class VIEW3D_PT_blenderkit_advanced_material_search(Panel): - bl_category = "BlenderKit" - bl_idname = "VIEW3D_PT_blenderkit_advanced_material_search" - bl_parent_id = "VIEW3D_PT_blenderkit_unified" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_label = "Search filters" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - s = context.scene - ui_props = bpy.context.window_manager.blenderkitUI - return ui_props.down_up == 'SEARCH' and ui_props.asset_type == 'MATERIAL' - - def draw(self, context): - wm = context.window_manager - props = wm.blenderkit_mat - layout = self.layout - layout.separator() - - layout.prop(props, "own_only") - - layout.label(text='Texture:') - col = layout.column() - col.prop(props, "search_procedural", expand=True) - - if props.search_procedural == 'TEXTURE_BASED': - # TEXTURE RESOLUTION - layout.prop(props, "search_texture_resolution", text='Texture Resolution') - if props.search_texture_resolution: - row = layout.row(align=True) - row.prop(props, "search_texture_resolution_min", text='Min') - row.prop(props, "search_texture_resolution_max", text='Max') - - # FILE SIZE - layout.prop(props, "search_file_size", text='File size (MB)') - if props.search_file_size: - row = layout.row(align=True) - row.prop(props, "search_file_size_min", text='Min') - row.prop(props, "search_file_size_max", text='Max') - layout.prop(props, "quality_limit", slider=True) - - -class VIEW3D_PT_blenderkit_advanced_HDR_search(Panel): - bl_category = "BlenderKit" - bl_idname = "VIEW3D_PT_blenderkit_advanced_HDR_search" - bl_parent_id = "VIEW3D_PT_blenderkit_unified" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_label = "Search filters" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - s = context.scene - ui_props = bpy.context.window_manager.blenderkitUI - return ui_props.down_up == 'SEARCH' and ui_props.asset_type == 'HDR' - - def draw(self, context): - wm = context.window_manager - props = wm.blenderkit_HDR - layout = self.layout - layout.separator() - - layout.prop(props, "own_only") - layout.prop(props, "true_hdr") - - -class VIEW3D_PT_blenderkit_categories(Panel): - bl_category = "BlenderKit" - bl_idname = "VIEW3D_PT_blenderkit_categories" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_label = "Categories" - bl_parent_id = "VIEW3D_PT_blenderkit_unified" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - s = context.scene - ui_props = bpy.context.window_manager.blenderkitUI - mode = True - if ui_props.asset_type == 'BRUSH' and not (context.sculpt_object or context.image_paint_object): - mode = False - return ui_props.down_up == 'SEARCH' and mode - - def draw(self, context): - draw_panel_categories(self, context) - - -def draw_scene_import_settings(self, context): - wm = bpy.context.window_manager - props = wm.blenderkit_scene - layout = self.layout - layout.prop(props, 'switch_after_append') - # layout.label(text='Import method:') - row = layout.row() - row.prop(props, 'append_link', expand=True, icon_only=False) - - -class VIEW3D_PT_blenderkit_import_settings(Panel): - bl_category = "BlenderKit" - bl_idname = "VIEW3D_PT_blenderkit_import_settings" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_label = "Import settings" - bl_parent_id = "VIEW3D_PT_blenderkit_unified" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - s = context.scene - ui_props = bpy.context.window_manager.blenderkitUI - return ui_props.down_up == 'SEARCH' and ui_props.asset_type in ['MATERIAL', 'MODEL', 'SCENE', 'HDR'] - - def draw(self, context): - layout = self.layout - - s = context.scene - wm = bpy.context.window_manager - ui_props = bpy.context.window_manager.blenderkitUI - - if ui_props.asset_type == 'MODEL': - # noinspection PyCallByClass - props = wm.blenderkit_models - layout.prop(props, 'randomize_rotation') - if props.randomize_rotation: - layout.prop(props, 'randomize_rotation_amount') - layout.prop(props, 'perpendicular_snap') - # if props.perpendicular_snap: - # layout.prop(props,'perpendicular_snap_threshold') - - layout.label(text='Import method:') - row = layout.row() - row.prop(props, 'append_method', expand=True, icon_only=False) - - if ui_props.asset_type == 'MATERIAL': - props = wm.blenderkit_mat - layout.prop(props, 'automap') - layout.label(text='Import method:') - row = layout.row() - - row.prop(props, 'append_method', expand=True, icon_only=False) - if ui_props.asset_type == 'SCENE': - draw_scene_import_settings(self, context) - - if ui_props.asset_type == 'HDR': - props = wm.blenderkit_HDR - - if ui_props.asset_type in ['MATERIAL', 'MODEL', 'HDR']: - layout.prop(props, 'resolution') - # layout.prop(props, 'unpack_files') - - -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 = "Find and Upload Assets" - - @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 = bpy.context.window_manager.blenderkitUI - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - wm = bpy.context.window_manager - layout = self.layout - # layout.prop_tabs_enum(ui_props, "asset_type", icon_only = True) - - row = layout.row() - # row.scale_x = 1.6 - # row.scale_y = 1.6 - # - row.prop(ui_props, 'down_up', expand=True, icon_only=False) - # row.label(text='') - # row = row.split().row() - # layout.alert = True - # layout.alignment = 'CENTER' - row = layout.row(align=True) - row.scale_x = 1.6 - row.scale_y = 1.6 - # split = row.split(factor=. - - expand_icon = 'TRIA_DOWN' - if ui_props.asset_type_fold: - expand_icon = 'TRIA_RIGHT' - row = layout.row() - split = row.split(factor=0.15) - split.prop(ui_props, 'asset_type_fold', icon=expand_icon, icon_only=True, emboss=False) - - if ui_props.asset_type_fold: - pass - # expanded interface with names in column - split = split.row() - split.scale_x = 8 - split.scale_y = 1.6 - # split = row - # split = layout.row() - else: - split = split.column() - - split.prop(ui_props, 'asset_type', expand=True, icon_only=ui_props.asset_type_fold) - # row = layout.column(align = False) - # layout.prop(ui_props, 'asset_type', expand=False, text='') - - w = context.region.width - if user_preferences.login_attempt: - draw_login_progress(layout) - return - - if len(user_preferences.api_key) < 20 and user_preferences.asset_counter > 20: - if user_preferences.enable_oauth: - draw_login_buttons(layout) - else: - op = layout.operator("wm.url_open", text="Get your API Key", - icon='QUESTION') - op.url = paths.BLENDERKIT_SIGNUP_URL - layout.label(text='Paste your API Key:') - layout.prop(user_preferences, 'api_key', text='') - layout.separator() - # if bpy.data.filepath == '': - # layout.alert = True - # utils.label_multiline(layout, text="It's better to save your file first.", width=w) - # layout.alert = False - # layout.separator() - - if ui_props.down_up == 'SEARCH': - if utils.profile_is_validator(): - search_props = utils.get_search_props() - layout.prop(search_props, 'search_verification_status') - layout.prop(search_props, "unrated_only") - - 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) - if ui_props.asset_type == 'HDR': - # noinspection PyCallByClass - draw_panel_hdr_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: - utils.label_multiline(layout, text='Switch to paint or sculpt mode.', width=context.region.width) - return - - - 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 - op.tooltip = 'Show/Hide asset preview' - - 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.' - utils.label_multiline(layout, rtext, icon='ERROR', width=w) - return; - - if ui_props.asset_type == 'MODEL': - # utils.label_multiline(layout, "Uploaded models won't be available in b2.79", icon='ERROR') - if bpy.context.view_layer.objects.active 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 == 'HDR': - draw_panel_hdr_upload(self, context) - - elif ui_props.asset_type == 'MATERIAL': - # utils.label_multiline(layout, "Uploaded materials won't be available in b2.79", icon='ERROR') - - if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None: - draw_panel_material_upload(self, context) - else: - utils.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.') - - -class BlenderKitWelcomeOperator(bpy.types.Operator): - """Login online on BlenderKit webpage""" - - bl_idname = "wm.blenderkit_welcome" - bl_label = "Welcome to BlenderKit!" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - step: IntProperty( - name="step", - description="Tutorial Step", - default=0, - options={'SKIP_SAVE'} - ) - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout - if self.step == 0: - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - - # message = "BlenderKit connects from Blender to an online, " \ - # "community built shared library of models, " \ - # "materials, and brushes. " \ - # "Use addon preferences to set up where files will be saved in the Global directory setting." - # - # utils.label_multiline(layout, text=message, width=300) - - layout.template_icon(icon_value=self.img.preview.icon_id, scale=18) - - # utils.label_multiline(layout, text="\n Let's start by searching for some cool materials?", width=300) - op = layout.operator("wm.url_open", text='Watch Video Tutorial', icon='QUESTION') - op.url = paths.BLENDERKIT_MANUAL - - else: - message = "Operator Tutorial called with invalid step" - - def execute(self, context): - if self.step == 0: - # move mouse: - # bpy.context.window_manager.windows[0].cursor_warp(1000, 1000) - # show n-key sidebar (spaces[index] has to be found for view3d too: - # bpy.context.window_manager.windows[0].screen.areas[5].spaces[0].show_region_ui = False - ui_props = bpy.context.window_manager.blenderkitUI - # random_searches = [ - # ('MATERIAL', 'ice'), - # ('MODEL', 'car'), - # ('MODEL', 'vase'), - # ('MODEL', 'grass'), - # ('MODEL', 'plant'), - # ('MODEL', 'man'), - # ('MATERIAL', 'metal'), - # ('MATERIAL', 'wood'), - # ('MATERIAL', 'floor'), - # ('MATERIAL', 'bricks'), - # ] - # random_search = random.choice(random_searches) - # ui_props.asset_type = random_search[0] - ui_props.asset_type = 'MODEL' - - score_limit = 450 - if ui_props.asset_type == 'MATERIAL': - props = bpy.context.window_manager.blenderkit_mat - - elif ui_props.asset_type == 'MODEL': - props = bpy.context.window_manager.blenderkit_models - score_limit = 1000 - - props.search_keywords = '' # random_search[1] - props.search_keywords += f'+is_free:true+score_gte:{score_limit}+order:-created' # random_search[1] - # search.search() - return {'FINISHED'} - - def invoke(self, context, event): - wm = bpy.context.window_manager - img = utils.get_thumbnail('intro.jpg') - utils.img_to_preview(img, copy_original=True) - self.img = img - w, a, r = utils.get_largest_area(area_type='VIEW_3D') - if a is not None: - a.spaces.active.show_region_ui = True - - return wm.invoke_props_dialog(self, width=500) - - -def draw_asset_context_menu(layout, context, asset_data, from_panel=False): - ui_props = context.window_manager.blenderkitUI - - author_id = str(asset_data['author'].get('id')) - wm = bpy.context.window_manager - - layout.operator_context = 'INVOKE_DEFAULT' - - if from_panel: - op = layout.operator('wm.blenderkit_menu_rating_upload', text='Add Rating') - op.asset_name = asset_data['name'] - op.asset_id = asset_data['id'] - op.asset_type = asset_data['assetType'] - - if from_panel and wm.get('bkit authors') is not None and author_id is not None: - a = bpy.context.window_manager['bkit authors'].get(author_id) - if a is not None: - # utils.p('author:', a) - op = layout.operator('wm.url_open', text="Open Author's Website") - if a.get('aboutMeUrl') is not None: - op.url = a['aboutMeUrl'] - else: - op.url = paths.get_author_gallery_url(a['id']) - op = layout.operator('view3d.blenderkit_search', text="Show Assets By Author") - op.keywords = '' - op.author_id = author_id - - op = layout.operator('view3d.blenderkit_search', text='Search Similar') - op.esc = True - op.tooltip = 'Search for similar assets in the library' - # build search string from description and tags: - op.keywords = asset_data['name'] - if asset_data.get('description'): - op.keywords += ' ' + asset_data.get('description') + ' ' - op.keywords += ' '.join(asset_data.get('tags')) - - if asset_data.get('canDownload') != 0: - if len(bpy.context.selected_objects) > 0 and ui_props.asset_type == 'MODEL': - aob = bpy.context.active_object - if aob is None: - aob = bpy.context.selected_objects[0] - op = layout.operator('scene.blenderkit_download', text='Replace Active Models') - op.tooltip = "Replace all selected models with this one." - - # this checks if the menu got called from right-click in assetbar(then index is 0 - x) or - # from a panel(then replacement happens from the active model) - if from_panel: - # called from addon panel - op.asset_base_id = asset_data['assetBaseId'] - else: - op.asset_index = ui_props.active_index - - # op.asset_type = ui_props.asset_type - op.model_location = aob.location - op.model_rotation = aob.rotation_euler - op.target_object = aob.name - op.material_target_slot = aob.active_material_index - op.replace = True - op.replace_resolution = False - - # resolution replacement operator - # if asset_data['downloaded'] == 100: # only show for downloaded/used assets - # if ui_props.asset_type in ('MODEL', 'MATERIAL'): - # layout.menu(OBJECT_MT_blenderkit_resolution_menu.bl_idname) - - if ui_props.asset_type in ('MODEL', 'MATERIAL', 'HDR') and \ - utils.get_param(asset_data, 'textureResolutionMax') is not None and \ - utils.get_param(asset_data, 'textureResolutionMax') > 512: - - s = bpy.context.scene - - col = layout.column() - col.operator_context = 'INVOKE_DEFAULT' - - if from_panel: - # Called from addon panel - - if asset_data.get('resolution'): - op = col.operator('scene.blenderkit_download', text='Replace asset resolution') - op.asset_base_id = asset_data['assetBaseId'] - if asset_data['assetType'] == 'MODEL': - o = utils.get_active_model() - op.model_location = o.location - op.model_rotation = o.rotation_euler - op.target_object = o.name - op.material_target_slot = o.active_material_index - - elif asset_data['assetType'] == 'MATERIAL': - aob = bpy.context.active_object - op.model_location = aob.location - op.model_rotation = aob.rotation_euler - op.target_object = aob.name - op.material_target_slot = aob.active_material_index - op.replace_resolution = True - op.replace = False - - op.invoke_resolution = True - op.max_resolution = asset_data.get('max_resolution', - 0) # str(utils.get_param(asset_data, 'textureResolutionMax')) - - elif asset_data['assetBaseId'] in s['assets used'].keys() and asset_data['assetType'] != 'hdr': - # HDRs are excluded from replacement, since they are always replaced. - # called from asset bar: - op = col.operator('scene.blenderkit_download', text='Replace asset resolution') - - op.asset_index = ui_props.active_index - # op.asset_type = ui_props.asset_type - op.replace_resolution = True - op.replace = False - op.invoke_resolution = True - o = utils.get_active_model() - if o and o.get('asset_data'): - if o['asset_data']['assetBaseId'] == bpy.context.window_manager['search results'][ - ui_props.active_index]: - op.model_location = o.location - op.model_rotation = o.rotation_euler - else: - op.model_location = (0, 0, 0) - op.model_rotation = (0, 0, 0) - op.max_resolution = asset_data.get('max_resolution', - 0) # str(utils.get_param(asset_data, 'textureResolutionMax')) - # print('operator res ', resolution) - # op.resolution = resolution - - wm = bpy.context.window_manager - profile = wm.get('bkit profile') - if profile is not None: - # validation - - if author_id == str(profile['user']['id']) or utils.profile_is_validator(): - layout.label(text='Management tools:') - - row = layout.row() - row.operator_context = 'INVOKE_DEFAULT' - op = layout.operator('wm.blenderkit_fast_metadata', text='Edit Metadata', icon='GREASEPENCIL') - op.asset_id = asset_data['id'] - op.asset_type = asset_data['assetType'] - - if author_id == str(profile['user']['id']): - row.operator_context = 'EXEC_DEFAULT' - op = layout.operator('wm.blenderkit_url', text='Edit Metadata (browser)', icon='GREASEPENCIL') - op.url = paths.get_bkit_url() + paths.BLENDERKIT_USER_ASSETS + f"/{asset_data['assetBaseId']}/?edit#" - - row.operator_context = 'INVOKE_DEFAULT' - - if asset_data['assetType'] == 'model': - op = layout.operator('object.blenderkit_regenerate_thumbnail', text='Regenerate thumbnail') - op.asset_index = ui_props.active_index - elif asset_data['assetType'] == 'material': - op = layout.operator('object.blenderkit_regenerate_material_thumbnail', text='Regenerate thumbnail') - op.asset_index = ui_props.active_index - # op.asset_id = asset_data['id'] - # op.asset_type = asset_data['assetType'] - - if author_id == str(profile['user']['id']): - row = layout.row() - row.operator_context = 'INVOKE_DEFAULT' - op = row.operator('object.blenderkit_change_status', text='Delete') - op.asset_id = asset_data['id'] - op.state = 'deleted' - - if utils.profile_is_validator(): - layout.label(text='Dev Tools:') - - op = layout.operator('object.blenderkit_print_asset_debug', text='Print asset debug') - op.asset_id = asset_data['id'] - - -# def draw_asset_resolution_replace(self, context, resolution): -# layout = self.layout -# ui_props = bpy.context.window_manager.blenderkitUI -# -# op = layout.operator('scene.blenderkit_download', text=resolution) -# if ui_props.active_index == -3: -# # This happens if the command is called from addon panel -# o = utils.get_active_model() -# op.asset_base_id = o['asset_data']['assetBaseId'] -# -# else: -# op.asset_index = ui_props.active_index -# -# op.asset_type = ui_props.asset_type -# if len(bpy.context.selected_objects) > 0: # and ui_props.asset_type == 'MODEL': -# aob = bpy.context.active_object -# op.model_location = aob.location -# op.model_rotation = aob.rotation_euler -# op.target_object = aob.name -# op.material_target_slot = aob.active_material_index -# op.replace_resolution = True -# print('operator res ', resolution) -# op.resolution = resolution - - -# class OBJECT_MT_blenderkit_resolution_menu(bpy.types.Menu): -# bl_label = "Replace Asset Resolution" -# bl_idname = "OBJECT_MT_blenderkit_resolution_menu" -# -# def draw(self, context): -# ui_props = context.window_manager.blenderkitUI -# -# # sr = bpy.context.window_manager['search results'] -# -# # sr = bpy.context.window_manager['search results'] -# # asset_data = sr[ui_props.active_index] -# -# for k in resolutions.resolution_props_to_server.keys(): -# draw_asset_resolution_replace(self, context, k) - - -class OBJECT_MT_blenderkit_asset_menu(bpy.types.Menu): - bl_label = "Asset options:" - bl_idname = "OBJECT_MT_blenderkit_asset_menu" - - def draw(self, context): - ui_props = context.window_manager.blenderkitUI - - sr = bpy.context.window_manager['search results'] - asset_data = sr[ui_props.active_index] - draw_asset_context_menu(self.layout, context, asset_data, from_panel=False) - - -def numeric_to_str(s): - if s: - if s < 1: - s = str(round(s, 1)) - else: - s = str(round(s)) - else: - s = '-' - return s - - -def push_op_left(layout, strength=3): - for a in range(0, strength): - layout.label(text='') - - -def label_or_url_or_operator(layout, text='', tooltip='', url='', operator=None, operator_kwargs={}, icon_value=None, - icon=None): - '''automatically switch between different layout options for linking or tooltips''' - layout.emboss = 'NONE' - - if operator is not None: - if icon: - op = layout.operator(operator, text=text, icon=icon) - elif icon_value: - op = layout.operator(operator, text=text, icon_value=icon_value) - else: - op = layout.operator(operator, text=text) - for kwarg in operator_kwargs.keys(): - if type(operator_kwargs[kwarg]) == str: - quoatation = "'" - else: - quoatation = "" - exec(f"op.{kwarg} = {quoatation}{operator_kwargs[kwarg]}{quoatation}") - push_op_left(layout, strength=2) - - return - if url != '': - if icon: - op = layout.operator('wm.blenderkit_url', text=text, icon=icon) - elif icon_value: - op = layout.operator('wm.blenderkit_url', text=text, icon_value=icon_value) - else: - op = layout.operator('wm.blenderkit_url', text=text) - op.url = url - op.tooltip = tooltip - push_op_left(layout, strength=5) - - return - if tooltip != '': - if icon: - op = layout.operator('wm.blenderkit_tooltip', text=text, icon=icon) - elif icon_value: - op = layout.operator('wm.blenderkit_tooltip', text=text, icon_value=icon_value) - else: - op = layout.operator('wm.blenderkit_tooltip', text=text) - op.tooltip = tooltip - - # these are here to move the text to left, since operators can only center text by default - push_op_left(layout, strength=3) - return - if icon: - layout.label(text=text, icon=icon) - elif icon_value: - layout.label(text=text, icon_value=icon_value) - else: - layout.label(text=text) - - -class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties): - """Generate Cycles thumbnail for model assets""" - bl_idname = "wm.blenderkit_asset_popup" - bl_label = "BlenderKit asset popup" - - width = 800 - - @classmethod - def poll(cls, context): - return True - - def draw_menu(self, context, layout): - # layout = layout.column() - draw_asset_context_menu(layout, context, self.asset_data, from_panel=False) - - def draw_property(self, layout, left, right, icon=None, icon_value=None, url='', tooltip='', operator=None, - operator_kwargs={}): - right = str(right) - row = layout.row() - split = row.split(factor=0.35) - split.alignment = 'RIGHT' - split.label(text=left) - split = split.split() - split.alignment = 'LEFT' - # split for questionmark: - if url != '': - split = split.split(factor=0.6) - label_or_url_or_operator(split, text=right, tooltip=tooltip, url=url, operator=operator, - operator_kwargs=operator_kwargs, icon_value=icon_value, icon=icon) - # additional questionmark icon where it's important? - if url != '': - split = split.split() - op = split.operator('wm.blenderkit_url', text='', icon='QUESTION') - op.url = url - op.tooltip = tooltip - - def draw_asset_parameter(self, layout, key='', pretext='', do_search=False): - parameter = utils.get_param(self.asset_data, key) - if parameter == None: - return - if type(parameter) == int: - parameter = f"{parameter:,d}" - elif type(parameter) == float: - parameter = f"{parameter:,.1f}" - if do_search: - kwargs = { - 'esc': True, - 'keywords': f'+{key}:{parameter}', - 'tooltip': f'search by {parameter}', - } - self.draw_property(layout, pretext, parameter, operator='view3d.blenderkit_search', operator_kwargs=kwargs) - else: - self.draw_property(layout, pretext, parameter) - - def draw_description(self, layout, width=250): - if len(self.asset_data['description']) > 0: - box = layout.box() - box.scale_y = 0.4 - box.label(text='Description') - box.separator() - link_more = utils.label_multiline(box, self.asset_data['description'], width=width, max_lines=10) - if link_more: - row = box.row() - row.scale_y = 2 - op = row.operator('wm.blenderkit_url', text='See full description', icon='URL') - op.url = paths.get_asset_gallery_url(self.asset_data['assetBaseId']) - op.tooltip = 'Read full description on website' - box.separator() - - def draw_properties(self, layout, width=250): - - # if type(self.asset_data['parameters']) == list: - # mparams = utils.params_to_dict(self.asset_data['parameters']) - # else: - # mparams = self.asset_data['parameters'] - mparams = self.asset_data['dictParameters'] - - pcoll = icons.icon_collections["main"] - - box = layout.box() - - box.scale_y = 0.4 - box.label(text='Properties') - box.separator() - - if self.asset_data.get('license') == 'cc_zero': - t = 'CC Zero ' - icon = pcoll['cc0'] - - else: - t = 'Royalty free' - icon = pcoll['royalty_free'] - - self.draw_property(box, - 'License', t, - # icon_value=icon.icon_id, - url="https://www.blenderkit.com/docs/licenses/", - tooltip='All BlenderKit assets are available for commercial use. \n' \ - 'Click to read more about BlenderKit licenses on the website' - ) - - if upload.can_edit_asset(asset_data=self.asset_data): - icon = pcoll[self.asset_data['verificationStatus']] - verification_status_tooltips = { - 'uploading': "Your asset got stuck during upload. Probably, your file was too large " - "or your connection too slow or interrupting. If you have repeated issues, " - "please contact us and let us know, it might be a bug", - 'uploaded': "Your asset uploaded successfully. Yay! If it's public, " - "it's awaiting validation. If it's private, use it", - 'on_hold': "Your asset needs some (usually smaller) fixes, " - "so we can make it public for everybody." - " Please check your email to see the feedback " - "that we send to every creator personally", - 'rejected': "The asset has serious quality issues, " \ - "and it's probable that it might be good to start " \ - "all over again or try with something simpler. " \ - "You also get personal feedback into your e-mail, " \ - "since we believe that together, we can all learn " \ - "to become awesome 3D artists", - 'deleted': "You deleted this asset", - 'validated': "Your asset passed our validation process, " - "and is now available to BlenderKit users" - - } - self.draw_property(box, - 'Verification', - self.asset_data['verificationStatus'], - icon_value=icon.icon_id, - url="https://www.blenderkit.com/docs/validation-status/", - tooltip=verification_status_tooltips[self.asset_data['verificationStatus']] - - ) - # resolution/s - resolution = utils.get_param(self.asset_data, 'textureResolutionMax') - - if resolution is not None: - fs = self.asset_data['files'] - - ress = f"{int(round(resolution / 1024, 0))}K" - self.draw_property(box, 'Resolution', ress, - tooltip='Maximal resolution of textures in this asset.\n' \ - 'Most texture asset have also lower resolutions generated.\n' \ - 'Go to BlenderKit add-on import settings to set default resolution') - - if fs and len(fs) > 2 and utils.profile_is_validator(): - resolutions = '' - list.sort(fs, key=lambda f: f['fileType']) - for f in fs: - if f['fileType'].find('resolution') > -1: - resolutions += f['fileType'][11:] + ' ' - resolutions = resolutions.replace('_', '.') - self.draw_property(box, 'Generated', resolutions) - - self.draw_asset_parameter(box, key='designer', pretext='Designer', do_search=True) - self.draw_asset_parameter(box, key='manufacturer', pretext='Manufacturer', - do_search=True) - self.draw_asset_parameter(box, key='designCollection', pretext='Collection', do_search=True) - self.draw_asset_parameter(box, key='designVariant', pretext='Variant') - self.draw_asset_parameter(box, key='designYear', pretext='Design year') - - self.draw_asset_parameter(box, key='faceCount', pretext='Face count') - # self.draw_asset_parameter(box, key='thumbnailScale', pretext='Preview scale') - # self.draw_asset_parameter(box, key='purePbr', pretext='Pure PBR') - # self.draw_asset_parameter(box, key='productionLevel', pretext='Readiness') - # self.draw_asset_parameter(box, key='condition', pretext='Condition') - if utils.profile_is_validator(): - self.draw_asset_parameter(box, key='materialStyle', pretext='Style') - self.draw_asset_parameter(box, key='modelStyle', pretext='Style') - - if utils.get_param(self.asset_data, 'dimensionX'): - t = '%s×%s×%s m' % (utils.fmt_length(mparams['dimensionX']), - utils.fmt_length(mparams['dimensionY']), - utils.fmt_length(mparams['dimensionZ'])) - self.draw_property(box, 'Size', t) - if self.asset_data.get('filesSize'): - fs = self.asset_data['filesSize'] - fsmb = fs // (1024 * 1024) - fskb = fs % 1024 - if fsmb == 0: - self.draw_property(box, 'Original size', f'{fskb} KB') - else: - self.draw_property(box, 'Original size', f'{fsmb} MB') - # Tags section - # row = box.row() - # letters_on_row = 0 - # max_on_row = width / 10 - # for tag in self.asset_data['tags']: - # if tag in ('manifold', 'uv', 'non-manifold'): - # # these are sometimes accidentally stored in the lib - # continue - # - # # row.emboss='NONE' - # # we need to split wisely - # remaining_row = (max_on_row - letters_on_row) / max_on_row - # split_factor = (len(tag) / max_on_row) / remaining_row - # row = row.split(factor=split_factor) - # letters_on_row += len(tag) - # if letters_on_row > max_on_row: - # letters_on_row = len(tag) - # row = box.row() - # remaining_row = (max_on_row - letters_on_row) / max_on_row - # split_factor = (len(tag) / max_on_row) / remaining_row - # row = row.split(factor=split_factor) - # - # op = row.operator('wm') - # op = row.operator('view3d.blenderkit_search', text=tag) - # op.tooltip = f'Search items with tag {tag}' - # # build search string from description and tags: - # op.keywords = f'+tags:{tag}' - - # self.draw_property(box, 'Tags', self.asset_data['tags']) #TODO make them clickable! - - # Free/Full plan or private Access - plans_tooltip = 'BlenderKit has 2 plans:\n' \ - ' * Free plan - more than 50% of all assets\n' \ - ' * Full plan - unlimited access to everything\n' \ - 'Click to go to subscriptions page' - plans_link = 'https://www.blenderkit.com/plans/pricing/' - if self.asset_data['isPrivate']: - t = 'Private' - self.draw_property(box, 'Access', t, icon='LOCKED') - elif self.asset_data['isFree']: - t = 'Free plan' - icon = pcoll['free'] - self.draw_property(box, 'Access', t, - icon_value=icon.icon_id, - tooltip=plans_tooltip, - url=plans_link) - else: - t = 'Full plan' - icon = pcoll['full'] - self.draw_property(box, 'Access', t, - icon_value=icon.icon_id, - tooltip=plans_tooltip, - url=plans_link) - if utils.profile_is_validator(): - date = self.asset_data['created'][:10] - date = f"{date[8:10]}. {date[5:7]}. {date[:4]}" - self.draw_property(box, 'Created', date) - if utils.asset_from_newer_blender_version(self.asset_data): - # row = box.row() - box.alert = True - self.draw_property(box, - 'Blender version', - self.asset_data['sourceAppVersion'], - # icon='ERROR', - tooltip='Asset is from a newer Blender version and might work incorrectly in your scene', - ) - box.alert = False - box.separator() - - def draw_author_area(self, context, layout, width=330): - self.draw_author(context, layout, width=width) - - def draw_author(self, context, layout, width=330): - image_split = 0.25 - text_width = width - authors = bpy.context.window_manager['bkit authors'] - a = authors.get(self.asset_data['author']['id']) - if a is not None: # or a is '' or (a.get('gravatarHash') is not None and a.get('gravatarImg') is None): - - row = layout.row() - author_box = row.box() - author_box.scale_y = 0.6 # get text lines closer to each other - author_box.label(text='Author') # just one extra line to give spacing - if hasattr(self, 'gimg'): - - author_left = author_box.split(factor=image_split) - author_left.template_icon(icon_value=self.gimg.preview.icon_id, scale=7) - text_area = author_left.split() - text_width = int(text_width * (1 - image_split)) - else: - text_area = author_box - - author_right = text_area.column() - row = author_right.row() - col = row.column() - - utils.label_multiline(col, text=a['tooltip'], width=text_width) - # check if author didn't fill any data about himself and prompt him if that's the case - if utils.user_is_owner(asset_data=self.asset_data) and a.get('aboutMe') is not None and len( - a.get('aboutMe', '')) == 0: - row = col.row() - row.enabled = False - row.label(text='Please introduce yourself to the community!') - - op = col.operator('wm.blenderkit_url', text='Edit your profile') - op.url = 'https://www.blenderkit.com/profile' - op.tooltip = 'Edit your profile on BlenderKit webpage' - - button_row = author_box.row() - button_row.scale_y = 2.0 - - if a.get('aboutMeUrl') is not None: - url = a['aboutMeUrl'] - text = url - if len(url) > 45: - text = url[:45] + '...' - op = button_row.operator('wm.url_open', text=text) - op.url = url - button_row = author_box.row() - button_row.scale_y = 2.0 - - url = paths.get_author_gallery_url(a['id']) - text = "Author's Profile" - - op = button_row.operator('wm.url_open', text=text) - op.url = url - - op = button_row.operator('view3d.blenderkit_search', text="Find Assets By Author") - op.esc = True - op.keywords = '' - op.author_id = self.asset_data['author']['id'] - - def draw_thumbnail_box(self, layout, width=250): - layout.emboss = 'NORMAL' - - box_thumbnail = layout.box() - - box_thumbnail.scale_y = .4 - box_thumbnail.template_icon(icon_value=self.img.preview.icon_id, scale=width * .12) - - # op = row.operator('view3d.asset_drag_drop', text='Drag & Drop from here', depress=True) - # From here on, only ratings are drawn, which won't be displayed for private assets from now on. - - if not self.asset_data['isPrivate']: - row = box_thumbnail.row() - row.alignment = 'EXPAND' - - # display_ratings = can_display_ratings(self.asset_data) - rc = self.asset_data.get('ratingsCount') - show_rating_threshold = 0 - show_rating_prompt_threshold = 5 - - if rc: - rcount = min(rc['quality'], rc['workingHours']) - else: - rcount = 0 - if rcount >= show_rating_threshold or upload.can_edit_asset(asset_data=self.asset_data): - s = numeric_to_str(self.asset_data['score']) - q = numeric_to_str(self.asset_data['ratingsAverage'].get('quality')) - c = numeric_to_str(self.asset_data['ratingsAverage'].get('workingHours')) - else: - s = '-' - q = '-' - c = '-' - - pcoll = icons.icon_collections["main"] - - row.emboss = 'NONE' - op = row.operator('wm.blenderkit_tooltip', text=str(s), icon_value=pcoll['trophy'].icon_id) - op.tooltip = 'Asset score calculated from user ratings. \n\n' \ - 'Score = average quality × median complexity × 10*\n\n *Happiness multiplier' - row.label(text=' ') - - tooltip_extension = f'.\n\nRatings results are shown for assets with more than {show_rating_threshold} ratings' - op = row.operator('wm.blenderkit_tooltip', text=str(q), icon='SOLO_ON') - op.tooltip = f"Quality, average from {rc['quality']} ratings" \ - f"{tooltip_extension if rcount <= show_rating_threshold else ''}" - row.label(text=' ') - - op = row.operator('wm.blenderkit_tooltip', text=str(c), icon_value=pcoll['dumbbell'].icon_id) - op.tooltip = f"Complexity, median from {rc['workingHours']} ratings" \ - f"{tooltip_extension if rcount <= show_rating_threshold else ''}" - - if rcount <= show_rating_prompt_threshold: - box_thumbnail.alert = True - box_thumbnail.label(text=f"") - box_thumbnail.label( - text=f"This asset has only {rcount} rating{'' if rcount == 1 else 's'}, please rate.") - # box_thumbnail.label(text=f"Please rate this asset.") - - row = box_thumbnail.row() - row.alert = False - - row.scale_y = 3 - ui_props = bpy.context.window_manager.blenderkitUI - if self.asset_data.get('canDownload', True): - row.prop(ui_props, 'drag_init_button', icon='MOUSE_LMB_DRAG', text='Click / Drag from here', emboss=True) - else: - op = layout.operator('wm.blenderkit_url', text='Unlock this asset', icon='UNLOCKED') - op.url = paths.get_bkit_url() + '/get-blenderkit/' + self.asset_data['id'] + '/?from_addon=True' - - def draw_menu_desc_author(self, context, layout, width=330): - box = layout.column() - - box.emboss = 'NORMAL' - # left - tooltip & params - row = box.row() - split_factor = 0.7 - split_left = row.split(factor=split_factor) - col = split_left.column() - width_left = int(width * split_factor) - self.draw_description(col, width=width_left) - - self.draw_properties(col, width=width_left) - - # right - menu - split_right = split_left.split() - col = split_right.column() - self.draw_menu(context, col) - - # author - self.draw_author_area(context, box, width=width) - - # self.draw_author_area(context, box, width=width) - # - # col = box.column_flow(columns=2) - # self.draw_menu(context, col) - # - # - # # self.draw_description(box, width=int(width)) - # self.draw_properties(box, width=int(width)) - - # define enum flags - - def draw_titlebar(self, context, layout): - top_drag_bar = layout.box() - bcats = bpy.context.window_manager['bkit_categories'] - - cat_path = categories.get_category_path(bcats, - self.asset_data['category'])[1:] - - cat_path_names = categories.get_category_name_path(bcats, - self.asset_data['category'])[1:] - - aname = self.asset_data['displayName'] - aname = aname[0].upper() + aname[1:] - - if 1: - name_row = top_drag_bar.row() - # name_row = name_row.split(factor=0.5) - # name_row = name_row.column() - # name_row = name_row.row() - for i, c in enumerate(cat_path): - cat_name = cat_path_names[i] - op = name_row.operator('view3d.blenderkit_asset_bar_widget', text=cat_name + ' >', emboss=True) - op.do_search = True - op.keep_running = True - op.tooltip = f"Browse {cat_name} category" - op.category = c - # name_row.label(text='>') - - name_row.label(text=aname) - push_op_left(name_row, strength=3) - op = name_row.operator('view3d.close_popup_button', text='', icon='CANCEL') - - def draw_comment(self, context, layout, comment, width=330): - row = layout.row() - # print(comment) - if comment['level'] > 0: - split = row.split(factor=0.05 * comment['level']) - split.label(text='') - row = split.split() - box = row.box() - box.emboss = 'NORMAL' - row = box.row() - split = row.split(factor=0.8) - is_moderator = comment['userModerator'] - if is_moderator: - role_text = f" - moderator" - else: - role_text = "" - row = split.row() - row.enabled = False - row.label(text=f"{comment['submitDate']} - {comment['userName']}{role_text}") - removal = False - likes = 0 - dislikes = 0 - for l in comment['flags']: - if l['flag'] == 'like': - likes += 1 - if l['flag'] == 'dislike': - dislikes += 1 - if l['flag'] == 'removal': - removal = True - # row = box.row() - split1 = split.split() - # split1.emboss = 'NONE' - op = split1.operator('wm.blenderkit_like_comment', text=str(likes), icon='TRIA_UP') - op.asset_id = self.asset_data['assetBaseId'] - op.comment_id = comment['id'] - op.flag = 'like' - op = split1.operator('wm.blenderkit_like_comment', text=str(dislikes), icon='TRIA_DOWN') - op.asset_id = self.asset_data['assetBaseId'] - op.comment_id = comment['id'] - op.flag = 'dislike' - # op = split1.operator('wm.blenderkit_like_comment', text='report', icon='ERROR') - # op.asset_id = self.asset_data['assetBaseId'] - # op.comment_id = comment['id'] - # op.flag = 'removal' - if removal: - row.alert = True - row.label(text='', icon='ERROR') - - rows = utils.label_multiline(box, text=comment['comment'], width=width * (1 - 0.05 * comment['level'])) - - row = rows[-1] - split = row.split(factor=.8) - split.label(text='') - split = split.split() - op = split.operator('wm.blenderkit_url', text='Reply', icon='GREASEPENCIL') - op.tooltip = 'Open the browser on the asset page to comment' - op.url = paths.get_bkit_url() + f"/asset-gallery-detail/{self.asset_data['id']}/" - # box.label(text=str(comment['flags'])) - - def draw(self, context): - layout = self.layout - # top draggable bar with name of the asset - top_row = layout.row() - self.draw_titlebar(context, top_row) - # left side - row = layout.row(align=True) - split_ratio = 0.45 - split_left = row.split(factor=split_ratio) - left_column = split_left.column() - self.draw_thumbnail_box(left_column, width=int(self.width * split_ratio)) - # self.draw_description(left_column, width = int(self.width*split_ratio)) - # right split - split_right = split_left.split() - self.draw_menu_desc_author(context, split_right, width=int(self.width * (1 - split_ratio))) - - if not utils.user_is_owner(asset_data=self.asset_data): - # Draw ratings, but not for owners of assets - doesn't make sense. - ratings_box = layout.box() - ratings.draw_ratings_menu(self, context, ratings_box) - # else: - # ratings_box.label('Here you should find ratings, but you can not rate your own assets ;)') - - tip_box = layout.box() - tip_box.label(text=self.tip) - # comments - if utils.profile_is_validator(): - comments = bpy.context.window_manager.get('asset comments', {}) - self.comments = comments.get(self.asset_data['assetBaseId'], []) - if self.comments is not None: - for comment in self.comments: - self.draw_comment(context, layout, comment, width=self.width) - - def prefill_ratings(self): - # pre-fill ratings - ratings = ratings_utils.get_rating_local(self.asset_id) - if ratings and ratings.get('quality'): - self.rating_quality = ratings['quality'] - if ratings and ratings.get('working_hours'): - wh = int(ratings['working_hours']) - whs = str(wh) - if wh in self.possible_wh_values: - self.rating_work_hours_ui = whs - if wh < 6 and wh in self.possible_wh_values_1_5: - self.rating_work_hours_ui_1_5 = whs - if wh < 11 and wh in self.possible_wh_values_1_10: - self.rating_work_hours_ui_1_10 = whs - - def execute(self, context): - wm = context.window_manager - ui_props = context.window_manager.blenderkitUI - ui_props.draw_tooltip = False - sr = bpy.context.window_manager['search results'] - asset_data = sr[ui_props.active_index] - self.asset_data = asset_data - - self.img = ui.get_large_thumbnail_image(asset_data) - utils.img_to_preview(self.img, copy_original=True) - - self.asset_type = asset_data['assetType'] - self.asset_id = asset_data['id'] - # self.tex = utils.get_hidden_texture(self.img) - # self.tex.update_tag() - - authors = bpy.context.window_manager['bkit authors'] - a = authors.get(asset_data['author']['id']) - - if a is not None and a.get('gravatarImg') is not None: - self.gimg = utils.get_hidden_image(a['gravatarImg'], a['gravatarHash']) - - bl_label = asset_data['name'] - self.tip = search.get_random_tip() - self.tip = self.tip.replace('\n', '') - - # pre-fill ratings - self.prefill_ratings() - - # get comments - if utils.profile_is_validator(): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - api_key = user_preferences.api_key - comments = comments_utils.get_comments_local(asset_data['assetBaseId']) - # if comments is None: - comments_utils.get_comments_thread(asset_data['assetBaseId'], api_key) - comments = bpy.context.window_manager.get('asset comments', {}) - self.comments = comments.get(asset_data['assetBaseId'], []) - - return wm.invoke_popup(self, width=self.width) - - -class OBJECT_MT_blenderkit_login_menu(bpy.types.Menu): - bl_label = "BlenderKit login/signup:" - bl_idname = "OBJECT_MT_blenderkit_login_menu" - - def draw(self, context): - layout = self.layout - - # utils.label_multiline(layout, text=message) - draw_login_buttons(layout) - - -class SetCategoryOperator(bpy.types.Operator): - """Visit subcategory""" - bl_idname = "view3d.blenderkit_set_category" - bl_label = "BlenderKit Set Active Category" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - 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'} - - -class ClosePopupButton(bpy.types.Operator): - """Close popup window""" - bl_idname = "view3d.close_popup_button" - bl_label = "Close popup" - bl_options = {'REGISTER', 'INTERNAL'} - - @classmethod - def poll(cls, context): - return True - - def win_close(self): - VK_ESCAPE = 0x1B - ctypes.windll.user32.keybd_event(VK_ESCAPE) - return True - - def mouse_trick(self, context, x, y): - # import time - context.area.tag_redraw() - w = context.window - w.cursor_warp(w.x + 15, w.y + w.height - 15); - # time.sleep(.12) - w.cursor_warp(x, y); - context.area.tag_redraw() - - def invoke(self, context, event): - if platform.system() == 'Windows': - self.win_close() - else: - self.mouse_trick(context, event.mouse_x, event.mouse_y) - return {'FINISHED'} - - -class UrlPopupDialog(bpy.types.Operator): - """Generate Cycles thumbnail for model assets""" - bl_idname = "wm.blenderkit_url_dialog" - bl_label = "BlenderKit message:" - bl_options = {'REGISTER', 'INTERNAL'} - - url: bpy.props.StringProperty( - name="Url", - description="url", - default="") - - link_text: bpy.props.StringProperty( - name="Url", - description="url", - default="Go to website") - - message: bpy.props.StringProperty( - name="Text", - description="text", - default="") - - # @classmethod - # def poll(cls, context): - # return bpy.context.view_layer.objects.active is not None - - def draw(self, context): - layout = self.layout - utils.label_multiline(layout, text=self.message, width=300) - - layout.active_default = True - op = layout.operator("wm.url_open", text=self.link_text, icon='QUESTION') - if not utils.user_logged_in(): - utils.label_multiline(layout, - text='Already subscribed? You need to login to access your Full Plan.', - width=300) - - layout.operator_context = 'EXEC_DEFAULT' - layout.operator("wm.blenderkit_login", text="Login", - icon='URL').signup = False - op.url = self.url - - def execute(self, context): - return {'FINISHED'} - - def invoke(self, context, event): - wm = context.window_manager - return wm.invoke_props_dialog(self, width=300) - - -class LoginPopupDialog(bpy.types.Operator): - """Popup a dialog which enables the user to log in after being logged out automatically.""" - bl_idname = "wm.blenderkit_login_dialog" - bl_label = "BlenderKit login" - bl_options = {'REGISTER', 'INTERNAL'} - - message: bpy.props.StringProperty( - name="Message", - description="", - default="Your were logged out from BlenderKit. Please login again. ") - - # @classmethod - # def poll(cls, context): - # return bpy.context.view_layer.objects.active is not None - - def draw(self, context): - layout = self.layout - utils.label_multiline(layout, text=self.message) - - layout.active_default = True - op = layout.operator - op = layout.operator("wm.url_open", text=self.link_text, icon='QUESTION') - op.url = self.url - - 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) - - -def draw_panel_categories(self, context): - s = context.scene - ui_props = bpy.context.window_manager.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) - wm = bpy.context.window_manager - if wm.get('bkit_categories') == None: - return - 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_widget', text='freebies') - # op.free_only = True - - for c in cats['children']: - if c['assetCount'] > 0 or (utils.profile_is_validator() and user_preferences.categories_fix): - row = col.row(align=True) - if len(c['children']) > 0 and c['assetCount'] > 15 or ( - utils.profile_is_validator() and user_preferences.categories_fix): - row = row.split(factor=.8, align=True) - # row = split.split() - ctext = '%s (%i)' % (c['name'], c['assetCount']) - - preferences = bpy.context.preferences.addons['blenderkit'].preferences - if 1:#preferences.experimental_features: - op = row.operator('view3d.blenderkit_asset_bar_widget', text=ctext) - else: - op = row.operator('view3d.blenderkit_asset_bar', text=ctext) - op.do_search = True - op.keep_running = True - op.tooltip = f"Browse {c['name']} category" - op.category = c['slug'] - if len(c['children']) > 0 and c['assetCount'] > 15 or ( - utils.profile_is_validator() and user_preferences.categories_fix): - # 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 i, threaddata in enumerate(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)) + ' %') - op = row.operator('scene.blenderkit_download_kill', text='', icon='CANCEL') - op.thread_index = i - if tcom.passargs.get('retry_counter', 0) > 0: - row = layout.row() - row.label(text='failed. retrying ... ', icon='ERROR') - row.label(text=str(tcom.passargs["retry_counter"])) - - layout.separator() - - -def header_search_draw(self, context): - '''Top bar menu in 3D view''' - - if not utils.guard_from_crash(): - return; - - preferences = bpy.context.preferences.addons['blenderkit'].preferences - if not preferences.search_in_header: - return - if context.mode not in ('PAINT_TEXTURE', 'OBJECT', 'SCULPT'): - return - - layout = self.layout - s = bpy.context.scene - wm = bpy.context.window_manager - ui_props = bpy.context.window_manager.blenderkitUI - if ui_props.asset_type == 'MODEL': - props = wm.blenderkit_models - if ui_props.asset_type == 'MATERIAL': - props = wm.blenderkit_mat - if ui_props.asset_type == 'BRUSH': - props = wm.blenderkit_brush - if ui_props.asset_type == 'HDR': - props = wm.blenderkit_HDR - if ui_props.asset_type == 'SCENE': - props = wm.blenderkit_scene - - # the center snap menu is in edit and object mode if tool settings are off. - # if context.space_data.show_region_tool_header == True or context.mode[:4] not in ('EDIT', 'OBJE'): - # layout.separator_spacer() - layout.prop(ui_props, "asset_type", expand=True, icon_only=True, text='', icon='URL') - layout.prop(props, "search_keywords", text="", icon='VIEWZOOM') - draw_assetbar_show_hide(layout, props) - layout.popover(panel="VIEW3D_PT_blenderkit_categories", text="", icon='OUTLINER') - - pcoll = icons.icon_collections["main"] - - if props.use_filters: - icon_id = pcoll['filter_active'].icon_id - else: - icon_id = pcoll['filter'].icon_id - - if ui_props.asset_type == 'MODEL': - layout.popover(panel="VIEW3D_PT_blenderkit_advanced_model_search", text="", icon_value=icon_id) - - elif ui_props.asset_type == 'MATERIAL': - layout.popover(panel="VIEW3D_PT_blenderkit_advanced_material_search", text="", icon_value=icon_id) - elif ui_props.asset_type == 'HDR': - layout.popover(panel="VIEW3D_PT_blenderkit_advanced_HDR_search", text="", icon_value=icon_id) - - notifications = bpy.context.window_manager.get('bkit notifications') - if notifications is not None and notifications['count'] > 0: - layout.operator('wm.show_notifications', text="", icon_value=pcoll['bell'].icon_id) - # layout.popover(panel="VIEW3D_PT_blenderkit_notifications", text="", icon_value=pcoll['bell'].icon_id) - - if utils.profile_is_validator(): - search_props = utils.get_search_props() - layout.prop(search_props, 'search_verification_status', text='') - - -def ui_message(title, message): - def draw_message(self, context): - layout = self.layout - utils.label_multiline(layout, text=message, width=400) - - bpy.context.window_manager.popup_menu(draw_message, title=title, icon='INFO') - - -# We can store multiple preview collections here, -# however in this example we only store "main" -preview_collections = {} - -classes = ( - SetCategoryOperator, - VIEW3D_PT_blenderkit_profile, - VIEW3D_PT_blenderkit_login, - VIEW3D_PT_blenderkit_notifications, - VIEW3D_PT_blenderkit_unified, - VIEW3D_PT_blenderkit_advanced_model_search, - VIEW3D_PT_blenderkit_advanced_material_search, - VIEW3D_PT_blenderkit_advanced_HDR_search, - VIEW3D_PT_blenderkit_categories, - VIEW3D_PT_blenderkit_import_settings, - VIEW3D_PT_blenderkit_model_properties, - NODE_PT_blenderkit_material_properties, - # VIEW3D_PT_blenderkit_ratings, - VIEW3D_PT_blenderkit_downloads, - # OBJECT_MT_blenderkit_resolution_menu, - OBJECT_MT_blenderkit_asset_menu, - OBJECT_MT_blenderkit_login_menu, - AssetPopupCard, - UrlPopupDialog, - ClosePopupButton, - BlenderKitWelcomeOperator, - MarkNotificationRead, - LikeComment, - ShowNotifications, - NotificationOpenTarget, - MarkAllNotificationsRead, -) - - -def header_draw(self, context): - layout = self.layout - - self.draw_tool_settings(context) - - layout.separator_spacer() - header_search_draw(self,context) - layout.separator_spacer() - - self.draw_mode_settings(context) - - -def register_ui_panels(): - for c in classes: - bpy.utils.register_class(c) - - bpy.types.VIEW3D_HT_tool_header.draw = header_draw - # bpy.types.VIEW3D_HT_tool_header.append(header_search_draw) - # bpy.types.VIEW3D_MT_editor_menus.append(header_search_draw) - - -def unregister_ui_panels(): - bpy.types.VIEW3D_HT_tool_header.remove(header_search_draw) - # bpy.types.VIEW3D_MT_editor_menus.remove(header_search_draw) - for c in classes: - # print('unregister', c) - bpy.utils.unregister_class(c) diff --git a/blenderkit/upload.py b/blenderkit/upload.py deleted file mode 100644 index 3d8b705b..00000000 --- a/blenderkit/upload.py +++ /dev/null @@ -1,1387 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import asset_inspector, paths, utils, bg_blender, autothumb, version_checker, search, ui_panels, ui, \ - overrides, colors, rerequests, categories, upload_bg, tasks_queue, image_utils, asset_bar_op, reports - -import tempfile, os, subprocess, json, re - -import bpy -import requests -import threading -import sys - -BLENDERKIT_EXPORT_DATA_FILE = "data.json" - -from bpy.props import ( # TODO only keep the ones actually used when cleaning - EnumProperty, - BoolProperty, - StringProperty, -) -from bpy.types import ( - Operator, - Panel, - AddonPreferences, - PropertyGroup, - UIList -) - -licenses = ( - ('royalty_free', 'Royalty Free', 'royalty free commercial license'), - ('cc_zero', 'Creative Commons Zero', 'Creative Commons Zero'), -) - - -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 write_to_report(props, text): - props.report = props.report + ' - ' + text + '\n\n' - - -def check_missing_data_model(props): - autothumb.update_upload_model_preview(None, None) - if not props.has_thumbnail: - write_to_report(props, 'Add thumbnail:') - props.report += props.thumbnail_generating_state + '\n' - 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 check_missing_data_scene(props): - autothumb.update_upload_model_preview(None, None) - if not props.has_thumbnail: - write_to_report(props, 'Add thumbnail:') - props.report += props.thumbnail_generating_state + '\n' - if props.engine == 'NONE': - write_to_report(props, 'Set at least one rendering/output engine') - - -def check_missing_data_material(props): - autothumb.update_upload_material_preview(None, None) - 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 check_missing_data_brush(props): - autothumb.update_upload_brush_preview(None, None) - if not props.has_thumbnail: - write_to_report(props, 'Add thumbnail:') - props.report += props.thumbnail_generating_state - - -def check_missing_data(asset_type, props): - ''' - checks if user did everything allright for particular assets and notifies him back if not. - Parameters - ---------- - asset_type - props - - Returns - ------- - - ''' - props.report = '' - - if props.name == '': - write_to_report(props, f'Set {asset_type.lower()} name.\n' - f'It has to be in English and \n' - f'can not be longer than 40 letters.\n') - if len(props.name) > 40: - write_to_report(props, f'The name is too long. maximum is 40 letters') - - if props.is_private == 'PUBLIC': - - if len(props.description) < 20: - write_to_report(props, "The description is too short or empty. \n" - "Please write a description that describes \n " - "your asset as good as possible.\n" - "Description helps to bring your asset up\n in relevant search results. ") - if props.tags == '': - write_to_report(props, 'Write at least 3 tags.\n' - 'Tags help to bring your asset up in relevant search results.') - - if asset_type == 'MODEL': - check_missing_data_model(props) - if asset_type == 'SCENE': - check_missing_data_scene(props) - elif asset_type == 'MATERIAL': - check_missing_data_material(props) - elif asset_type == 'BRUSH': - check_missing_data_brush(props) - - if props.report != '': - props.report = f'Please fix these issues before {props.is_private.lower()} upload:\n\n' + props.report - - -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(caller=None, context=None, asset_type=None): - ''' - works though metadata from addom props and prepares it for upload to dicts. - Parameters - ---------- - caller - upload operator or none - context - context - asset_type - asset type in capitals (blender enum) - - Returns - ------- - export_ddta- all extra data that the process needs to upload and communicate with UI from a thread. - - eval_path_computing - string path to UI prop that denots if upload is still running - - eval_path_state - string path to UI prop that delivers messages about upload to ui - - eval_path - path to object holding upload data to be able to access it with various further commands - - models - in case of model upload, list of objects - - thumbnail_path - path to thumbnail file - - upload_data - asset_data generated from the ui properties - - ''' - 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, - - "procedural": props.is_procedural, - "nodeCount": props.node_count, - "textureCount": props.texture_count, - "megapixels": round(props.total_megapixels / 1000000), - # "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, - "procedural": props.is_procedural, - "nodeCount": props.node_count, - "textureCount": props.texture_count, - "megapixels": round(props.total_megapixels / 1000000), - - } - - 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 == 'HDR': - ui_props = bpy.context.window_manager.blenderkitUI - - # imagename = ui_props.hdr_upload_image - image = ui_props.hdr_upload_image # bpy.data.images.get(imagename) - if not image: - return None, None - - props = image.blenderkit - - image_utils.analyze_image_is_true_hdr(image) - - # props.name = brush.name - base, ext = os.path.splitext(image.filepath) - thumb_path = base + '.jpg' - export_data["thumbnail_path"] = bpy.path.abspath(thumb_path) - - export_data["hdr"] = str(image.name) - export_data["hdr_filepath"] = str(bpy.path.abspath(image.filepath)) - # export_data["thumbnail_path"] = bpy.path.abspath(brush.icon_filepath) - - eval_path_computing = "bpy.data.images['%s'].blenderkit.uploading" % image.name - eval_path_state = "bpy.data.images['%s'].blenderkit.upload_state" % image.name - eval_path = "bpy.data.images['%s']" % image.name - - # mat analytics happen here, since they don't take up any time... - - upload_params = { - "textureResolutionMax": props.texture_resolution_max, - "trueHDR": props.true_hdr - } - - upload_data = { - "assetType": 'hdr', - } - - 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) - - # caller can be upload operator, but also asset bar called from tooltip generator - if caller and caller.properties.main_file == True: - upload_data["name"] = props.name - upload_data["displayName"] = props.name - else: - upload_data["displayName"] = props.name - - upload_data["description"] = props.description - upload_data["tags"] = comma2array(props.tags) - # category is always only one value by a slug, that's why we go down to the lowest level and overwrite. - if props.category == '': - upload_data["category"] = asset_type.lower() - else: - upload_data["category"] = props.category - if props.subcategory != 'NONE': - upload_data["category"] = props.subcategory - if props.subcategory1 != 'NONE': - upload_data["category"] = props.subcategory1 - - upload_data["license"] = props.license - upload_data["isFree"] = props.is_free == 'FREE' - upload_data["isPrivate"] = props.is_private == 'PRIVATE' - upload_data["token"] = user_preferences.api_key - - upload_data['parameters'] = upload_params - - # if props.asset_base_id != '': - export_data['assetBaseId'] = props.asset_base_id - export_data['id'] = props.id - export_data['eval_path_computing'] = eval_path_computing - export_data['eval_path_state'] = eval_path_state - export_data['eval_path'] = eval_path - - return export_data, upload_data - - -def patch_individual_metadata(asset_id, metadata_dict, api_key): - upload_data = metadata_dict - url = paths.get_api_url() + 'assets/' + str(asset_id) + '/' - headers = utils.get_headers(api_key) - try: - r = rerequests.patch(url, json=upload_data, headers=headers, verify=True) # files = files, - except requests.exceptions.RequestException as e: - print(e) - return {'CANCELLED'} - return {'FINISHED'} - - -# class OBJECT_MT_blenderkit_fast_metadata_menu(bpy.types.Menu): -# bl_label = "Fast category change" -# bl_idname = "OBJECT_MT_blenderkit_fast_metadata_menu" -# -# def draw(self, context): -# layout = self.layout -# ui_props = context.window_manager.blenderkitUI -# -# # sr = bpy.context.window_manager['search results'] -# sr = bpy.context.window_manager['search results'] -# asset_data = sr[ui_props.active_index] -# categories = bpy.context.window_manager['bkit_categories'] -# wm = bpy.context.win -# for c in categories: -# if c['name'].lower() == asset_data['assetType']: -# for ch in c['children']: -# op = layout.operator('wm.blenderkit_fast_metadata', text = ch['name']) -# op = layout.operator('wm.blenderkit_fast_metadata', text = ch['name']) - - -def update_free_full(self, context): - if self.asset_type == 'material': - if self.free_full == 'FULL': - self.free_full = 'FREE' - ui_panels.ui_message(title="All BlenderKit materials are free", - message="Any material uploaded to BlenderKit is free." \ - " However, it can still earn money for the author," \ - " based on our fair share system. " \ - "Part of subscription is sent to artists based on usage by paying users.") - - -def can_edit_asset(active_index=-1, asset_data=None): - if active_index < 0 and not asset_data: - return False - profile = bpy.context.window_manager.get('bkit profile') - if profile is None: - return False - if utils.profile_is_validator(): - return True - if not asset_data: - sr = bpy.context.window_manager['search results'] - asset_data = dict(sr[active_index]) - if int(asset_data['author']['id']) == int(profile['user']['id']): - return True - return False - - -class FastMetadata(bpy.types.Operator): - """Edit metadata of the asset""" - bl_idname = "wm.blenderkit_fast_metadata" - bl_label = "Update metadata" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - asset_id: StringProperty( - name="Asset Base Id", - description="Unique name of the asset (hidden)", - default="" - ) - asset_type: StringProperty( - name="Asset Type", - description="Asset Type", - default="" - ) - name: StringProperty( - name="Name", - description="Main 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="", - ) - category: EnumProperty( - name="Category", - description="main category to put into", - items=categories.get_category_enums, - update=categories.update_category_enums - ) - subcategory: EnumProperty( - name="Subcategory", - description="main category to put into", - items=categories.get_subcategory_enums, - update=categories.update_subcategory_enums - ) - subcategory1: EnumProperty( - name="Subcategory", - description="main category to put into", - items=categories.get_subcategory1_enums - ) - license: EnumProperty( - items=licenses, - default='royalty_free', - description='License. Please read our help for choosing the right licenses', - ) - is_private: EnumProperty( - name="Thumbnail Style", - items=( - ('PRIVATE', 'Private', "You asset will be hidden to public. The private assets are limited by a quota."), - ('PUBLIC', 'Public', '"Your asset will go into the validation process automatically') - ), - description="If not marked private, your asset will go into the validation process automatically\n" - "Private assets are limited by quota", - default="PUBLIC", - ) - - free_full: EnumProperty( - name="Free or Full Plan", - items=( - ('FREE', 'Free', "You consent you want to release this asset as free for everyone"), - ('FULL', 'Full', 'Your asset will be in the full plan') - ), - description="Choose whether the asset should be free or in the Full Plan", - default="FULL", - update=update_free_full - ) - - #################### - - @classmethod - def poll(cls, context): - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - return True - - def draw(self, context): - layout = self.layout - # col = layout.column() - layout.label(text=self.message) - row = layout.row() - - layout.prop(self, 'category') - if self.category != 'NONE' and self.subcategory != 'NONE': - layout.prop(self, 'subcategory') - if self.subcategory != 'NONE' and self.subcategory1 != 'NONE': - enums = categories.get_subcategory1_enums(self, context) - if enums[0][0] != 'NONE': - layout.prop(self, 'subcategory1') - layout.prop(self, 'name') - layout.prop(self, 'description') - layout.prop(self, 'tags') - layout.prop(self, 'is_private', expand=True) - layout.prop(self, 'free_full', expand=True) - if self.is_private == 'PUBLIC': - layout.prop(self, 'license') - - def execute(self, context): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - props = bpy.context.window_manager.blenderkitUI - if self.subcategory1 != 'NONE': - category = self.subcategory1 - elif self.subcategory != 'NONE': - category = self.subcategory - else: - category = self.category - utils.update_tags(self, context) - - mdict = { - 'category': category, - 'displayName': self.name, - 'description': self.description, - 'tags': comma2array(self.tags), - 'isPrivate': self.is_private == 'PRIVATE', - 'isFree': self.free_full == 'FREE', - 'license': self.license, - } - - thread = threading.Thread(target=patch_individual_metadata, - args=(self.asset_id, mdict, user_preferences.api_key)) - thread.start() - tasks_queue.add_task((reports.add_report, (f'Uploading metadata for {self.name}. ' - f'Refresh search results to see that changes applied correctly.', 8,))) - - return {'FINISHED'} - - def invoke(self, context, event): - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - if ui_props.active_index > -1: - sr = bpy.context.window_manager['search results'] - asset_data = dict(sr[ui_props.active_index]) - else: - - active_asset = utils.get_active_asset_by_type(asset_type=self.asset_type) - asset_data = active_asset.get('asset_data') - - if not can_edit_asset(asset_data=asset_data): - return {'CANCELLED'} - self.asset_id = asset_data['id'] - self.asset_type = asset_data['assetType'] - cat_path = categories.get_category_path(bpy.context.window_manager['bkit_categories'], - asset_data['category']) - try: - if len(cat_path) > 1: - self.category = cat_path[1] - if len(cat_path) > 2: - self.subcategory = cat_path[2] - except Exception as e: - print(e) - - self.message = f"Fast edit metadata of {asset_data['name']}" - self.name = asset_data['displayName'] - self.description = asset_data['description'] - self.tags = ','.join(asset_data['tags']) - if asset_data['isPrivate']: - self.is_private = 'PRIVATE' - else: - self.is_private = 'PUBLIC' - - if asset_data['isFree']: - self.free_full = 'FREE' - else: - self.free_full = 'FULL' - self.license = asset_data['license'] - - wm = context.window_manager - - return wm.invoke_props_dialog(self, width=600) - - -def verification_status_change_thread(asset_id, state, api_key): - upload_data = { - "verificationStatus": state - } - url = paths.get_api_url() + 'assets/' + str(asset_id) + '/' - headers = utils.get_headers(api_key) - try: - r = rerequests.patch(url, json=upload_data, headers=headers, verify=True) # files = files, - except requests.exceptions.RequestException as e: - print(e) - return {'CANCELLED'} - return {'FINISHED'} - - -def get_upload_location(props): - ''' - not used by now, gets location of uploaded asset - potentially usefull if we draw a nice upload gizmo in viewport. - Parameters - ---------- - props - - Returns - ------- - - ''' - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - if ui_props.asset_type == 'MODEL': - if bpy.context.view_layer.objects.active 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.view_layer.objects.active 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 check_storage_quota(props): - if props.is_private == 'PUBLIC': - return True - - profile = bpy.context.window_manager.get('bkit profile') - if profile is None or profile.get('remainingPrivateQuota') is None: - preferences = bpy.context.preferences.addons['blenderkit'].preferences - adata = search.request_profile(preferences.api_key) - if adata is None: - props.report = 'Please log-in first.' - return False - search.write_profile(adata) - profile = adata - quota = profile['user'].get('remainingPrivateQuota') - if quota is None or quota > 0: - return True - props.report = 'Private storage quota exceeded.' - return False - - -def auto_fix(asset_type=''): - # this applies various procedures to ensure coherency in the database. - asset = utils.get_active_asset() - props = utils.get_upload_props() - if asset_type == 'MATERIAL': - overrides.ensure_eevee_transparency(asset) - asset.name = props.name - - -upload_threads = [] - - -class Uploader(threading.Thread): - ''' - Upload thread - - - first uploads metadata - - blender gets started to process the file if .blend is uploaded - - if files need to be uploaded, uploads them - - thumbnail goes first - - files get uploaded - - Returns - ------- - - ''' - - def __init__(self, upload_data=None, export_data=None, upload_set=None): - super(Uploader, self).__init__() - self.upload_data = upload_data - self.export_data = export_data - self.upload_set = upload_set - self._stop_event = threading.Event() - - def stop(self): - self._stop_event.set() - - def stopped(self): - return self._stop_event.is_set() - - def send_message(self, message): - message = str(message).replace("'", "") - - # this adds a UI report but also writes above the upload panel fields. - tasks_queue.add_task((reports.add_report, (message,))) - estring = f"{self.export_data['eval_path_state']} = '{message}'" - tasks_queue.add_task((exec, (estring,))) - - def end_upload(self, message): - estring = self.export_data['eval_path_computing'] + ' = False' - tasks_queue.add_task((exec, (estring,))) - self.send_message(message) - - def run(self): - # utils.pprint(upload_data) - self.upload_data['parameters'] = utils.dict_to_params( - self.upload_data['parameters']) # weird array conversion only for upload, not for tooltips. - - script_path = os.path.dirname(os.path.realpath(__file__)) - - # first upload metadata to server, so it can be saved inside the current file - url = paths.get_api_url() + 'assets/' - - headers = utils.get_headers(self.upload_data['token']) - - # self.upload_data['license'] = 'ovejajojo' - json_metadata = self.upload_data # json.dumps(self.upload_data, ensure_ascii=False).encode('utf8') - - # tasks_queue.add_task((reports.add_report, ('Posting metadata',))) - self.send_message('Posting metadata') - if self.export_data['assetBaseId'] == '': - try: - r = rerequests.post(url, json=json_metadata, headers=headers, verify=True, - immediate=True) # files = files, - - # tasks_queue.add_task((reports.add_report, ('uploaded metadata',))) - utils.p(r.text) - self.send_message('uploaded metadata') - - except requests.exceptions.RequestException as e: - print(e) - self.end_upload(e) - return {'CANCELLED'} - - else: - url += self.export_data['id'] + '/' - try: - if 'MAINFILE' in self.upload_set: - json_metadata["verificationStatus"] = "uploading" - r = rerequests.patch(url, json=json_metadata, headers=headers, verify=True, - immediate=True) # files = files, - self.send_message('uploaded metadata') - - # tasks_queue.add_task((reports.add_report, ('uploaded metadata',))) - # parse the request - # print('uploaded metadata') - print(r.text) - except requests.exceptions.RequestException as e: - print(e) - self.end_upload(e) - return {'CANCELLED'} - - if self.stopped(): - self.end_upload('Upload cancelled by user') - return - # props.upload_state = 'step 1' - if self.upload_set == ['METADATA']: - self.end_upload('Metadata posted successfully') - return {'FINISHED'} - try: - rj = r.json() - # utils.pprint(rj) - # if r.status_code not in (200, 201): - # if r.status_code == 401: - # ###reports.add_report(r.detail, 5, colors.RED) - # return {'CANCELLED'} - # if props.asset_base_id == '': - # props.asset_base_id = rj['assetBaseId'] - # props.id = rj['id'] - if self.export_data['assetBaseId'] == '': - self.export_data['assetBaseId'] = rj['assetBaseId'] - self.export_data['id'] = rj['id'] - # here we need to send asset ID's back into UI to be written in asset data. - estring = f"{self.export_data['eval_path']}.blenderkit.asset_base_id = '{rj['assetBaseId']}'" - tasks_queue.add_task((exec, (estring,))) - estring = f"{self.export_data['eval_path']}.blenderkit.id = '{rj['id']}'" - tasks_queue.add_task((exec, (estring,))) - # after that, the user's file needs to be saved to save the - # estring = f"bpy.ops.wm.save_as_mainfile(filepath={self.export_data['source_filepath']}, compress=False, copy=True)" - # tasks_queue.add_task((exec, (estring,))) - - self.upload_data['assetBaseId'] = self.export_data['assetBaseId'] - self.upload_data['id'] = self.export_data['id'] - - # props.uploading = True - - if 'MAINFILE' in self.upload_set: - if self.upload_data['assetType'] == 'hdr': - fpath = self.export_data['hdr_filepath'] - else: - fpath = os.path.join(self.export_data['temp_dir'], self.upload_data['assetBaseId'] + '.blend') - - clean_file_path = paths.get_clean_filepath() - - data = { - 'export_data': self.export_data, - 'upload_data': self.upload_data, - 'debug_value': self.export_data['debug_value'], - 'upload_set': self.upload_set, - } - datafile = os.path.join(self.export_data['temp_dir'], BLENDERKIT_EXPORT_DATA_FILE) - - with open(datafile, 'w', encoding='utf-8') as s: - json.dump(data, s, ensure_ascii=False, indent=4) - - self.send_message('preparing scene - running blender instance') - - proc = subprocess.run([ - self.export_data['binary_path'], - "--background", - "-noaudio", - clean_file_path, - "--python", os.path.join(script_path, "upload_bg.py"), - "--", datafile - ], bufsize=1, stdout=sys.stdout, stdin=subprocess.PIPE, creationflags=utils.get_process_flags()) - - if self.stopped(): - self.end_upload('Upload stopped by user') - return - - files = [] - if 'THUMBNAIL' in self.upload_set: - files.append({ - "type": "thumbnail", - "index": 0, - "file_path": self.export_data["thumbnail_path"] - }) - if 'MAINFILE' in self.upload_set: - files.append({ - "type": "blend", - "index": 0, - "file_path": fpath - }) - - if not os.path.exists(fpath): - self.send_message("File packing failed, please try manual packing first") - return {'CANCELLED'} - - self.send_message('Uploading files') - - uploaded = upload_bg.upload_files(self.upload_data, files) - - if uploaded: - # mark on server as uploaded - if 'MAINFILE' in self.upload_set: - confirm_data = { - "verificationStatus": "uploaded" - } - - url = paths.get_api_url() + 'assets/' - - headers = utils.get_headers(self.upload_data['token']) - - url += self.upload_data["id"] + '/' - - r = rerequests.patch(url, json=confirm_data, headers=headers, verify=True) # files = files, - - self.end_upload('Upload finished successfully') - else: - self.end_upload('Upload failed') - except Exception as e: - self.end_upload(e) - print(e) - return {'CANCELLED'} - - -def start_upload(self, context, asset_type, reupload, upload_set): - '''start upload process, by processing data, then start a thread that cares about the rest of the upload.''' - - # fix the name first - props = utils.get_upload_props() - - utils.name_update(props) - - storage_quota_ok = check_storage_quota(props) - if not storage_quota_ok: - self.report({'ERROR_INVALID_INPUT'}, props.report) - return {'CANCELLED'} - - location = get_upload_location(props) - props.upload_state = 'preparing upload' - - auto_fix(asset_type=asset_type) - - # do this for fixing long tags in some upload cases - props.tags = props.tags[:] - - # check for missing metadata - check_missing_data(asset_type, props) - # if previous check did find any problems then - if props.report != '': - return {'CANCELLED'} - - if not reupload: - props.asset_base_id = '' - props.id = '' - - export_data, upload_data = get_upload_data(caller=self, context=context, asset_type=asset_type) - - # check if thumbnail exists, generate for HDR: - if 'THUMBNAIL' in upload_set: - if asset_type == 'HDR': - image_utils.generate_hdr_thumbnail() - # get upload data because the image utils function sets true_hdr - export_data, upload_data = get_upload_data(caller=self, context=context, asset_type=asset_type) - - elif not os.path.exists(export_data["thumbnail_path"]): - props.upload_state = 'Thumbnail not found' - props.uploading = False - return {'CANCELLED'} - - if upload_set == {'METADATA'}: - props.upload_state = "Updating metadata. Please don't close Blender until upload finishes" - else: - props.upload_state = "Starting upload. Please don't close Blender until upload finishes" - props.uploading = True - - # save a copy of the file for processing. Only for blend files - basename, ext = os.path.splitext(bpy.data.filepath) - if not ext: - ext = ".blend" - export_data['temp_dir'] = tempfile.mkdtemp() - export_data['source_filepath'] = os.path.join(export_data['temp_dir'], "export_blenderkit" + ext) - if asset_type != 'HDR': - # if this isn't here, blender crashes. - bpy.context.preferences.filepaths.file_preview_type = 'NONE' - - bpy.ops.wm.save_as_mainfile(filepath=export_data['source_filepath'], compress=False, copy=True) - - export_data['binary_path'] = bpy.app.binary_path - export_data['debug_value'] = bpy.app.debug_value - - upload_thread = Uploader(upload_data=upload_data, export_data=export_data, upload_set=upload_set) - - upload_thread.start() - - upload_threads.append(upload_thread) - return {'FINISHED'} - - -asset_types = ( - ('MODEL', 'Model', 'Set of objects'), - ('SCENE', 'Scene', 'Scene'), - ('HDR', 'HDR', 'HDR image'), - ('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 UploadOperator(Operator): - """Tooltip""" - bl_idname = "object.blenderkit_upload" - bl_description = "Upload or re-upload asset + thumbnail + metadata" - - bl_label = "BlenderKit asset upload" - bl_options = {'REGISTER', 'INTERNAL'} - - # type of upload - model, material, textures, e.t.c. - asset_type: EnumProperty( - name="Type", - items=asset_types, - description="Type of upload", - default="MODEL", - ) - - reupload: BoolProperty( - name="reupload", - description="reupload but also draw so that it asks what to reupload", - default=False, - options={'SKIP_SAVE'} - ) - - metadata: BoolProperty( - name="metadata", - default=True, - options={'SKIP_SAVE'} - ) - - thumbnail: BoolProperty( - name="thumbnail", - default=False, - options={'SKIP_SAVE'} - ) - - main_file: BoolProperty( - name="main file", - default=False, - options={'SKIP_SAVE'} - ) - - @classmethod - def poll(cls, context): - return utils.uploadable_asset_poll() - - 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 - # if props.name_changed: - # # TODO: this needs to be replaced with new double naming scheme (metadata vs blend data) - # # print('has to reupload whole data, name has changed.') - # self.main_file = True - # props.name_changed = False - - upload_set = [] - if not self.reupload: - upload_set = ['METADATA', 'THUMBNAIL', 'MAINFILE'] - else: - if self.metadata: - upload_set.append('METADATA') - if self.thumbnail: - upload_set.append('THUMBNAIL') - if self.main_file: - upload_set.append('MAINFILE') - - # this is accessed later in get_upload_data and needs to be written. - # should pass upload_set all the way to it probably - if 'MAINFILE' in upload_set: - self.main_file = True - - result = start_upload(self, context, self.asset_type, self.reupload, upload_set=upload_set, ) - - if props.report != '': - # self.report({'ERROR_INVALID_INPUT'}, props.report) - self.report({'ERROR_INVALID_CONTEXT'}, props.report) - - return result - - def draw(self, context): - props = utils.get_upload_props() - layout = self.layout - - if self.reupload: - # layout.prop(self, 'metadata') - layout.prop(self, 'main_file') - layout.prop(self, 'thumbnail') - - if props.asset_base_id != '' and not self.reupload: - utils.label_multiline(layout, text="Really upload as new?\n" - "Do this only when you create\n" - "a new asset from an old one.\n" - "For updates of thumbnail or model use reupload.\n", - width=400, icon='ERROR') - - - if props.is_private == 'PUBLIC': - if self.asset_type == 'MODEL': - utils.label_multiline(layout, text='You marked the asset as public.\n' - 'This means it will be validated by our team.\n\n' - 'Please test your upload after it finishes:\n' - '- Open a new file\n' - '- Find the asset and download it\n' - '- Check if it snaps correctly to surfaces\n' - '- Check if it has all textures and renders as expected\n' - '- Check if it has correct size in world units (for models)' - , width=400) - elif self.asset_type == 'HDR': - if not props.true_hdr: - utils.label_multiline(layout, text="This image isn't HDR,\n" - "It has a low dynamic range.\n" - "BlenderKit library accepts 360 degree images\n" - "however the default filter setting for search\n" - "is to show only true HDR images\n" - , icon='ERROR', width=400) - - utils.label_multiline(layout, text='You marked the asset as public.\n' - 'This means it will be validated by our team.\n\n' - 'Please test your upload after it finishes:\n' - '- Open a new file\n' - '- Find the asset and download it\n' - '- Check if it works as expected\n' - , width=400) - else: - utils.label_multiline(layout, text='You marked the asset as public.\n' - 'This means it will be validated by our team.\n\n' - 'Please test your upload after it finishes:\n' - '- Open a new file\n' - '- Find the asset and download it\n' - '- Check if it works as expected\n' - , width=400) - - def invoke(self, context, event): - - if not utils.user_logged_in(): - ui_panels.draw_not_logged_in(self, message='To upload assets you need to login/signup.') - return {'CANCELLED'} - - if self.asset_type == 'HDR': - props = utils.get_upload_props() - # getting upload data for images ensures true_hdr check so users can be informed about their handling - # simple 360 photos or renders with LDR are hidden by default.. - export_data, upload_data = get_upload_data(asset_type='HDR') - - # if props.is_private == 'PUBLIC': - return context.window_manager.invoke_props_dialog(self) - # else: - # return self.execute(context) - - -class AssetDebugPrint(Operator): - """Change verification status""" - bl_idname = "object.blenderkit_print_asset_debug" - bl_description = "BlenderKit print asset data for debug purposes" - bl_label = "BlenderKit print asset data" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - # type of upload - model, material, textures, e.t.c. - asset_id: StringProperty( - name="asset id", - ) - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - preferences = bpy.context.preferences.addons['blenderkit'].preferences - - if not bpy.context.window_manager['search results']: - print('no search results found') - return {'CANCELLED'}; - # update status in search results for validator's clarity - sr = bpy.context.window_manager['search results'] - - result = None - for r in sr: - if r['id'] == self.asset_id: - result = r.to_dict() - if not result: - ad = bpy.context.active_object.get('asset_data') - if ad: - result = ad.to_dict() - if result: - t = bpy.data.texts.new(result['name']) - t.write(json.dumps(result, indent=4, sort_keys=True)) - print(json.dumps(result, indent=4, sort_keys=True)) - return {'FINISHED'} - - -class AssetVerificationStatusChange(Operator): - """Change verification status""" - bl_idname = "object.blenderkit_change_status" - bl_description = "Change asset status" - bl_label = "Change verification status" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - # type of upload - model, material, textures, e.t.c. - asset_id: StringProperty( - name="asset id", - ) - - state: StringProperty( - name="verification_status", - default='uploaded' - ) - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout - # if self.state == 'deleted': - layout.label(text='Really delete asset from BlenderKit online storage?') - # layout.prop(self, 'state') - - def execute(self, context): - preferences = bpy.context.preferences.addons['blenderkit'].preferences - - if not bpy.context.window_manager['search results']: - return {'CANCELLED'}; - # update status in search results for validator's clarity - sr = bpy.context.window_manager['search results'] - - for r in sr: - if r['id'] == self.asset_id: - r['verificationStatus'] = self.state - - thread = threading.Thread(target=verification_status_change_thread, - args=(self.asset_id, self.state, preferences.api_key)) - thread.start() - if asset_bar_op.asset_bar_operator is not None: - asset_bar_op.asset_bar_operator.update_layout(context, None) - return {'FINISHED'} - - def invoke(self, context, event): - # print(self.state) - if self.state == 'deleted': - wm = context.window_manager - return wm.invoke_props_dialog(self) - return {'RUNNING_MODAL'} - - -def register_upload(): - bpy.utils.register_class(UploadOperator) - bpy.utils.register_class(FastMetadata) - bpy.utils.register_class(AssetDebugPrint) - bpy.utils.register_class(AssetVerificationStatusChange) - - -def unregister_upload(): - bpy.utils.unregister_class(UploadOperator) - bpy.utils.unregister_class(FastMetadata) - bpy.utils.unregister_class(AssetDebugPrint) - bpy.utils.unregister_class(AssetVerificationStatusChange) diff --git a/blenderkit/upload_bg.py b/blenderkit/upload_bg.py deleted file mode 100644 index cfcb8224..00000000 --- a/blenderkit/upload_bg.py +++ /dev/null @@ -1,187 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - - -from blenderkit import paths, append_link, bg_blender, utils, rerequests, tasks_queue, ui, reports - -import sys, json, os, time -import requests -import logging - -import bpy - -BLENDERKIT_EXPORT_DATA = sys.argv[-1] - - -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 - tasks_queue.add_task((reports.add_report, (f"Uploading {self.report_name} {percent}%",))) - - # 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_file(upload_data, f): - headers = utils.get_headers(upload_data['token']) - version_id = upload_data['id'] - - message = f"uploading {f['type']} {os.path.basename(f['file_path'])}" - tasks_queue.add_task((reports.add_report, (message,))) - - upload_info = { - 'assetId': version_id, - 'fileType': f['type'], - 'fileIndex': f['index'], - 'originalFilename': os.path.basename(f['file_path']) - } - upload_create_url = paths.get_api_url() + 'uploads/' - upload = rerequests.post(upload_create_url, json=upload_info, headers=headers, verify=True) - upload = upload.json() - # - chunk_size = 1024 * 1024 * 2 - # utils.pprint(upload) - # file gets uploaded here: - uploaded = False - # s3 upload is now the only option - for a in range(0, 5): - 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) - - if 250 > upload_response.status_code > 199: - uploaded = True - upload_done_url = paths.get_api_url() + 'uploads_s3/' + upload['id'] + '/upload-file/' - upload_response = rerequests.post(upload_done_url, headers=headers, verify=True) - # print(upload_response) - # print(upload_response.text) - tasks_queue.add_task((reports.add_report, (f"Finished file upload: {os.path.basename(f['file_path'])}",))) - return True - else: - print(upload_response.text) - message = f"Upload failed, retry. File : {f['type']} {os.path.basename(f['file_path'])}" - tasks_queue.add_task((reports.add_report, (message,))) - - except Exception as e: - print(e) - message = f"Upload failed, retry. File : {f['type']} {os.path.basename(f['file_path'])}" - tasks_queue.add_task((reports.add_report, (message,))) - time.sleep(1) - - # confirm single file upload to bkit server - - - - - return False - - -def upload_files(upload_data, files): - '''uploads several files in one run''' - uploaded_all = True - for f in files: - uploaded = upload_file(upload_data, f) - if not uploaded: - uploaded_all = False - tasks_queue.add_task((reports.add_report, (f"Uploaded all files for asset {upload_data['name']}",))) - return uploaded_all - - -if __name__ == "__main__": - try: - # bg_blender.progress('preparing scene - append data') - with open(BLENDERKIT_EXPORT_DATA, 'r',encoding='utf-8') 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'] - - bpy.data.scenes.new('upload') - for s in bpy.data.scenes: - if s.name != 'upload': - bpy.data.scenes.remove(s) - - if upload_data['assetType'] == 'model': - obnames = export_data['models'] - main_source, allobs = append_link.append_objects(file_name=export_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) - elif upload_data['assetType'] == 'scene': - sname = export_data['scene'] - main_source = append_link.append_scene(file_name=export_data['source_filepath'], - scenename=sname) - bpy.data.scenes.remove(bpy.data.scenes['upload']) - main_source.name = sname - elif upload_data['assetType'] == 'material': - matname = export_data['material'] - main_source = append_link.append_material(file_name=export_data['source_filepath'], matname=matname) - - elif upload_data['assetType'] == 'brush': - brushname = export_data['brush'] - main_source = append_link.append_brush(file_name=export_data['source_filepath'], brushname=brushname) - - bpy.ops.file.pack_all() - - main_source.blenderkit.uploading = False - #write ID here. - main_source.blenderkit.asset_base_id = export_data['assetBaseId'] - main_source.blenderkit.id = export_data['id'] - - fpath = os.path.join(export_data['temp_dir'], upload_data['assetBaseId'] + '.blend') - - #if this isn't here, blender crashes. - bpy.context.preferences.filepaths.file_preview_type = 'NONE' - - bpy.ops.wm.save_as_mainfile(filepath=fpath, compress=True, copy=False) - os.remove(export_data['source_filepath']) - - - except Exception as e: - print(e) - # bg_blender.progress(e) - sys.exit(1) diff --git a/blenderkit/utils.py b/blenderkit/utils.py deleted file mode 100644 index 5bdace75..00000000 --- a/blenderkit/utils.py +++ /dev/null @@ -1,986 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -from blenderkit import paths, rerequests, image_utils - -import bpy -from mathutils import Vector -import json -import os -import sys -import shutil -import logging -import traceback -import inspect -import datetime - -bk_logger = logging.getLogger('blenderkit') - -ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000 -BELOW_NORMAL_PRIORITY_CLASS = 0x00004000 -HIGH_PRIORITY_CLASS = 0x00000080 -IDLE_PRIORITY_CLASS = 0x00000040 -NORMAL_PRIORITY_CLASS = 0x00000020 -REALTIME_PRIORITY_CLASS = 0x00000100 - -supported_material_click = ('MESH', 'CURVE', 'META', 'FONT', 'SURFACE', 'VOLUME', 'GPENCIL') -supported_material_drag = ('MESH', 'CURVE', 'META', 'FONT', 'SURFACE', 'VOLUME', 'GPENCIL') - - -# supported_material_drag = ('MESH') - - -def experimental_enabled(): - preferences = bpy.context.preferences.addons['blenderkit'].preferences - return preferences.experimental_features - - -def get_process_flags(): - flags = BELOW_NORMAL_PRIORITY_CLASS - if sys.platform != 'win32': # TODO test this on windows - flags = 0 - return flags - - -def activate(ob): - bpy.ops.object.select_all(action='DESELECT') - ob.select_set(True) - bpy.context.view_layer.objects.active = ob - -def selection_get(): - aob = bpy.context.view_layer.objects.active - selobs = bpy.context.view_layer.objects.selected[:] - return (aob, selobs) - - -def selection_set(sel): - bpy.ops.object.select_all(action='DESELECT') - bpy.context.view_layer.objects.active = sel[0] - for ob in sel[1]: - ob.select_set(True) - - -def get_active_model(): - if bpy.context.view_layer.objects.active is not None: - ob = bpy.context.view_layer.objects.active - while ob.parent is not None: - ob = ob.parent - return ob - return None - - -def get_active_HDR(): - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - image = ui_props.hdr_upload_image - return image - - -def get_selected_models(): - ''' - Detect all hierarchies that contain asset data from selection. Only parents that have actual ['asset data'] get returned - Returns - list of objects containing asset data. - - ''' - obs = bpy.context.selected_objects[:] - done = {} - parents = [] - for ob in obs: - if ob not in done: - while ob.parent is not None and ob not in done and ob.blenderkit.asset_base_id == '' and ob.instance_collection is None: - done[ob] = True - ob = ob.parent - - if ob not in parents and ob not in done: - if ob.blenderkit.name != '' or ob.instance_collection is not None: - parents.append(ob) - done[ob] = True - - # if no blenderkit - like objects were found, use the original selection. - if len(parents) == 0: - parents = obs - return parents - - -def get_selected_replace_adepts(): - ''' - Detect all hierarchies that contain either asset data from selection, or selected objects themselves. - Returns - list of objects for replacement. - - ''' - obs = bpy.context.selected_objects[:] - done = {} - parents = [] - for selected_ob in obs: - ob = selected_ob - if ob not in done: - while ob.parent is not None and ob not in done and ob.blenderkit.asset_base_id == '' and ob.instance_collection is None: - done[ob] = True - # print('step,',ob.name) - ob = ob.parent - - # print('fin', ob.name) - if ob not in parents and ob not in done: - if ob.blenderkit.name != '' or ob.instance_collection is not None: - parents.append(ob) - - done[ob] = True - # print(parents) - # if no blenderkit - like objects were found, use the original selection. - if len(parents) == 0: - parents = obs - pprint('replace adepts') - pprint(str(parents)) - return parents - - -def exclude_collection(name, state=True): - ''' - Set the exclude state of collection - Parameters - ---------- - name - name of collection - state - default True. - - Returns - ------- - None - ''' - vl = bpy.context.view_layer.layer_collection - cc = [vl] - found = False - while len(cc) > 0 and not found: - c = cc.pop() - if c.name == name: - c.exclude = state - found = True - cc.extend(c.children) - -def get_search_props(): - scene = bpy.context.scene - wm = bpy.context.window_manager - if scene is None: - return; - uiprops = bpy.context.window_manager.blenderkitUI - props = None - if uiprops.asset_type == 'MODEL': - if not hasattr(wm, 'blenderkit_models'): - return; - props = wm.blenderkit_models - if uiprops.asset_type == 'SCENE': - if not hasattr(wm, 'blenderkit_scene'): - return; - props = wm.blenderkit_scene - if uiprops.asset_type == 'HDR': - if not hasattr(wm, 'blenderkit_HDR'): - return; - props = wm.blenderkit_HDR - if uiprops.asset_type == 'MATERIAL': - if not hasattr(wm, 'blenderkit_mat'): - return; - props = wm.blenderkit_mat - - if uiprops.asset_type == 'TEXTURE': - if not hasattr(wm, 'blenderkit_tex'): - return; - # props = scene.blenderkit_tex - - if uiprops.asset_type == 'BRUSH': - if not hasattr(wm, 'blenderkit_brush'): - return; - props = wm.blenderkit_brush - return props - - -def get_active_asset_by_type(asset_type='model'): - asset_type = asset_type.lower() - if asset_type == 'model': - if bpy.context.view_layer.objects.active is not None: - ob = get_active_model() - return ob - if asset_type == 'scene': - return bpy.context.scene - if asset_type == 'hdr': - return get_active_HDR() - if asset_type == 'material': - if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None: - return bpy.context.active_object.active_material - if asset_type == 'texture': - return None - if asset_type == 'brush': - b = get_active_brush() - if b is not None: - return b - return None - - -def get_active_asset(): - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - if ui_props.asset_type == 'MODEL': - if bpy.context.view_layer.objects.active is not None: - ob = get_active_model() - return ob - if ui_props.asset_type == 'SCENE': - return bpy.context.scene - if ui_props.asset_type == 'HDR': - return get_active_HDR() - elif ui_props.asset_type == 'MATERIAL': - if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None: - return bpy.context.active_object.active_material - elif ui_props.asset_type == 'TEXTURE': - return None - elif ui_props.asset_type == 'BRUSH': - b = get_active_brush() - if b is not None: - return b - return None - - -def get_upload_props(): - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - if ui_props.asset_type == 'MODEL': - if bpy.context.view_layer.objects.active is not None: - ob = get_active_model() - return ob.blenderkit - if ui_props.asset_type == 'SCENE': - s = bpy.context.scene - return s.blenderkit - if ui_props.asset_type == 'HDR': - - hdr = ui_props.hdr_upload_image # bpy.data.images.get(ui_props.hdr_upload_image) - if not hdr: - return None - return hdr.blenderkit - elif ui_props.asset_type == 'MATERIAL': - if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None: - return bpy.context.active_object.active_material.blenderkit - elif ui_props.asset_type == 'TEXTURE': - return None - elif ui_props.asset_type == 'BRUSH': - b = get_active_brush() - if b is not None: - return b.blenderkit - return None - - -def previmg_name(index, fullsize=False): - if not fullsize: - return '.bkit_preview_' + str(index).zfill(3) - else: - return '.bkit_preview_full_' + str(index).zfill(3) - - -def get_active_brush(): - context = bpy.context - brush = None - if context.sculpt_object: - brush = context.tool_settings.sculpt.brush - elif context.image_paint_object: # could be just else, but for future possible more types... - brush = context.tool_settings.image_paint.brush - return brush - - -def load_prefs(): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - # if user_preferences.api_key == '': - fpath = paths.BLENDERKIT_SETTINGS_FILENAME - if os.path.exists(fpath): - try: - with open(fpath, 'r', encoding='utf-8') as s: - prefs = json.load(s) - user_preferences.api_key = prefs.get('API_key', '') - user_preferences.global_dir = prefs.get('global_dir', paths.default_global_dict()) - user_preferences.api_key_refresh = prefs.get('API_key_refresh', '') - except Exception as e: - print('failed to read addon preferences.') - print(e) - os.remove(fpath) - - -def save_prefs(self, context): - # first check context, so we don't do this on registration or blender startup - if not bpy.app.background: # (hasattr kills blender) - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - # we test the api key for length, so not a random accidentally typed sequence gets saved. - lk = len(user_preferences.api_key) - if 0 < lk < 25: - # reset the api key in case the user writes some nonsense, e.g. a search string instead of the Key - user_preferences.api_key = '' - props = get_search_props() - props.report = 'Login failed. Please paste a correct API Key.' - - prefs = { - 'API_key': user_preferences.api_key, - 'API_key_refresh': user_preferences.api_key_refresh, - 'global_dir': user_preferences.global_dir, - } - try: - fpath = paths.BLENDERKIT_SETTINGS_FILENAME - if not os.path.exists(paths._presets): - os.makedirs(paths._presets) - with open(fpath, 'w', encoding='utf-8') as s: - json.dump(prefs, s, ensure_ascii=False, indent=4) - except Exception as e: - print(e) - - -def uploadable_asset_poll(): - '''returns true if active asset type can be uploaded''' - ui_props = bpy.context.window_manager.blenderkitUI - if ui_props.asset_type == 'MODEL': - return bpy.context.view_layer.objects.active is not None - if ui_props.asset_type == 'MATERIAL': - return bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None - if ui_props.asset_type == 'HDR': - return ui_props.hdr_upload_image is not None - return True - - -def get_hidden_texture(name, force_reload=False): - t = bpy.data.textures.get(name) - if t is None: - t = bpy.data.textures.new(name, 'IMAGE') - if not t.image or t.image.name != name: - img = bpy.data.images.get(name) - if img: - t.image = img - return t - - -def img_to_preview(img, copy_original=False): - if bpy.app.version[0] >= 3: - img.preview_ensure() - if not copy_original: - return; - if img.preview.image_size != img.size: - img.preview.image_size = (img.size[0], img.size[1]) - img.preview.image_pixels_float = img.pixels[:] - # img.preview.icon_size = (img.size[0], img.size[1]) - # img.preview.icon_pixels_float = img.pixels[:] - - -def get_hidden_image(tpath, bdata_name, force_reload=False, colorspace='sRGB'): - if bdata_name[0] == '.': - hidden_name = bdata_name - else: - hidden_name = '.%s' % bdata_name - img = bpy.data.images.get(hidden_name) - - if tpath.startswith('//'): - tpath = bpy.path.abspath(tpath) - - if img == None or (img.filepath != tpath): - if tpath.startswith('//'): - tpath = bpy.path.abspath(tpath) - if not os.path.exists(tpath) or os.path.isdir(tpath): - tpath = paths.get_addon_thumbnail_path('thumbnail_notready.jpg') - - if img is None: - img = bpy.data.images.load(tpath) - img_to_preview(img) - 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() - img_to_preview(img) - image_utils.set_colorspace(img, colorspace) - - elif force_reload: - if img.packed_file is not None: - img.unpack(method='USE_ORIGINAL') - img.reload() - img_to_preview(img) - image_utils.set_colorspace(img, colorspace) - - return img - - -def get_thumbnail(name): - p = paths.get_addon_thumbnail_path(name) - name = '.%s' % name - img = bpy.data.images.get(name) - if img == None: - img = bpy.data.images.load(p) - image_utils.set_colorspace(img, 'sRGB') - img.name = name - img.name = name - - return img - - -def files_size_to_text(size): - fsmb = size / (1024 * 1024) - fskb = size % 1024 - if fsmb == 0: - return f'{round(fskb)}KB' - else: - return f'{round(fsmb, 1)}MB' - - -def get_brush_props(context): - brush = get_active_brush() - if brush is not None: - return brush.blenderkit - return None - - -def p(text, text1='', text2='', text3='', text4='', text5='', level='DEBUG'): - '''debug printing depending on blender's debug value''' - - if 1: # bpy.app.debug_value != 0: - # print('-----BKit debug-----\n') - # traceback.print_stack() - texts = [text1, text2, text3, text4, text5] - text = str(text) - for t in texts: - if t != '': - text += ' ' + str(t) - - bk_logger.debug(text) - # print('---------------------\n') - - -def copy_asset(fp1, fp2): - '''synchronizes the asset between folders, including it's texture subdirectories''' - if 1: - bk_logger.debug('copy asset') - bk_logger.debug(fp1 + ' ' + fp2) - if not os.path.exists(fp2): - shutil.copyfile(fp1, fp2) - bk_logger.debug('copied') - source_dir = os.path.dirname(fp1) - target_dir = os.path.dirname(fp2) - for subdir in os.scandir(source_dir): - if not subdir.is_dir(): - continue - target_subdir = os.path.join(target_dir, subdir.name) - if os.path.exists(target_subdir): - continue - bk_logger.debug(str(subdir) + ' ' + str(target_subdir)) - shutil.copytree(subdir, target_subdir) - bk_logger.debug('copied') - - # except Exception as e: - # print('BlenderKit failed to copy asset') - # print(fp1, fp2) - # print(e) - - -def pprint(data, data1=None, data2=None, data3=None, data4=None): - '''pretty print jsons''' - p(json.dumps(data, indent=4, sort_keys=True)) - - -def get_hierarchy(ob): - '''get all objects in a tree''' - obs = [] - doobs = [ob] - # pprint('get hierarchy') - pprint(ob.name) - while len(doobs) > 0: - o = doobs.pop() - doobs.extend(o.children) - obs.append(o) - return obs - - -def select_hierarchy(ob, state=True): - obs = get_hierarchy(ob) - for ob in obs: - ob.select_set(state) - return obs - - -def delete_hierarchy(ob): - obs = get_hierarchy(ob) - bpy.ops.object.delete({"selected_objects": obs}) - - -def get_bounds_snappable(obs, use_modifiers=False): - # progress('getting bounds of object(s)') - parent = obs[0] - while parent.parent is not None: - parent = parent.parent - maxx = maxy = maxz = -10000000 - minx = miny = minz = 10000000 - - s = bpy.context.scene - - obcount = 0 # calculates the mesh obs. Good for non-mesh objects - matrix_parent = parent.matrix_world - for ob in obs: - # bb=ob.bound_box - mw = ob.matrix_world - subp = ob.parent - # while parent.parent is not None: - # mw = - - if ob.type == 'MESH' or ob.type == 'CURVE': - # If to_mesh() works we can use it on curves and any other ob type almost. - # disabled to_mesh for 2.8 by now, not wanting to use dependency graph yet. - depsgraph = bpy.context.evaluated_depsgraph_get() - - object_eval = ob.evaluated_get(depsgraph) - if ob.type == 'CURVE': - mesh = object_eval.to_mesh() - else: - mesh = object_eval.data - - # to_mesh(context.depsgraph, apply_modifiers=self.applyModifiers, calc_undeformed=False) - obcount += 1 - if mesh is not None: - for c in mesh.vertices: - coord = c.co - parent_coord = matrix_parent.inverted() @ mw @ Vector( - (coord[0], coord[1], coord[2])) # copy this when it works below. - minx = min(minx, parent_coord.x) - miny = min(miny, parent_coord.y) - minz = min(minz, parent_coord.z) - maxx = max(maxx, parent_coord.x) - maxy = max(maxy, parent_coord.y) - maxz = max(maxz, parent_coord.z) - # bpy.data.meshes.remove(mesh) - if ob.type == 'CURVE': - object_eval.to_mesh_clear() - - if obcount == 0: - minx, miny, minz, maxx, maxy, maxz = 0, 0, 0, 0, 0, 0 - - minx *= parent.scale.x - maxx *= parent.scale.x - miny *= parent.scale.y - maxy *= parent.scale.y - minz *= parent.scale.z - maxz *= parent.scale.z - - return minx, miny, minz, maxx, maxy, maxz - - -def get_bounds_worldspace(obs, use_modifiers=False): - # progress('getting bounds of object(s)') - s = bpy.context.scene - maxx = maxy = maxz = -10000000 - minx = miny = minz = 10000000 - obcount = 0 # calculates the mesh obs. Good for non-mesh objects - for ob in obs: - # bb=ob.bound_box - mw = ob.matrix_world - if ob.type == 'MESH' or ob.type == 'CURVE': - depsgraph = bpy.context.evaluated_depsgraph_get() - ob_eval = ob.evaluated_get(depsgraph) - mesh = ob_eval.to_mesh() - obcount += 1 - if mesh is not None: - for c in mesh.vertices: - coord = c.co - world_coord = mw @ Vector((coord[0], coord[1], coord[2])) - minx = min(minx, world_coord.x) - miny = min(miny, world_coord.y) - minz = min(minz, world_coord.z) - maxx = max(maxx, world_coord.x) - maxy = max(maxy, world_coord.y) - maxz = max(maxz, world_coord.z) - ob_eval.to_mesh_clear() - - if obcount == 0: - minx, miny, minz, maxx, maxy, maxz = 0, 0, 0, 0, 0, 0 - return minx, miny, minz, maxx, maxy, maxz - - -def is_linked_asset(ob): - return ob.get('asset_data') and ob.instance_collection != None - - -def get_dimensions(obs): - minx, miny, minz, maxx, maxy, maxz = get_bounds_snappable(obs) - bbmin = Vector((minx, miny, minz)) - bbmax = Vector((maxx, maxy, maxz)) - dim = Vector((maxx - minx, maxy - miny, maxz - minz)) - return dim, bbmin, bbmax - - -def requests_post_thread(url, json, headers): - r = rerequests.post(url, json=json, verify=True, headers=headers) - - -def get_headers(api_key): - headers = { - "accept": "application/json", - } - if api_key != '': - headers["Authorization"] = "Bearer %s" % api_key - return headers - - -def scale_2d(v, s, p): - '''scale a 2d vector with a pivot''' - return (p[0] + s[0] * (v[0] - p[0]), p[1] + s[1] * (v[1] - p[1])) - - -def scale_uvs(ob, scale=1.0, pivot=Vector((.5, .5))): - mesh = ob.data - if len(mesh.uv_layers) > 0: - uv = mesh.uv_layers[mesh.uv_layers.active_index] - - # Scale a UV map iterating over its coordinates to a given scale and with a pivot point - for uvindex in range(len(uv.data)): - uv.data[uvindex].uv = scale_2d(uv.data[uvindex].uv, scale, pivot) - - -# map uv cubic and switch of auto tex space and set it to 1,1,1 -def automap(target_object=None, target_slot=None, tex_size=1, bg_exception=False, just_scale=False): - wm = bpy.context.window_manager - mat_props = wm.blenderkit_mat - if mat_props.automap: - tob = bpy.data.objects[target_object] - # only automap mesh models - if tob.type == 'MESH' and len(tob.data.polygons) > 0: - # check polycount for a rare case where no polys are in editmesh - actob = bpy.context.active_object - bpy.context.view_layer.objects.active = tob - - # auto tex space - if tob.data.use_auto_texspace: - tob.data.use_auto_texspace = False - - if not just_scale: - tob.data.texspace_size = (1, 1, 1) - - if 'automap' not in tob.data.uv_layers: - bpy.ops.mesh.uv_texture_add() - uvl = tob.data.uv_layers[-1] - uvl.name = 'automap' - - # TODO limit this to active material - # tob.data.uv_textures['automap'].active = True - - scale = tob.scale.copy() - - if target_slot is not None: - tob.active_material_index = target_slot - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_all(action='DESELECT') - - # this exception is just for a 2.8 background thunmbnailer crash, can be removed when material slot select works... - if bg_exception: - bpy.ops.mesh.select_all(action='SELECT') - else: - bpy.ops.object.material_slot_select() - - scale = (scale.x + scale.y + scale.z) / 3.0 - - if tex_size == 0:# prevent division by zero, it's possible to have 0 in tex size by unskilled uploaders - tex_size = 1 - - if not just_scale: - - bpy.ops.uv.cube_project( - cube_size=scale * 2.0 / (tex_size), - correct_aspect=False) # it's * 2.0 because blender can't tell size of a unit cube :) - - bpy.ops.object.editmode_toggle() - tob.data.uv_layers.active = tob.data.uv_layers['automap'] - tob.data.uv_layers["automap"].active_render = True - # this by now works only for thumbnail preview, but should be extended to work on arbitrary objects. - # by now, it takes the basic uv map = 1 meter. also, it now doeasn't respect more materials on one object, - # it just scales whole UV. - if just_scale: - scale_uvs(tob, scale=Vector((1 / tex_size, 1 / tex_size))) - bpy.context.view_layer.objects.active = actob - - -def name_update(props): - ''' - Update asset name function, gets run also before upload. Makes sure name doesn't change in case of reuploads, - and only displayName gets written to server. - ''' - scene = bpy.context.scene - ui_props = bpy.context.window_manager.blenderkitUI - - # props = get_upload_props() - if props.name_old != props.name: - props.name_changed = True - props.name_old = props.name - nname = props.name.strip() - nname = nname.replace('_', ' ') - - if nname.isupper(): - nname = nname.lower() - nname = nname[0].upper() + nname[1:] - props.name = nname - # here we need to fix the name for blender data = ' or " give problems in path evaluation down the road. - fname = props.name - fname = fname.replace('\'', '') - fname = fname.replace('\"', '') - asset = get_active_asset() - if ui_props.asset_type != 'HDR': - # Here we actually rename assets datablocks, but don't do that with HDR's and possibly with others - asset.name = fname - - -def fmt_length(prop): - prop = str(round(prop, 2)) - return prop - - -def get_param(asset_data, parameter_name, default=None): - if not asset_data.get('dictParameters'): - # this can appear in older version files. - return default - - return asset_data['dictParameters'].get(parameter_name, default) - - # for p in asset_data['parameters']: - # if p.get('parameterType') == parameter_name: - # return p['value'] - # return default - - -def params_to_dict(params): - params_dict = {} - for p in params: - params_dict[p['parameterType']] = p['value'] - return params_dict - - -def dict_to_params(inputs, parameters=None): - if parameters == None: - parameters = [] - for k in inputs.keys(): - if type(inputs[k]) == list: - strlist = "" - for idx, s in enumerate(inputs[k]): - strlist += s - if idx < len(inputs[k]) - 1: - strlist += ',' - - value = "%s" % strlist - elif type(inputs[k]) != bool: - value = inputs[k] - else: - value = str(inputs[k]) - parameters.append( - { - "parameterType": k, - "value": value - }) - return parameters - - -def update_tags(self, context): - props = self - - commasep = props.tags.split(',') - ntags = [] - for tag in commasep: - if len(tag) > 19: - short_tags = tag.split(' ') - for short_tag in short_tags: - if len(short_tag) > 19: - short_tag = short_tag[:18] - ntags.append(short_tag) - else: - ntags.append(tag) - if len(ntags) == 1: - ntags = ntags[0].split(' ') - ns = '' - for t in ntags: - if t != '': - ns += t + ',' - ns = ns[:-1] - if props.tags != ns: - props.tags = ns - - -def user_logged_in(): - a = bpy.context.window_manager.get('bkit profile') - if a is not None: - return True - return False - - -def profile_is_validator(): - a = bpy.context.window_manager.get('bkit profile') - if a is not None and a['user'].get('exmenu'): - return True - return False - - -def user_is_owner(asset_data=None): - '''Checks if the current logged in user is owner of the asset''' - profile = bpy.context.window_manager.get('bkit profile') - if profile is None: - return False - if int(asset_data['author']['id']) == int(profile['user']['id']): - return True - return False - - -def asset_from_newer_blender_version(asset_data): - '''checks if asset is from a newer blender version, to avoid incompatibility''' - bver = bpy.app.version - aver = asset_data['sourceAppVersion'].split('.') - bver_f = bver[0] + bver[1] * .01 + bver[2] * .0001 - if len(aver)>=3: - aver_f = int(aver[0]) + int(aver[1]) * .01 + int(aver[2]) * .0001 - return aver_f>bver_f - return False - -def guard_from_crash(): - ''' - Blender tends to crash when trying to run some functions - with the addon going through unregistration process. - This function is used in these functions (like draw callbacks) - so these don't run during unregistration. - ''' - if bpy.context.preferences.addons.get('blenderkit') is None: - return False; - if bpy.context.preferences.addons['blenderkit'].preferences is None: - return False; - return True - - -def get_largest_area(area_type='VIEW_3D'): - maxsurf = 0 - maxa = None - maxw = None - region = None - for w in bpy.data.window_managers[0].windows: - for a in w.screen.areas: - if a.type == area_type: - asurf = a.width * a.height - if asurf > maxsurf: - maxa = a - maxw = w - maxsurf = asurf - - for r in a.regions: - if r.type == 'WINDOW': - region = r - global active_area_pointer, active_window_pointer, active_region_pointer - active_window_pointer = maxw.as_pointer() - active_area_pointer = maxa.as_pointer() - active_region_pointer = region.as_pointer() - return maxw, maxa, region - - -def get_fake_context(context, area_type='VIEW_3D'): - C_dict = {} # context.copy() #context.copy was a source of problems - incompatibility with addons that also define context - C_dict.update(region='WINDOW') - - # try: - # context = context.copy() - # # print('bk context copied successfully') - # except Exception as e: - # print(e) - # print('BlenderKit: context.copy() failed. Can be a colliding addon.') - context = {} - - if context.get('area') is None or context.get('area').type != area_type: - w, a, r = get_largest_area(area_type=area_type) - if w: - # sometimes there is no area of the requested type. Let's face it, some people use Blender without 3d view. - override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} - C_dict.update(override) - # print(w,a,r) - return C_dict - - -# def is_url(text): - - -def label_multiline(layout, text='', icon='NONE', width=-1, max_lines=10, split_last = 0): - ''' - draw a ui label, but try to split it in multiple lines. - - Parameters - ---------- - layout - text - icon - width width to split by in character count - max_lines maximum lines to draw - - Returns - ------- - rows of the text(to add extra elements) - ''' - rows = [] - if text.strip() == '': - return [layout.row()] - text = text.replace('\r\n', '\n') - lines = text.split('\n') - if width > 0: - threshold = int(width / 5.5) - else: - threshold = 35 - li = 0 - for l in lines: - # if is_url(l): - li += 1 - while len(l) > threshold: - i = l.rfind(' ', 0, threshold) - if i < 1: - i = threshold - l1 = l[:i] - row = layout.row() - row.label(text=l1, icon=icon) - rows.append(row) - icon = 'NONE' - l = l[i:].lstrip() - li += 1 - if li > max_lines: - break; - if li > max_lines: - break; - row = layout.row() - if split_last > 0: - row = row.split(factor=split_last) - row.label(text=l, icon=icon) - rows.append(row) - icon = 'NONE' - # if li > max_lines: - return rows - - -def is_upload_old(asset_data): - ''' - estimates if the asset is far too long in the 'uploaded' state - This returns the number of days the validation is over the limit. - ''' - date_time_str = asset_data["created"][:10] - # date_time_str = 'Jun 28 2018 7:40AM' - date_time_obj = datetime.datetime.strptime(date_time_str, '%Y-%m-%d') - today = date_time_obj.today() - age = today - date_time_obj - old = datetime.timedelta(days=5) - if age > old: - return (age.days - old.days) - return 0 - -def trace(): - traceback.print_stack() diff --git a/blenderkit/version_checker.py b/blenderkit/version_checker.py deleted file mode 100644 index 37aeadc4..00000000 --- a/blenderkit/version_checker.py +++ /dev/null @@ -1,80 +0,0 @@ -# ##### 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 -from blenderkit import paths - -import requests, os, json, threading - - -def get_addon_version(): - # should return addon version, but since Blender 3.0 this is synced with Blender version - ver = bpy.app.version - return '%i.%i.%i' % (ver[0], ver[1], ver[2]) - - - # 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', encoding = 'utf-8') as s: - json.dump(ver_online, s, ensure_ascii=False, 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',encoding='utf-8') 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() |