diff --git a/blenderkit/__init__.py b/blenderkit/__init__.py index 5cecc7b9144a2739646fc8c62e10a2ad5c80b4c4..87358d9b7e43f62bdcfe3183a73a7336ada5967c 100644 --- a/blenderkit/__init__.py +++ b/blenderkit/__init__.py @@ -19,14 +19,14 @@ bl_info = { "name": "BlenderKit Asset Library", "author": "Vilem Duha, Petr Dlouhy", - "version": (1, 0, 29), + "version": (1, 0, 30), "blender": (2, 82, 0), "location": "View3D > Properties > BlenderKit", "description": "Online BlenderKit library (materials, models, brushes and more)", "warning": "", "wiki_url": "https://youtu.be/1hVgcQhIAo8" "Scripts/Add_Mesh/BlenderKit", - "category": "Add Mesh", + "category": "3D View", } if "bpy" in locals(): @@ -39,6 +39,7 @@ if "bpy" in locals(): ratings = reload(ratings) autothumb = reload(autothumb) ui = reload(ui) + icons = reload(icons) bg_blender = reload(bg_blender) paths = reload(paths) utils = reload(utils) @@ -48,7 +49,8 @@ if "bpy" in locals(): bkit_oauth = reload(bkit_oauth) tasks_queue = reload(tasks_queue) else: - from blenderkit import asset_inspector, search, download, upload, ratings, autothumb, ui, bg_blender, paths, utils, \ + from blenderkit import asset_inspector, search, download, upload, ratings, autothumb, ui, icons, bg_blender, paths, \ + utils, \ overrides, ui_panels, categories, bkit_oauth, tasks_queue import os @@ -103,6 +105,7 @@ def check_timers_timer(): bpy.app.timers.register(bg_blender.bg_update) return 5.0 + licenses = ( ('royalty_free', 'Royalty Free', 'royalty free commercial license'), ('cc_zero', 'Creative Commons Zero', 'Creative Commons Zero'), @@ -191,6 +194,7 @@ thumbnail_resolutions = ( ('2048', '2048', ''), ) + def get_upload_asset_type(self): typemapper = { BlenderKitModelUploadProps: 'model', @@ -244,16 +248,17 @@ def switch_search_results(self, context): s['search results orig'] = s.get('bkit brush search orig') search.load_previews() + def asset_type_callback(self, context): - #s = bpy.context.scene - #ui_props = s.blenderkitUI + # s = bpy.context.scene + # ui_props = s.blenderkitUI if self.down_up == 'SEARCH': items = ( - ('MODEL', 'Search Models', 'Browse models', 'OBJECT_DATAMODE', 0), + ('MODEL', 'Find Models', 'Find models in the BlenderKit online database', 'OBJECT_DATAMODE', 0), # ('SCENE', 'SCENE', 'Browse scenes', 'SCENE_DATA', 1), - ('MATERIAL', 'Search Materials', 'Browse materials', 'MATERIAL', 2), + ('MATERIAL', 'Find Materials', 'Find models in the BlenderKit online database', 'MATERIAL', 2), # ('TEXTURE', 'Texture', 'Browse textures', 'TEXTURE', 3), - ('BRUSH', 'Search Brushes', 'Browse brushes', 'BRUSH_DATA', 3) + ('BRUSH', 'Find Brushes', 'Find models in the BlenderKit online database', 'BRUSH_DATA', 3) ) else: items = ( @@ -265,6 +270,7 @@ def asset_type_callback(self, context): ) return items + class BlenderKitUIProps(PropertyGroup): down_up: EnumProperty( name="Download vs Upload", @@ -378,6 +384,10 @@ class BlenderKitUIProps(PropertyGroup): dragging_rating_work_hours: BoolProperty(name="Dragging Rating Work Hours", default=False) last_rating_time: FloatProperty(name="Last Rating Time", default=0.0) +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 @@ -386,12 +396,90 @@ class BlenderKitCommonSearchProps(object): default=False) search_done: BoolProperty(name="Search Completed", description="at least one search did run (internal)", default=False) + own_only: BoolProperty(name="My Assets", description="Search only for your assets", + default=False) + search_advanced: BoolProperty(name="Advanced Search Options", description="use advanced search properties", + default=False, update=search.search_update) + 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="Span of the 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="Span of the 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'), + ('VALIDATED', 'Validated', 'Calidated'), + ('ON_HOLD', 'On Hold', 'On Hold'), + ('REJECTED', 'Rejected', 'Rejected'), + ('DELETED', 'Deleted', 'Deleted'), + ), + default='ALL', + 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 @@ -399,7 +487,6 @@ def name_update(self, context): utils.name_update() - def update_tags(self, context): props = utils.get_upload_props() @@ -424,6 +511,7 @@ def update_tags(self, context): if props.tags != ns: props.tags = ns + def update_free(self, context): if self.is_free == False: self.is_free = True @@ -438,6 +526,7 @@ def update_free(self, context): bpy.context.window_manager.popup_menu(draw_message, title=title, icon='INFO') + class BlenderKitCommonUploadProps(object): id: StringProperty( name="Asset Version Id", @@ -504,12 +593,12 @@ class BlenderKitCommonUploadProps(object): ) is_procedural: BoolProperty(name="Procedural", - description="Asset is procedural - has no texture.", - default=True - ) + 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="Node count", description="Total nodes in the asset", default=0) - total_megapixels: 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" @@ -591,22 +680,26 @@ class BlenderKitMaterialSearchProps(PropertyGroup, BlenderKitCommonSearchProps): 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, ) automap: BoolProperty(name="Auto-Map", description="reset object texture space and also add automatically a cube mapped UV " @@ -1145,10 +1238,7 @@ class BlenderKitModelSearchProps(PropertyGroup, BlenderKitCommonSearchProps): ) free_only: BoolProperty(name="Free only", description="Show only free models", - default=False) - - search_advanced: BoolProperty(name="Advanced Search Options", description="use advanced search properties", - default=False) + default=False, update=search.search_update) # CONDITION search_condition: EnumProperty( @@ -1167,52 +1257,45 @@ class BlenderKitModelSearchProps(PropertyGroup, BlenderKitCommonSearchProps): # DESIGN YEAR search_design_year: BoolProperty(name="Sesigned in Year", - description="when the object was approximately designed", - default=False) + description="When the object was approximately designed", + default=False, + update=search.search_update, + ) - search_design_year_min: IntProperty(name="Min Age", - description="when the object was approximately designed", - default=1950, min=-100000000, max=1000000000) + 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="Max Age", - description="when the object was approximately designed", + search_design_year_max: IntProperty(name="Maximum Design Year", + description="Maximum design year", default=2017, min=0, - max=10000000) - - # TEXTURE RESOLUTION - search_texture_resolution: BoolProperty(name="Texture Resolution", - description="Span of the texture resolutions", - default=False) - - search_texture_resolution_min: IntProperty(name="Min Texture Resolution", - description="when the object was approximately designed", - default=256, - min=0, - max=32768) - - search_texture_resolution_max: IntProperty(name="Max Texture Resolution", - description="when the object was approximately designed", - default=4096, - min=0, - max=32768) + max=10000000, + update=search.search_update, + ) # POLYCOUNT search_polycount: BoolProperty(name="Use Polycount", - description="use polycount of object search tag", - default=False) + description="Use polycount of object search tag", + default=False, + update=search.search_update, ) search_polycount_min: IntProperty(name="Min Polycount", - description="polycount of the asset minimum", + description="Minimum poly count of the asset", default=0, min=0, - max=100000000) + max=100000000, + update=search.search_update, ) search_polycount_max: IntProperty(name="Max Polycount", - description="polycount of the asset maximum", + description="Maximum poly count of the asset", default=100000000, min=0, - max=100000000) + max=100000000, + update=search.search_update, + ) append_method: EnumProperty( name="Import Method", @@ -1324,9 +1407,9 @@ class BlenderKitAddonPreferences(AddonPreferences): ) api_key_timeout: IntProperty( - name = 'api key timeout', - description = 'time where the api key will need to be refreshed', - default = 0, + name='api key timeout', + description='time where the api key will need to be refreshed', + default=0, ) api_key_life: IntProperty( @@ -1353,6 +1436,18 @@ class BlenderKitAddonPreferences(AddonPreferences): default=False ) + tips_on_start: BoolProperty( + name="Show tips when starting blender", + description="Show tips when starting blender", + default=False + ) + + 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", @@ -1389,7 +1484,7 @@ class BlenderKitAddonPreferences(AddonPreferences): thumbnail_use_gpu: BoolProperty( name="Use GPU for Thumbnails Rendering", description="By default this is off so you can continue your work without any lag", - default=True + default=False ) panel_behaviour: EnumProperty( @@ -1413,10 +1508,8 @@ class BlenderKitAddonPreferences(AddonPreferences): min=0, max=20) - thumb_size: IntProperty(name="Assetbar thumbnail Size", default=96, min=-1, max=256) - asset_counter: IntProperty(name="Usage Counter", description="Counts usages so it asks for registration only after reaching a limit", default=0, @@ -1447,7 +1540,7 @@ class BlenderKitAddonPreferences(AddonPreferences): layout.operator("wm.blenderkit_logout", text="Logout", icon='URL') - #if not self.enable_oauth: + # 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") @@ -1459,6 +1552,8 @@ class BlenderKitAddonPreferences(AddonPreferences): # 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") # registration @@ -1531,6 +1626,7 @@ def register(): ratings.register_ratings() autothumb.register_thumbnailer() ui.register_ui() + icons.register_icons() ui_panels.register_ui_panels() bg_blender.register() utils.load_prefs() @@ -1544,10 +1640,10 @@ def register(): def unregister(): - bpy.app.timers.unregister(check_timers_timer) ui.unregister_ui() + icons.unregister_icons() search.unregister_search() asset_inspector.unregister_asset_inspector() download.unregister_download() diff --git a/blenderkit/asset_inspector.py b/blenderkit/asset_inspector.py index 74f814ca3e3fefdec393297b7a7ee45e62b5b7a4..e6fdc65938ec1ad3167a70e5fbf318bae78deec2 100644 --- a/blenderkit/asset_inspector.py +++ b/blenderkit/asset_inspector.py @@ -85,8 +85,11 @@ def check_render_engine(props, obs): 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: @@ -108,6 +111,7 @@ def check_render_engine(props, obs): 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': @@ -115,19 +119,21 @@ def check_render_engine(props, obs): shaders.append(n.type) if n.type == 'TEX_IMAGE': mattype = 'image based' - if n.image is not None: + 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) + # if mattype == None: # mattype = 'procedural' # tags['material type'] = mattype diff --git a/blenderkit/autothumb.py b/blenderkit/autothumb.py index 06efd8a031b341de0fbe21adad4c96562bc4c2af..f26f99ddae467be508abfa17a05387e56ee4f1fe 100644 --- a/blenderkit/autothumb.py +++ b/blenderkit/autothumb.py @@ -262,6 +262,8 @@ class GenerateThumbnailOperator(bpy.types.Operator): 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): start_thumbnailer(self, context) @@ -307,6 +309,8 @@ class GenerateMaterialThumbnailOperator(bpy.types.Operator): 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): start_material_thumbnailer(self, context) diff --git a/blenderkit/download.py b/blenderkit/download.py index c4a14ecdfc40b74f45c8daaa6a7c271926e186ab..3a99f66f42cc65ffab616bc544e50610bfa88eeb 100644 --- a/blenderkit/download.py +++ b/blenderkit/download.py @@ -441,6 +441,7 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, scene['assets rated'][id] = scene['assets rated'].get(id, False) parent['asset_data'] = asset_data # TODO remove this??? should write to blenderkit Props? + bpy.ops.wm.undo_push_context() # moving reporting to on save. # report_use_success(asset_data['id']) @@ -523,6 +524,34 @@ def timer_update(): # TODO might get moved to handle all blenderkit stuff, not return .5 +def download_file(asset_data): + #this is a simple non-threaded way to download files for background resolution genenration tool + file_name = paths.get_download_filenames(asset_data)[0] # prefer global dir if possible. + + if check_existing(asset_data): + # this sends the thread for processing, where another check should occur, since the file might be corrupted. + utils.p('not downloading, already in db') + return file_name + preferences = bpy.context.preferences.addons['blenderkit'].preferences + api_key = preferences.api_key + + with open(file_name, "wb") as f: + print("Downloading %s" % file_name) + headers = utils.get_headers(api_key) + + response = requests.get(asset_data['url'], stream=True) + total_length = response.headers.get('Content-Length') + + if total_length is None: # no content length header + f.write(response.content) + else: + dl = 0 + for data in response.iter_content(chunk_size=4096): + dl += len(data) + print(dl) + f.write(data) + return file_name + class Downloader(threading.Thread): def __init__(self, asset_data, tcom, scene_id, api_key): super(Downloader, self).__init__() diff --git a/blenderkit/icons.py b/blenderkit/icons.py new file mode 100644 index 0000000000000000000000000000000000000000..3c6cea4b039f2ea24c2211b32dd35aa27364cc45 --- /dev/null +++ b/blenderkit/icons.py @@ -0,0 +1,53 @@ +# ##### 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', +} + + +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') + + icon_collections["main"] = pcoll + + +def unregister_icons(): + for pcoll in icon_collections.values(): + bpy.utils.previews.remove(pcoll) + icon_collections.clear() diff --git a/blenderkit/paths.py b/blenderkit/paths.py index 3b0f22f9f630971213cb171bbdfd4545a91120aa..112e2465b907bca8d9be20a2df86b6838f7852fa 100644 --- a/blenderkit/paths.py +++ b/blenderkit/paths.py @@ -33,7 +33,6 @@ BLENDERKIT_BRUSH_UPLOAD_INSTRUCTIONS_URL = "https://www.blenderkit.com/docs/uplo 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_ADDON_FILE_URL = "https://www.blenderkit.com/get-blenderkit/" BLENDERKIT_SETTINGS_FILENAME = os.path.join(_presets, "bkit.json") diff --git a/blenderkit/ratings.py b/blenderkit/ratings.py index 7684d0177d781bed07a14672f35e6b04b51299a5..96cbc01f91868d9fdfc054be979255b4961d1e68 100644 --- a/blenderkit/ratings.py +++ b/blenderkit/ratings.py @@ -80,19 +80,30 @@ def uplaod_review_thread(url, reviews, headers): # except requests.exceptions.RequestException as e: # print('reviews upload failed: %s' % str(e)) +def get_rating(asset_id): + user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + api_key = user_preferences.api_key + headers = utils.get_headers(api_key) + rl = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/' + rtypes = ['quality', 'working_hours'] + for rt in rtypes: + params = { + 'rating_type' : rt + } + r = rerequests.get(r1, params=data, verify=True, headers=headers) + print(r.text) def upload_rating(asset): user_preferences = bpy.context.preferences.addons['blenderkit'].preferences api_key = user_preferences.api_key headers = utils.get_headers(api_key) - asset_data = asset['asset_data'] - bkit_ratings = asset.bkit_ratings # print('rating asset', asset_data['name'], asset_data['asset_base_id']) url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/' ratings = [ + ] if bkit_ratings.rating_quality > 0.1: @@ -154,7 +165,7 @@ asset_types = ( class UploadRatingOperator(bpy.types.Operator): """Upload rating to the web db""" bl_idname = "object.blenderkit_rating_upload" - bl_label = "Upload the Rating" + bl_label = "Send Rating" bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} # type of upload - model, material, textures, e.t.c. diff --git a/blenderkit/rerequests.py b/blenderkit/rerequests.py index 0524c156929300e82dd9ddbbf12fb6fd7d922b95..eab78fba473957d2cb34415e2be5a9245dd1ebd2 100644 --- a/blenderkit/rerequests.py +++ b/blenderkit/rerequests.py @@ -55,9 +55,11 @@ def rerequest(method, url, **kwargs): user_preferences = bpy.context.preferences.addons['blenderkit'].preferences if user_preferences.api_key != '': if user_preferences.enable_oauth: - tasks_queue.add_task((ui.add_report, ('refreshing token.',))) + tasks_queue.add_task((ui.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) + auth_token, refresh_token, oauth_response = bkit_oauth.refresh_token( + user_preferences.api_key_refresh, refresh_url) # utils.p(auth_token, refresh_token) if auth_token is not None: diff --git a/blenderkit/search.py b/blenderkit/search.py index 87bf45da1f223e5b6a79076aca2e3452b84e5e56..56c22dbba5106dffb6f29d94f7ab1eb4abc9668c 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -24,12 +24,13 @@ if "bpy" in locals(): utils = reload(utils) categories = reload(categories) ui = reload(ui) + colors = reload(colors) bkit_oauth = reload(bkit_oauth) version_checker = reload(version_checker) tasks_queue = reload(tasks_queue) rerequests = reload(rerequests) else: - from blenderkit import paths, utils, categories, ui, bkit_oauth, version_checker, tasks_queue, rerequests + from blenderkit import paths, utils, categories, ui, colors, bkit_oauth, version_checker, tasks_queue, rerequests import blenderkit from bpy.app.handlers import persistent @@ -80,6 +81,16 @@ thumb_sml_download_threads = {} thumb_full_download_threads = {} reports = '' +rtips = ['Click or drag model or material in scene to link/append ', + "Please rate responsively and plentifully. This helps us distribute rewards to the authors.", + "Click on brushes to link them into scene.", + "All materials and brushes 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 asset bar to search assets by same author.", + "Use 'W' key over asset bar to open Authors webpage.", ] + def refresh_token_timer(): ''' this timer gets run every time the token needs refresh. It refreshes tokens and also categories.''' @@ -117,16 +128,37 @@ def fetch_server_data(): first_time = True +last_clipboard = '' + @bpy.app.handlers.persistent def timer_update(): # TODO might get moved to handle all blenderkit stuff. - #this makes a first search after opening blender. showing latest assets. + # this makes a first search after opening blender. showing latest assets. global first_time preferences = bpy.context.preferences.addons['blenderkit'].preferences if first_time: first_time = False if preferences.show_on_start: search() + if preferences.tips_on_start: + ui.get_largest_3dview() + ui.update_ui_size(ui.active_area, ui.active_region) + ui.add_report(text='BlenderKit Tip: ' + random.choice(rtips), timeout=12, color=colors.GREEN) + + # clipboard monitoring to search assets from web + global last_clipboard + if bpy.context.window_manager.clipboard != last_clipboard: + last_clipboard = bpy.context.window_manager.clipboard + instr = 'asset_base_id:' + if last_clipboard[:len(instr)] == instr: + atstr = 'asset_type:' + ati = last_clipboard.find(atstr) + if ati > -1: + at = last_clipboard[ati:] + + search_props = utils.get_search_props() + search_props.search_keywords = last_clipboard + search() global search_threads # don't do anything while dragging - this could switch asset type during drag, and make results list length different, @@ -134,7 +166,7 @@ def timer_update(): # TODO might get moved to handle all blenderkit stuff. if len(search_threads) == 0 or bpy.context.scene.blenderkitUI.dragging: return 1 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 + # but most of the time only one is running anyway if not thread[0].is_alive(): search_threads.remove(thread) # icons_dir = thread[1] @@ -220,7 +252,7 @@ def timer_update(): # TODO might get moved to handle all blenderkit stuff. asset_data['downloaded'] = 0 # parse extra params needed for blender here - params = params_to_dict(r['parameters']) + params = utils.params_to_dict(r['parameters']) if asset_type == 'model': if params.get('boundBoxMinX') != None: @@ -333,11 +365,11 @@ def split_subs(text, threshold=40): lines = [] while len(text) > threshold: - #first handle if there's an \n line ending + # 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) + text = text.replace('\n', '', 1) else: i = text.rfind(' ', 0, threshold) i1 = text.rfind(',', 0, threshold) @@ -403,17 +435,10 @@ def has(mdata, prop): return False -def params_to_dict(params): - params_dict = {} - for p in params: - params_dict[p['parameterType']] = p['value'] - return params_dict - - def generate_tooltip(mdata): col_w = 40 if type(mdata['parameters']) == list: - mparams = params_to_dict(mdata['parameters']) + mparams = utils.params_to_dict(mdata['parameters']) else: mparams = mdata['parameters'] t = '' @@ -479,7 +504,7 @@ def generate_tooltip(mdata): # t += 'uv: %s\n' % mdata['uv'] # t += '\n' - # t = writeblockm(t, mdata, key='license', width = col_w) + t = writeblockm(t, mdata, key='license', width = col_w) # generator is for both upload preview and search, this is only after search # if mdata.get('versionNumber'): @@ -500,14 +525,7 @@ def generate_tooltip(mdata): def get_random_tip(mdata): t = '' - rtips = ['Click or drag model or material in scene to link/append ', - "Click on brushes to link them into scene.", - "All materials are free.", - "All brushes are free.", - "Locked models are available if you subscribe to Full plan.", - "Login to upload your own models, materials or brushes.", - "Use 'A' key to search assets by same author.", - "Use 'W' key to open Authors webpage.", ] + tip = 'Tip: ' + random.choice(rtips) t = writeblock(t, tip) return t @@ -623,6 +641,7 @@ def fetch_author(a_id, api_key): utils.p(e) utils.p('finish fetch') + # profile_counter =0 def get_author(r): @@ -632,7 +651,7 @@ def get_author(r): 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): + if a is None: # or a is '' or (a.get('gravatarHash') is not None and a.get('gravatarImg') is None): authors[a_id] = '' thread = threading.Thread(target=fetch_author, args=(a_id, preferences.api_key), daemon=True) thread.start() @@ -683,7 +702,6 @@ def fetch_profile(api_key): utils.p(e) - def get_profile(): preferences = bpy.context.preferences.addons['blenderkit'].preferences a = bpy.context.window_manager.get('bkit profile') @@ -691,11 +709,6 @@ def get_profile(): thread.start() return a -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 class Searcher(threading.Thread): query = None @@ -712,6 +725,45 @@ class Searcher(threading.Thread): def stopped(self): return self._stop_event.is_set() + def query_to_url(self): + query = self.query + params = self.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() + + # result ordering: _score - relevance, score - BlenderKit score + + 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 + requeststring += '+order:-last_upload' + elif query.get('author_id') is not None and utils.profile_is_validator(): + + requeststring += '+order:-created' + else: + if query.get('category_subtree') is not None: + requeststring += '+order:-score,_score' + else: + requeststring += '+order:_score' + + 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 run(self): maxthreads = 50 query = self.query @@ -733,44 +785,23 @@ class Searcher(threading.Thread): try: origdata = json.load(infile) urlquery = origdata['next'] + # rparameters = {} if urlquery == None: return; except: # in case no search results found on drive we don't do next page loading. params['get_next'] = False if not params['get_next']: - # build a new request url = paths.get_api_url() + 'search/' - # build request manually - # TODO use real queries - requeststring = '?query=' + query['keywords'].lower() + '+' - # - for i, q in enumerate(query): - requeststring += q + ':' + str(query[q]).lower() - if i < len(query) - 1: - requeststring += '+' - - # result ordering: _score - relevance, score - BlenderKit score - #first condition assumes no keywords and no category, thus an empty search that is triggered on start. - if query['keywords'] == '' and query.get('category_subtree') == None: - requeststring += '+order:-created' - elif query.get('author_id') is not None and profile_is_validator(): - requeststring += '+order:-created' - else: - if query.get('category_subtree') is not None: - requeststring += '+order:-score,_score' - else: - requeststring += '+order:_score' - - requeststring += '&addon_version=%s' % params['addon_version'] - if params.get('scene_uuid') is not None: - requeststring += '&scene_uuid=%s' % params['scene_uuid'] + urlquery = url - urlquery = url + requeststring + # rparameters = query + urlquery = self.query_to_url() try: utils.p(urlquery) - r = rerequests.get(urlquery, headers=headers) + r = rerequests.get(urlquery, headers=headers) # , params = rparameters) + # print(r.url) reports = '' # utils.p(r.text) except requests.exceptions.RequestException as e: @@ -798,13 +829,13 @@ class Searcher(threading.Thread): if p['parameterType'] == 'mode': mode = p['value'] if query['asset_type'] != 'brush' or ( - query.get('brushType') != None and query['brushType']) == mode: + query.get('mode') != None and query['mode']) == mode: nresults.append(d) rdata['results'] = nresults # print('number of results: ', len(rdata.get('results', []))) if self.stopped(): - utils.p('stopping search : ' + query['keywords']) + utils.p('stopping search : ' + str(query)) return mt('search finished') @@ -863,7 +894,7 @@ class Searcher(threading.Thread): # TODO do the killing/ stopping here! remember threads might have finished inbetween! if self.stopped(): - utils.p('stopping search : ' + query['keywords']) + utils.p('stopping search : ' + str(query)) return # this loop handles downloading of small thumbnails @@ -887,7 +918,7 @@ class Searcher(threading.Thread): # utils.p('fetched thumbnail ', i) i += 1 if self.stopped(): - utils.p('stopping search : ' + query['keywords']) + utils.p('stopping search : ' + str(query)) return idx = 0 while len(thumb_sml_download_threads) > 0: @@ -899,7 +930,7 @@ class Searcher(threading.Thread): i += 1 if self.stopped(): - utils.p('stopping search : ' + query['keywords']) + utils.p('stopping search : ' + str(query)) return # start downloading full thumbs in the end @@ -914,13 +945,35 @@ class Searcher(threading.Thread): def build_query_common(query, props): - query_common = { - "keywords": props.search_keywords - } - query.update(query_common) + '''add shared parameters to query''' + query_common = {} + if props.search_keywords != '': + query_common["query"] = props.search_keywords + + if props.search_verification_status != 'ALL': + query_common['verification_status'] = props.search_verification_status.lower() + + if props.search_advanced: + 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 == 'TEXTURE_BASED': + # todo this procedural hack should be replaced with the parameter + query["textureResolutionMax_gte"] = 0 + # query["procedural"] = False + if props.search_procedural == "PROCEDURAL": + #todo this procedural hack should be replaced with the parameter + query["files_size_lte"] = 1024 * 1024 + # query["procedural"] = True + elif 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 + + + query.update(query_common) -# def query_add_range(query, name, rmin, rmax): def build_query_model(): '''use all search input to request results from server''' @@ -944,14 +997,11 @@ def build_query_model(): if props.search_condition != 'UNSPECIFIED': query["condition"] = props.search_condition if props.search_design_year: - query["designYearMin"] = props.search_design_year_min - query["designYearMax"] = props.search_design_year_max + query["designYear_gte"] = props.search_design_year_min + query["designYear_lte"] = props.search_design_year_max if props.search_polycount: - query["polyCountMin"] = props.search_polycount_min - query["polyCountMax"] = props.search_polycount_max - if props.search_texture_resolution: - query["textureResolutionMin"] = props.search_texture_resolution_min - query["textureResolutionMax"] = props.search_texture_resolution_max + query["faceCount_gte"] = props.search_polycount_min + query["faceCount_lte"] = props.search_polycount_max build_query_common(query, props) @@ -988,6 +1038,7 @@ def build_query_material(): query["style"] = props.search_style else: query["style"] = props.search_style_other + build_query_common(query, props) return query @@ -1024,7 +1075,7 @@ def build_query_brush(): query = { "asset_type": 'brush', - "brushType": brush_type + "mode": brush_type } build_query_common(query, props) @@ -1063,7 +1114,7 @@ def search(category='', get_next=False, author_id=''): user_preferences = bpy.context.preferences.addons['blenderkit'].preferences search_start_time = time.time() - mt('start') + #mt('start') scene = bpy.context.scene uiprops = scene.blenderkitUI @@ -1106,6 +1157,12 @@ def search(category='', get_next=False, author_id=''): 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 @@ -1127,7 +1184,7 @@ def search(category='', get_next=False, author_id=''): def search_update(self, context): utils.p('search updater') - #if self.search_keywords != '': + # if self.search_keywords != '': ui_props = bpy.context.scene.blenderkitUI if ui_props.down_up != 'SEARCH': ui_props.down_up = 'SEARCH' diff --git a/blenderkit/thumbnails/flp.png b/blenderkit/thumbnails/flp.png new file mode 100644 index 0000000000000000000000000000000000000000..7ac3c3d75ad7181f0105f40a4cc9001ae12b8ce8 Binary files /dev/null and b/blenderkit/thumbnails/flp.png differ diff --git a/blenderkit/thumbnails/fp.png b/blenderkit/thumbnails/fp.png new file mode 100644 index 0000000000000000000000000000000000000000..4e356ab19d22eac1b05edc2a7f97e3a50c3cf407 Binary files /dev/null and b/blenderkit/thumbnails/fp.png differ diff --git a/blenderkit/thumbnails/vs_rejected.png b/blenderkit/thumbnails/vs_rejected.png new file mode 100644 index 0000000000000000000000000000000000000000..6ff663cf546c5c520bb21a659e4cc52251ac57dc Binary files /dev/null and b/blenderkit/thumbnails/vs_rejected.png differ diff --git a/blenderkit/ui.py b/blenderkit/ui.py index fc9563a9d4d997cda6770102e50c7d86743c2b83..3f4d0381a4c142397fbce984457ee6953f902d9d 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -50,6 +50,11 @@ import os handler_2d = None handler_3d = None +active_area = None +active_area = None +active_window = None +active_region = None + reports = [] mappingdict = { @@ -67,7 +72,7 @@ verification_icons = { 'uploading': 'vs_uploading.png', 'on_hold': 'vs_on_hold.png', 'validated': None, - 'rejected': None + 'rejected': 'vs_rejected.png' } @@ -133,7 +138,8 @@ class Report(): pass; def draw(self, x, y): - ui_bgl.draw_text(self.text, x, y + 8, 16, self.draw_color) + if bpy.context.area == active_area: + ui_bgl.draw_text(self.text, x, y + 8, 16, self.draw_color) def get_asset_under_mouse(mousex, mousey): @@ -698,6 +704,7 @@ def draw_callback_2d_upload_preview(self, context): props = utils.get_upload_props() if props != None and ui_props.draw_tooltip: + if ui_props.asset_type != 'BRUSH': ui_props.thumbnail_image = props.thumbnail else: @@ -889,8 +896,12 @@ def draw_callback_2d_search(self, context): ui_props.mouse_y - linelength, 2, white) + def draw_callback_3d(self, context): ''' Draw snapped bbox while dragging and in the future other blenderkit related stuff. ''' + if not utils.guard_from_crash(): + return; + ui = context.scene.blenderkitUI if ui.dragging and ui.asset_type == 'MODEL': @@ -1173,6 +1184,10 @@ def get_largest_3dview(): for r in a.regions: if r.type == 'WINDOW': region = r + global active_area, active_window, active_region + active_window = maxw + active_area = maxa + active_region = region return maxw, maxa, region @@ -1417,6 +1432,8 @@ class AssetBarOperator(bpy.types.Operator): 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 @@ -1476,7 +1493,7 @@ class AssetBarOperator(bpy.types.Operator): if not asset_data['can_download']: message = 'Asset locked. Find out how to unlock Everything and ...' link_text = 'support all BlenderKit artists.' - url = paths.get_bkit_url() + '/get-blenderkit/' + asset_data['id'] + url = paths.get_bkit_url() + '/get-blenderkit/' + asset_data['id'] + '/?from_addon' bpy.ops.wm.blenderkit_url_dialog('INVOKE_REGION_WIN', url=url, message=message, link_text=link_text) return {'RUNNING_MODAL'} @@ -1696,30 +1713,38 @@ class AssetBarOperator(bpy.types.Operator): if sr is None: bpy.context.scene['search results'] = [] - if context.area.type == 'VIEW_3D': - # the arguments we pass the the callback - args = (self, context) - self.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 + if context.area.type != 'VIEW_3D': + self.report({'WARNING'}, "View3D not found, cannot run operator") + return {'CANCELLED'} - for r in self.area.regions: - if r.type == 'WINDOW': - self.region = r + # the arguments we pass the the callback + args = (self, context) - update_ui_size(self.area, self.region) + self.window = context.window + self.area = context.area + self.scene = bpy.context.scene - self._handle_2d = bpy.types.SpaceView3D.draw_handler_add(draw_callback_2d, args, 'WINDOW', 'POST_PIXEL') - self._handle_3d = bpy.types.SpaceView3D.draw_handler_add(draw_callback_3d, args, 'WINDOW', 'POST_VIEW') - context.window_manager.modal_handler_add(self) - ui_props.assetbar_on = True - return {'RUNNING_MODAL'} - else: - self.report({'WARNING'}, "View3D not found, cannot run operator") - return {'CANCELLED'} + 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, active_area, active_region + active_window = self.window + active_area = self.area + active_region = self.region + + update_ui_size(self.area, self.region) + + self._handle_2d = bpy.types.SpaceView3D.draw_handler_add(draw_callback_2d, args, 'WINDOW', 'POST_PIXEL') + self._handle_3d = bpy.types.SpaceView3D.draw_handler_add(draw_callback_3d, args, 'WINDOW', 'POST_VIEW') + + context.window_manager.modal_handler_add(self) + ui_props.assetbar_on = True + return {'RUNNING_MODAL'} def execute(self, context): return {'RUNNING_MODAL'} @@ -1743,6 +1768,27 @@ class TransferBlenderkitData(bpy.types.Operator): 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'} + + def execute(self, context): + C_dict = bpy.context.copy() + C_dict.update(region='WINDOW') + if context.area is None or context.area.type != 'VIEW_3D': + w, a, r = get_largest_3dview() + override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} + C_dict.update(override) + bpy.ops.ed.undo_push(C_dict, 'INVOKE_REGION_WIN') + return {'FINISHED'} + + class RunAssetBarWithContext(bpy.types.Operator): """Regenerate cobweb""" bl_idname = "object.run_assetbar_fix_context" @@ -1761,18 +1807,28 @@ class RunAssetBarWithContext(bpy.types.Operator): override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} C_dict.update(override) bpy.ops.view3d.blenderkit_asset_bar(C_dict, 'INVOKE_REGION_WIN', keep_running=True, do_search=False) - return {'RUNNING_MODAL'} + return {'FINISHED'} classess = ( AssetBarOperator, RunAssetBarWithContext, - TransferBlenderkitData + TransferBlenderkitData, + UndoWithContext ) # store keymap items here to access after registration addon_keymapitems = [] +#@persistent +def pre_load(context): + ui_props = bpy.context.scene.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 @@ -1803,6 +1859,7 @@ def register_ui(): 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') diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py index b981fbccd905c56c0094b041e18419c3569480f2..8bd9fd58cefb3d635df151007c0d1c90d9b2ec83 100644 --- a/blenderkit/ui_panels.py +++ b/blenderkit/ui_panels.py @@ -24,8 +24,9 @@ if "bpy" in locals(): utils = importlib.reload(utils) download = importlib.reload(download) categories = importlib.reload(categories) + icons = importlib.reload(icons) else: - from blenderkit import paths, ratings, utils, download, categories + from blenderkit import paths, ratings, utils, download, categories, icons from bpy.types import ( Panel @@ -43,7 +44,7 @@ def label_multiline(layout, text='', icon='NONE', width=-1): threshold = int(width / 5.5) else: threshold = 35 - maxlines = 6 + maxlines = 8 li = 0 for l in lines: while len(l) > threshold: @@ -53,7 +54,7 @@ def label_multiline(layout, text='', icon='NONE', width=-1): l1 = l[:i] layout.label(text=l1, icon=icon) icon = 'NONE' - l = l[i:] + l = l[i:].lstrip() li += 1 if li > maxlines: break; @@ -75,10 +76,10 @@ def draw_ratings(layout, context): layout.prop(bkit_ratings, 'rating_work_hours') w = context.region.width - layout.label(text='problems') - layout.prop(bkit_ratings, 'rating_problems', text='') - layout.label(text='compliments') - layout.prop(bkit_ratings, 'rating_compliments', text='') + # 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') @@ -151,7 +152,7 @@ def prop_needed(layout, props, name, value, is_not_filled=''): # row.label(text='', icon = 'ERROR') icon = 'ERROR' row.alert = True - row.prop(props, name)#, icon=icon) + row.prop(props, name) # , icon=icon) row.alert = False else: # row.label(text='', icon = 'FILE_TICK') @@ -264,6 +265,7 @@ def draw_panel_scene_upload(self, context): row.prop(props, 'work_hours') layout.prop(props, 'adult') + def draw_assetbar_show_hide(layout, props): s = bpy.context.scene ui_props = s.blenderkitUI @@ -299,7 +301,9 @@ def draw_panel_model_search(self, context): 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") @@ -307,7 +311,7 @@ def draw_panel_model_search(self, context): # layout.prop(props, 'append_link', expand=True, icon_only=False) # layout.prop(props, 'import_as', expand=True, icon_only=False) - # layout.prop(props, "search_advanced") + layout.prop(props, "search_advanced") if props.search_advanced: layout.separator() @@ -318,34 +322,39 @@ def draw_panel_model_search(self, context): # layout.prop(props, "search_engine_keyword") # AGE - layout.prop(props, "search_condition") # , text ='condition of object new/old e.t.c.') + layout.prop(props, "search_condition", text='Condition') # , text ='condition of object new/old e.t.c.') # DESIGN YEAR layout.prop(props, "search_design_year", text='designed in ( min - max )') - row = layout.row(align=True) - if not props.search_design_year_min: - row.active = False - row.prop(props, "search_design_year_min", text='min') - row.prop(props, "search_design_year_max", text='max') + 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='polycount in ( min - max )') - row = layout.row(align=True) - if not props.search_polycount: - row.active = False - row.prop(props, "search_polycount_min", text='min') - row.prop(props, "search_polycount_max", text='max') + layout.prop(props, "search_polycount", text='Poly count in ( min - max )') + 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 resolution ( min - max )') - row = layout.row(align=True) - if not props.search_texture_resolution: - row.active = False - row.prop(props, "search_texture_resolution_min", text='min') - row.prop(props, "search_texture_resolution_max", text='max') - + 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 ( min - max )') + 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, "search_procedural", expand=True) # ADULT - layout.prop(props, "search_adult") # , text ='condition of object new/old e.t.c.') + # layout.prop(props, "search_adult") # , text ='condition of object new/old e.t.c.') draw_panel_categories(self, context) @@ -366,7 +375,7 @@ def draw_panel_scene_search(self, context): row = layout.row() row.prop(props, "search_keywords", text="", icon='VIEWZOOM') draw_assetbar_show_hide(row, props) - + layout.prop(props, "own_only") label_multiline(layout, text=props.report) # layout.prop(props, "search_style") @@ -401,6 +410,8 @@ class VIEW3D_PT_blenderkit_model_properties(Panel): if o.instance_type == 'COLLECTION' and o.instance_collection is not None: layout.operator('object.blenderkit_bring_to_scene', text='Bring to scene') + draw_panel_model_rating(self, context) + # if 'rig' in ad['tags']: # # layout.label(text = 'can make proxy') # layout.operator('object.blenderkit_make_proxy', text = 'Make Armature proxy') @@ -424,7 +435,7 @@ class VIEW3D_PT_blenderkit_profile(Panel): bl_idname = "VIEW3D_PT_blenderkit_profile" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' - bl_label = "Profile" + bl_label = "BlenderKit Profile" @classmethod def poll(cls, context): @@ -440,26 +451,59 @@ class VIEW3D_PT_blenderkit_profile(Panel): draw_login_progress(layout) return - if user_preferences.enable_oauth: - draw_login_buttons(layout) - if user_preferences.api_key != '': me = bpy.context.window_manager.get('bkit profile') if me is not None: me = me['user'] - layout.label(text='User: %s %s' % (me['firstName'], me['lastName'])) - layout.label(text='Email: %s' % (me['email'])) - if me.get('sumAssetFilesSize') is not None: # TODO remove this when production server has these too. - layout.label(text='Public assets: %i MiB' % (me['sumAssetFilesSize'])) - if me.get('sumPrivateAssetFilesSize') is not None: - layout.label(text='Private assets: %i MiB' % (me['sumPrivateAssetFilesSize'])) + # user name + layout.label(text='Me: %s %s' % (me['firstName'], me['lastName'])) + # layout.label(text='Email: %s' % (me['email'])) + + # plan information + + # pcoll = icons.icon_collections["main"] + # my_icon = pcoll['free'] + # row = layout.row() + # row.label(text='My plan:') + # row.label(text='Free plan', icon_value=my_icon.icon_id) + # 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='Remaining private storage: %i MiB' % (me['remainingPrivateQuota'])) + 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 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 op = draw_ratings(self.layout, context) # , props) @@ -526,7 +570,7 @@ def draw_panel_material_search(self, context): row = layout.row() row.prop(props, "search_keywords", text="", icon='VIEWZOOM') draw_assetbar_show_hide(row, props) - + layout.prop(props, "own_only") label_multiline(layout, text=props.report) # layout.prop(props, 'search_style') @@ -536,10 +580,33 @@ def draw_panel_material_search(self, context): # if props.search_engine == 'OTHER': # layout.prop(props, 'search_engine_other') - layout.prop(props, 'automap') + layout.prop(props, "search_advanced") + if props.search_advanced: + layout.separator() + + layout.label(text = 'texture types') + 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 ( min - max )') + 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 ( min - max in 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') draw_panel_categories(self, context) + layout.prop(props, 'automap') + def draw_panel_material_ratings(self, context): op = draw_ratings(self.layout, context) # , props) @@ -568,7 +635,7 @@ def draw_panel_brush_search(self, context): row = layout.row() row.prop(props, "search_keywords", text="", icon='VIEWZOOM') draw_assetbar_show_hide(row, props) - + layout.prop(props, "own_only") label_multiline(layout, text=props.report) draw_panel_categories(self, context) @@ -605,7 +672,7 @@ class VIEW3D_PT_blenderkit_unified(Panel): bl_idname = "VIEW3D_PT_blenderkit_unified" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' - bl_label = "BlenderKit" + bl_label = "Find and Upload Assets" @classmethod def poll(cls, context): @@ -622,19 +689,19 @@ class VIEW3D_PT_blenderkit_unified(Panel): # 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.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) - #split = row.split(factor=.5) - #row.prop(ui_props, 'asset_type', expand=True, icon_only=True) - #row = layout.column(align = False) - layout.prop(ui_props, 'asset_type', expand=False, text = '') + # row = row.split().row() + # layout.alert = True + # layout.alignment = 'CENTER' + # row = layout.row(align = True) + # split = row.split(factor=.5) + # row.prop(ui_props, 'asset_type', expand=True, icon_only=True) + # row = layout.column(align = False) + layout.prop(ui_props, 'asset_type', expand=False, text='') w = context.region.width if user_preferences.login_attempt: @@ -651,14 +718,16 @@ class VIEW3D_PT_blenderkit_unified(Panel): layout.label(text='Paste your API Key:') layout.prop(user_preferences, 'api_key', text='') layout.separator() - if bpy.data.filepath == '': - layout.alert = True - label_multiline(layout, text="It's better to save your file first.", width=w) - layout.alert = False - layout.separator() + # if bpy.data.filepath == '': + # layout.alert = True + # 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') if ui_props.asset_type == 'MODEL': # noinspection PyCallByClass draw_panel_model_search(self, context) @@ -744,7 +813,6 @@ class VIEW3D_PT_blenderkit_unified(Panel): layout.label(text='not yet implemented') - class OBJECT_MT_blenderkit_asset_menu(bpy.types.Menu): bl_label = "Asset options:" bl_idname = "OBJECT_MT_blenderkit_asset_menu" @@ -788,9 +856,13 @@ class OBJECT_MT_blenderkit_asset_menu(bpy.types.Menu): wm = bpy.context.window_manager profile = wm.get('bkit profile') if profile is not None: - # validation by admin - if profile['user']['exmenu']: + # validation + if utils.profile_is_validator(): layout.label(text='Validation tools:') + if asset_data['verificationStatus'] != 'uploaded': + op = layout.operator('object.blenderkit_change_status', text='set Uploaded') + op.asset_id = asset_data['id'] + op.state = 'uploaded' if asset_data['verificationStatus'] != 'validated': op = layout.operator('object.blenderkit_change_status', text='Validate') op.asset_id = asset_data['id'] @@ -881,7 +953,7 @@ class UrlPopupDialog(bpy.types.Operator): op.url = self.url def execute(self, context): - #start_thumbnailer(self, context) + # start_thumbnailer(self, context) return {'FINISHED'} def invoke(self, context, event): @@ -976,20 +1048,28 @@ class VIEW3D_PT_blenderkit_downloads(Panel): def header_search_draw(self, context): '''Top bar menu in 3d view''' - layout = self.layout - s = bpy.context.scene - ui_props = s.blenderkitUI - if ui_props.asset_type == 'MODEL': - props = s.blenderkit_models - if ui_props.asset_type == 'MATERIAL': - props = s.blenderkit_mat - if ui_props.asset_type == 'BRUSH': - props = s.blenderkit_brush - layout.separator_spacer() - layout.prop(ui_props, "asset_type", text='', icon='URL') - layout.prop(props, "search_keywords", text="", icon='VIEWZOOM') - draw_assetbar_show_hide(layout, props) + if not utils.guard_from_crash(): + return; + + preferences = bpy.context.preferences.addons['blenderkit'].preferences + if preferences.search_in_header: + layout = self.layout + s = bpy.context.scene + ui_props = s.blenderkitUI + if ui_props.asset_type == 'MODEL': + props = s.blenderkit_models + if ui_props.asset_type == 'MATERIAL': + props = s.blenderkit_mat + if ui_props.asset_type == 'BRUSH': + props = s.blenderkit_brush + + # 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", text='', icon='URL') + layout.prop(props, "search_keywords", text="", icon='VIEWZOOM') + draw_assetbar_show_hide(layout, props) # We can store multiple preview collections here, @@ -997,11 +1077,11 @@ def header_search_draw(self, context): preview_collections = {} classess = ( SetCategoryOperator, - + VIEW3D_PT_blenderkit_profile, + VIEW3D_PT_blenderkit_login, VIEW3D_PT_blenderkit_unified, VIEW3D_PT_blenderkit_model_properties, VIEW3D_PT_blenderkit_downloads, - VIEW3D_PT_blenderkit_profile, OBJECT_MT_blenderkit_asset_menu, UrlPopupDialog ) @@ -1015,4 +1095,6 @@ def register_ui_panels(): def unregister_ui_panels(): for c in classess: + print('unregister', c) bpy.utils.unregister_class(c) + bpy.types.VIEW3D_MT_editor_menus.remove(header_search_draw) diff --git a/blenderkit/upload.py b/blenderkit/upload.py index b1c4b25c812c9f59843f8bc029dfd87873d48343..f2f295e5998b810167acc3e41d583ec1c832eaef 100644 --- a/blenderkit/upload.py +++ b/blenderkit/upload.py @@ -81,28 +81,6 @@ def add_version(data): data["addonVersion"] = addon_version -def params_to_dict(inputs, parameters=None): - if parameters == None: - parameters = [] - for k in inputs.keys(): - if type(inputs[k]) == list: - strlist = "" - for idx, s in enumerate(inputs[k]): - strlist += s - if idx < len(inputs[k]) - 1: - strlist += ',' - - value = "%s" % strlist - elif type(inputs[k]) != bool: - value = inputs[k] - else: - value = str(inputs[k]) - parameters.append( - { - "parameterType": k, - "value": value - }) - return parameters def write_to_report(props, text): @@ -254,6 +232,10 @@ def get_upload_data(self, context, asset_type): "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: @@ -381,6 +363,7 @@ def get_upload_data(self, context, asset_type): "procedural": props.is_procedural, "nodeCount": props.node_count, "textureCount": props.texture_count, + "megapixels": round(props.total_megapixels/ 1000000), } @@ -570,7 +553,7 @@ def start_upload(self, context, asset_type, reupload, upload_set): export_data, upload_data, eval_path_computing, eval_path_state, eval_path, props = get_upload_data(self, context, asset_type) # utils.pprint(upload_data) - upload_data['parameters'] = params_to_dict( + upload_data['parameters'] = utils.dict_to_params( upload_data['parameters']) # weird array conversion only for upload, not for tooltips. binary_path = bpy.app.binary_path @@ -783,7 +766,10 @@ class UploadOperator(Operator): if props.is_private == 'PUBLIC': ui_panels.label_multiline(layout, text='public assets are validated several hours' - ' or days after upload. ', width=300) + ' or days after upload. Remember always to ' + 'test download your asset to a clean file' + ' to see if it uploaded correctly.' + , width=300) def invoke(self, context, event): props = utils.get_upload_props() diff --git a/blenderkit/upload_bg.py b/blenderkit/upload_bg.py index b2db44dad4f94071a537e30e942f3cba84a8b756..236793c08a03f984b67bf6ed7d2897bf6e368118 100644 --- a/blenderkit/upload_bg.py +++ b/blenderkit/upload_bg.py @@ -87,8 +87,8 @@ def upload_file(upload_data, f): 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 * 256 + # + chunk_size = 1024 * 1024 * 2 utils.pprint(upload) # file gets uploaded here: uploaded = False @@ -103,8 +103,10 @@ def upload_file(upload_data, f): if upload_response.status_code == 200: uploaded = True else: + print(upload_response.text) bg_blender.progress(f'Upload failed, retry. {a}') except Exception as e: + print(e) bg_blender.progress('Upload %s failed, retrying' % f['type']) time.sleep(1) diff --git a/blenderkit/utils.py b/blenderkit/utils.py index c1a59cd8b9488a61c3eb3d06b0acf9a960e785fd..706600ee9b72e2659dfe27cbb074e37683b0871d 100644 --- a/blenderkit/utils.py +++ b/blenderkit/utils.py @@ -514,4 +514,48 @@ def name_update(): fname = fname.replace('\'', '') fname = fname.replace('\"', '') asset = get_active_asset() - asset.name = fname \ No newline at end of file + asset.name = fname + +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 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 guard_from_crash(): + '''Blender tends to crash when trying to run some functions with the addon going through unregistration process.''' + if bpy.context.preferences.addons['blenderkit'] is None: + return False; + if bpy.context.preferences.addons['blenderkit'].preferences is None: + return False; + return True \ No newline at end of file