2026-02-16
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
bl_info = {
|
||||
"name": "BlenderKit Online Asset Library",
|
||||
"author": "Vilem Duha, Petr Dlouhy, A. Gajdosik",
|
||||
"version": (3, 18, 0, 251121), # X.Y.Z.yymmdd
|
||||
"version": (3, 18, 1, 251219), # X.Y.Z.yymmdd
|
||||
"blender": (3, 0, 0),
|
||||
"location": "View3D > Properties > BlenderKit",
|
||||
"description": "Boost your workflow with drag&drop assets from the community driven library.",
|
||||
@@ -28,7 +28,7 @@ bl_info = {
|
||||
"tracker_url": "https://github.com/BlenderKit/blenderkit/issues",
|
||||
"category": "3D View",
|
||||
}
|
||||
VERSION = (3, 18, 0, 251121)
|
||||
VERSION = (3, 18, 1, 251219)
|
||||
|
||||
import logging
|
||||
import random
|
||||
@@ -242,7 +242,7 @@ engines = (
|
||||
("CYCLES", "Cycles", "Blender Cycles"),
|
||||
("EEVEE", "Eevee", "Blender eevee renderer"),
|
||||
("EEEVE_NEXT", "Eevee Next", "Blender eevee renderer (new)"),
|
||||
("OCTANE", "Octane", "Octane render enginge"),
|
||||
("OCTANE", "Octane", "Octane render engine"),
|
||||
("ARNOLD", "Arnold", "Arnold render engine"),
|
||||
("V-RAY", "V-Ray", "V-Ray renderer"),
|
||||
("UNREAL", "Unreal", "Unreal engine"),
|
||||
@@ -267,6 +267,12 @@ mesh_poly_types = (
|
||||
)
|
||||
|
||||
|
||||
EXTRA_PATH_OPTIONS = {}
|
||||
|
||||
if bpy.app.version >= (4, 5, 0):
|
||||
EXTRA_PATH_OPTIONS = {"options": {"PATH_SUPPORTS_BLEND_RELATIVE"}}
|
||||
|
||||
|
||||
def udate_down_up(self, context):
|
||||
"""Perform a search if results are empty."""
|
||||
props = bpy.context.window_manager.blenderkitUI
|
||||
@@ -461,12 +467,12 @@ class BlenderKitUIProps(PropertyGroup):
|
||||
search_blender_version: BoolProperty(
|
||||
name="Asset Blender Version",
|
||||
description="Limit the assets by version of Blender (minimum, maximum) in which they were created. "
|
||||
+ "Use maximum version limit to exclude incompatible assets from newer Blender versions than yours. Or set the minumum version to exclude assets created in quite old Blender versions",
|
||||
+ "Use maximum version limit to exclude incompatible assets from newer Blender versions than yours. Or set the minimum version to exclude assets created in quite old Blender versions",
|
||||
)
|
||||
search_blender_version_min: StringProperty(
|
||||
name="Minimal version (including, higher than or equal)",
|
||||
default="0.0",
|
||||
description="Limit the assets by minimum version of Blender in which they were created, including also the specified version and exluding all older versions from the search results. "
|
||||
description="Limit the assets by minimum version of Blender in which they were created, including also the specified version and excluding all older versions from the search results. "
|
||||
+ "Only assets created in HIGHER THAN OR EQUAL (>= min) minimum version will be shown. Use semantic versioning format: X.Y.Z.\n\n"
|
||||
+ "E.g.: exclude all Blender 2 assets by specifying 3, 3.0, or 3.0.0. Assets created in 3.0 or higher will be shown",
|
||||
update=search.search_update,
|
||||
@@ -474,7 +480,7 @@ class BlenderKitUIProps(PropertyGroup):
|
||||
search_blender_version_max: StringProperty(
|
||||
name="Maximum version (excluding, lower than)",
|
||||
default="5.99",
|
||||
description="Limit the assets by maximum version of Blender in which they were created, exluding the specified version and all newer versions from the search results. "
|
||||
description="Limit the assets by maximum version of Blender in which they were created, excluding the specified version and all newer versions from the search results. "
|
||||
+ "Only assets created in LOWER THAN (< max) maximum version will be shown. Use semantic versioning format: X.Y.Z.\n\n"
|
||||
+ "E.g.: exclude all Blender 4 assets by specifying 4, 4.0, or 4.0.0. Assets created in 3.6 and lower will be shown",
|
||||
update=search.search_update,
|
||||
@@ -580,7 +586,7 @@ class BlenderKitUIProps(PropertyGroup):
|
||||
|
||||
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
|
||||
name="Rating UI Height", default=rating_ui_scale * 256
|
||||
)
|
||||
|
||||
quality_stars_x: IntProperty(name="Rating UI Stars X", default=rating_ui_scale * 90)
|
||||
@@ -1130,6 +1136,7 @@ class BlenderKitMaterialUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
update=autothumb.update_upload_material_preview,
|
||||
**EXTRA_PATH_OPTIONS,
|
||||
)
|
||||
|
||||
is_generating_thumbnail: BoolProperty(
|
||||
@@ -1213,13 +1220,14 @@ class BlenderKitBrushUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
)
|
||||
|
||||
|
||||
class BlenderKitNodeGroulUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
class BlenderKitNodeGroupUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
thumbnail: StringProperty(
|
||||
name="Thumbnail",
|
||||
description="Thumbnail path - minimum 1024x1024 square .jpg\n"
|
||||
"And make it beautiful!",
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
**EXTRA_PATH_OPTIONS,
|
||||
# update=autothumb.update_upload_model_preview,
|
||||
)
|
||||
# mode: EnumProperty(
|
||||
@@ -1326,6 +1334,7 @@ class BlenderKitModelUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
update=autothumb.update_upload_model_preview,
|
||||
**EXTRA_PATH_OPTIONS,
|
||||
)
|
||||
|
||||
thumbnail_background_lightness: FloatProperty(
|
||||
@@ -1529,6 +1538,7 @@ class BlenderKitModelUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
description="Photo of the 3D printed object (JPG or PNG, preferred size is 1024x1024 or higher)",
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
**EXTRA_PATH_OPTIONS,
|
||||
)
|
||||
photo_thumbnail_will_upload_on_website: BoolProperty(
|
||||
name="I will upload photo on website",
|
||||
@@ -1603,6 +1613,7 @@ class BlenderKitSceneUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
update=autothumb.update_upload_scene_preview,
|
||||
**EXTRA_PATH_OPTIONS,
|
||||
)
|
||||
|
||||
use_design_year: BoolProperty(
|
||||
@@ -1766,7 +1777,7 @@ class BlenderKitModelSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
||||
update=search.search_update,
|
||||
)
|
||||
search_design_year: BoolProperty(
|
||||
name="Sesigned in Year",
|
||||
name="Designed in Year",
|
||||
description="When the object was approximately designed. \n"
|
||||
"Useful for search of historical or future objects",
|
||||
default=False,
|
||||
@@ -1966,7 +1977,7 @@ def fix_subdir(self, context):
|
||||
|
||||
ui_panels.ui_message(
|
||||
title="Fixed to relative path",
|
||||
message="This path should be always realative.\n"
|
||||
message="This path should be always relative.\n"
|
||||
" It's a directory BlenderKit creates where your .blend is \n "
|
||||
"and uses it for storing assets.",
|
||||
)
|
||||
@@ -1992,7 +2003,7 @@ class BlenderKitAddonPreferences(AddonPreferences):
|
||||
|
||||
preferences_lock: BoolProperty(
|
||||
name="Preferences Locked",
|
||||
description="When this is on, preferences will not be saved. Used for programatical changes of preferences",
|
||||
description="When this is on, preferences will not be saved. Used for programmatic changes of preferences",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@@ -2120,6 +2131,14 @@ class BlenderKitAddonPreferences(AddonPreferences):
|
||||
update=utils.save_prefs,
|
||||
)
|
||||
|
||||
# USE OF CLIPBOARD SCAN
|
||||
use_clipboard_scan: BoolProperty(
|
||||
name="Use Clipboard Scan",
|
||||
description="Use the info from BlenderKit website clipboard for visual search",
|
||||
default=True,
|
||||
update=utils.save_prefs,
|
||||
)
|
||||
|
||||
unpack_files: BoolProperty(
|
||||
name="Unpack Files",
|
||||
description="Unpack assets after download \n "
|
||||
@@ -2233,8 +2252,8 @@ class BlenderKitAddonPreferences(AddonPreferences):
|
||||
|
||||
proxy_address: StringProperty(
|
||||
name="Custom proxy address",
|
||||
description="""Set custom HTTP proxy for HTTPS requests of add-on. This setting preceeds any system wide proxy settings. If left empty custom proxy will not be set.
|
||||
|
||||
description="""Set custom HTTP proxy for HTTPS requests of add-on. This setting precedes any system wide proxy settings. If left empty custom proxy will not be set.
|
||||
|
||||
If you use simple HTTP proxy, set in format http://ip:port, or http://username:password@ip:port if your HTTP proxy requires authentication (make sure to escape special characters like #$%:^&*() etc. in username and password). You have to specify the address with http:// prefix.
|
||||
|
||||
HTTPS proxies are not supported! We wait for support in Python 3.11 and in aiohttp module. You can specify the HTTPS proxy with https:// prefix for hacking around and development purposes, but functionality cannot be guaranteed.
|
||||
@@ -2430,12 +2449,21 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
default="[]",
|
||||
)
|
||||
|
||||
# EXPERIMENTAL AND DEBUG FEATURES CAN GO BELOW
|
||||
ignore_env_for_thumbnails: BoolProperty(
|
||||
name="Ignore ENVIRONMENT variables for thumbnails",
|
||||
description="If enabled, we will not modify the system environment variables for background thumbnail rendering.",
|
||||
default=False,
|
||||
# do not save prefs here, it's experimental
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if self.api_key.strip() == "":
|
||||
ui_panels.draw_login_buttons(layout)
|
||||
layout.label(
|
||||
text="Sign up to bookmark your favourite assets. Get 200 MiB of private storage in Free Plan."
|
||||
text="Sign up to bookmark your favorite assets. Get 200 MiB of private storage in Free Plan."
|
||||
)
|
||||
else:
|
||||
layout.operator("wm.blenderkit_logout", text="Logout", icon="URL")
|
||||
@@ -2470,8 +2498,9 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
gui_settings.prop(self, "show_VIEW3D_MT_blenderkit_model_properties")
|
||||
gui_settings.prop(self, "tips_on_start")
|
||||
gui_settings.prop(self, "announcements_on_start")
|
||||
gui_settings.prop(self, "use_clipboard_scan")
|
||||
|
||||
# NETWORKING SETINGS
|
||||
# NETWORKING SETTINGS
|
||||
network_settings = layout.box()
|
||||
network_settings.alignment = "EXPAND"
|
||||
network_settings.label(text="Networking settings")
|
||||
@@ -2487,6 +2516,14 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
# UPDATER SETTINGS
|
||||
addon_updater_ops.update_settings_ui(self, context)
|
||||
|
||||
# EXPERIMENTAL SETTINGS
|
||||
# only if experimental features enabled
|
||||
if self.experimental_features:
|
||||
experimental_settings = layout.box()
|
||||
experimental_settings.alignment = "EXPAND"
|
||||
experimental_settings.label(text="Experimental settings")
|
||||
experimental_settings.prop(self, "ignore_env_for_thumbnails")
|
||||
|
||||
# RUNTIME INFO
|
||||
globdir_op = layout.operator(
|
||||
"wm.blenderkit_open_global_directory",
|
||||
@@ -2535,7 +2572,7 @@ classes = (
|
||||
BlenderKitBrushSearchProps,
|
||||
BlenderKitBrushUploadProps,
|
||||
BlenderKitGeoToolSearchProps,
|
||||
BlenderKitNodeGroulUploadProps,
|
||||
BlenderKitNodeGroupUploadProps,
|
||||
BlenderKitAddonSearchProps,
|
||||
)
|
||||
|
||||
@@ -2598,10 +2635,10 @@ def register():
|
||||
type=BlenderKitGeoToolSearchProps
|
||||
)
|
||||
bpy.types.NodeGroup.blenderkit = PointerProperty( # for uploads, not now...
|
||||
type=BlenderKitNodeGroulUploadProps
|
||||
type=BlenderKitNodeGroupUploadProps
|
||||
)
|
||||
bpy.types.NodeTree.blenderkit = PointerProperty( # for uploads, not now...
|
||||
type=BlenderKitNodeGroulUploadProps
|
||||
type=BlenderKitNodeGroupUploadProps
|
||||
)
|
||||
bpy.types.WindowManager.blenderkit_addon = PointerProperty(
|
||||
type=BlenderKitAddonSearchProps
|
||||
|
||||
@@ -97,7 +97,13 @@ def make_annotations(cls):
|
||||
if bl_props:
|
||||
if "__annotations__" not in cls.__dict__:
|
||||
setattr(cls, "__annotations__", {})
|
||||
annotations = cls.__dict__["__annotations__"]
|
||||
|
||||
try:
|
||||
annotations = cls.__dict__["__annotations__"]
|
||||
except KeyError:
|
||||
# Fedora 43 bug workaround #1823
|
||||
annotations = getattr(cls, "__annotations__")
|
||||
|
||||
for k, v in bl_props.items():
|
||||
annotations[k] = v
|
||||
delattr(cls, k)
|
||||
|
||||
@@ -42,15 +42,19 @@ def find_layer_collection(layer_collection, collection_name):
|
||||
|
||||
def append_brush(file_name, brushname=None, link=False, fake_user=True):
|
||||
"""append a brush"""
|
||||
brushes_before = bpy.data.brushes[:]
|
||||
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:
|
||||
if brushname is None or m.strip() == brushname.strip():
|
||||
data_to.brushes = [m]
|
||||
brushname = m
|
||||
brush = bpy.data.brushes[brushname]
|
||||
for b in bpy.data.brushes:
|
||||
if b not in brushes_before:
|
||||
brush = b
|
||||
break
|
||||
brush.use_fake_user = fake_user
|
||||
return brush
|
||||
|
||||
@@ -93,8 +97,7 @@ def append_nodegroup(
|
||||
data_to,
|
||||
):
|
||||
for g in data_from.node_groups:
|
||||
print(g)
|
||||
if g == nodegroupname or nodegroupname is None:
|
||||
if nodegroupname is None or g.strip() == nodegroupname.strip():
|
||||
data_to.node_groups = [g]
|
||||
nodegroupname = g
|
||||
nodegroup = bpy.data.node_groups[nodegroupname]
|
||||
@@ -281,7 +284,7 @@ def append_material(file_name, matname=None, link=False, fake_user=True):
|
||||
):
|
||||
found = False
|
||||
for m in data_from.materials:
|
||||
if m == matname or matname is None:
|
||||
if matname is None or m.strip() == matname.strip():
|
||||
data_to.materials = [m]
|
||||
matname = m
|
||||
found = True
|
||||
@@ -319,7 +322,7 @@ def append_scene(file_name, scenename=None, link=False, fake_user=False):
|
||||
data_to,
|
||||
):
|
||||
for s in data_from.scenes:
|
||||
if s == scenename or scenename is None:
|
||||
if scenename is None or s.strip() == scenename.strip():
|
||||
data_to.scenes = [s]
|
||||
scenename = s
|
||||
scene = bpy.data.scenes[scenename]
|
||||
@@ -448,7 +451,7 @@ def link_collection(
|
||||
data_to,
|
||||
):
|
||||
for col in data_from.collections:
|
||||
if col == kwargs["name"]:
|
||||
if col.strip() == kwargs["name"].strip():
|
||||
data_to.collections = [col]
|
||||
|
||||
rotation = (0, 0, 0)
|
||||
|
||||
@@ -21,7 +21,7 @@ import math
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, StringProperty
|
||||
@@ -286,6 +286,12 @@ def modal_inside(self, context, event):
|
||||
if self.check_ui_resized(context) or self.check_new_search_results(context):
|
||||
self.update_assetbar_sizes(context)
|
||||
self.update_assetbar_layout(context)
|
||||
# also update tooltip visibility
|
||||
# if there's less results and active button is not visible, hide tooltip
|
||||
# happened only when e.g. running new search from web browser (copying assetbaseid to clipboard)
|
||||
# fixes issue #1766
|
||||
if self.active_index >= len(search.get_search_results()):
|
||||
self.hide_tooltip()
|
||||
self.scroll_update(
|
||||
always=True
|
||||
) # one extra update for scroll for correct redraw, updates all buttons
|
||||
@@ -395,6 +401,17 @@ def get_tooltip_data(asset_data):
|
||||
# Add pricing information
|
||||
price_text = ""
|
||||
price_color = colors.WHITE
|
||||
price_background = (0, 0, 0, 0)
|
||||
|
||||
def format_price(value):
|
||||
if value is None:
|
||||
return ""
|
||||
value_str = str(value).strip()
|
||||
if not value_str:
|
||||
return ""
|
||||
if value_str.startswith("$"):
|
||||
return value_str
|
||||
return f"${value_str}"
|
||||
|
||||
# Check if asset is free or paid (works for all asset types)
|
||||
is_free = asset_data.get("isFree", True)
|
||||
@@ -403,23 +420,38 @@ def get_tooltip_data(asset_data):
|
||||
if asset_data.get("assetType") == "addon":
|
||||
# Get pricing info from extensions cache.
|
||||
# Pricing info is shown only for add-ons.
|
||||
base_price = asset_data.get("basePrice")
|
||||
base_price = format_price(asset_data.get("basePrice"))
|
||||
user_price = format_price(asset_data.get("userPrice"))
|
||||
is_for_sale = asset_data.get("isForSale")
|
||||
|
||||
if is_for_sale and not can_download and base_price:
|
||||
price_text = f"${base_price}"
|
||||
price_color = colors.PURPLE
|
||||
if utils.profile_is_validator():
|
||||
segments = []
|
||||
if user_price:
|
||||
segments.append(f"User {user_price}")
|
||||
if base_price:
|
||||
segments.append(f"Base {base_price}")
|
||||
price_text = " | ".join(segments)
|
||||
price_background = colors.PURPLE_PRICE
|
||||
|
||||
elif is_for_sale and not can_download and user_price and base_price:
|
||||
price_text = f"{user_price} (was {base_price})"
|
||||
price_background = colors.PURPLE_PRICE
|
||||
|
||||
elif is_for_sale and not can_download and base_price:
|
||||
price_text = base_price
|
||||
price_background = colors.PURPLE_PRICE
|
||||
|
||||
elif not is_free and not is_for_sale:
|
||||
price_text = "Full Plan"
|
||||
price_color = colors.PURPLE
|
||||
elif (
|
||||
is_for_sale and can_download
|
||||
): # purchased, but not yet downloaded, so we can't show price
|
||||
price_text = f"Purchased (${base_price})"
|
||||
price_color = colors.PURPLE
|
||||
price_background = colors.ORANGE_FULL
|
||||
|
||||
elif is_for_sale and can_download:
|
||||
price_text = "Purchased"
|
||||
price_background = colors.PURPLE_PRICE
|
||||
|
||||
else:
|
||||
price_text = "Free"
|
||||
price_color = colors.GREEN_FREE
|
||||
price_background = colors.GREEN_PRICE
|
||||
|
||||
tooltip_data = {
|
||||
"aname": aname,
|
||||
@@ -427,12 +459,15 @@ def get_tooltip_data(asset_data):
|
||||
"quality": quality,
|
||||
"price_text": price_text,
|
||||
"price_color": price_color,
|
||||
"price_background": price_background,
|
||||
}
|
||||
asset_data["tooltip_data"] = tooltip_data
|
||||
|
||||
|
||||
def set_thumb_check(
|
||||
element: BL_UI_Button, asset: Dict[str, Any], thumb_type: str = "thumbnail_small"
|
||||
element: Union[BL_UI_Button, BL_UI_Image],
|
||||
asset: Dict[str, Any],
|
||||
thumb_type: str = "thumbnail_small",
|
||||
) -> None:
|
||||
"""Set image in case it is loaded in search results. Checks global_vars.DATA["images available"].
|
||||
- if image download failed, it will be set to 'thumbnail_not_available.jpg'
|
||||
@@ -457,6 +492,8 @@ def set_thumb_check(
|
||||
|
||||
|
||||
class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
"""BlenderKit Asset Bar Operator."""
|
||||
|
||||
bl_idname = "view3d.blenderkit_asset_bar_widget"
|
||||
bl_label = "BlenderKit asset bar refresh"
|
||||
bl_description = "BlenderKit asset bar refresh"
|
||||
@@ -508,8 +545,23 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
"""Initialize the tooltip panel and its widgets."""
|
||||
self.tooltip_widgets = []
|
||||
self.tooltip_scale = 1.0
|
||||
self.tooltip_height = self.tooltip_size
|
||||
self.tooltip_width = self.tooltip_size
|
||||
|
||||
# Fallbacks in case update_tooltip_size was not called yet
|
||||
self.tooltip_width = getattr(self, "tooltip_width", self.tooltip_size)
|
||||
image_height = getattr(self, "tooltip_image_height", self.tooltip_size)
|
||||
info_height = getattr(
|
||||
self,
|
||||
"tooltip_info_height",
|
||||
max(
|
||||
int(image_height * self.bottom_panel_fraction),
|
||||
self.asset_name_text_size * 3,
|
||||
),
|
||||
)
|
||||
self.tooltip_image_height = image_height
|
||||
self.tooltip_info_height = info_height
|
||||
self.tooltip_height = self.tooltip_image_height + self.tooltip_info_height
|
||||
self.labels_start = self.tooltip_image_height
|
||||
|
||||
# total_size = tooltip# + 2 * self.margin
|
||||
self.tooltip_panel = BL_UI_Drag_Panel(
|
||||
0, 0, self.tooltip_width, self.tooltip_height
|
||||
@@ -520,20 +572,16 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
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_size((self.tooltip_width, self.tooltip_image_height))
|
||||
tooltip_image.set_image_position((0, 0))
|
||||
tooltip_image.set_image_colorspace("")
|
||||
self.tooltip_image = tooltip_image
|
||||
self.tooltip_widgets.append(tooltip_image)
|
||||
|
||||
self.bottom_panel_fraction = 0.15
|
||||
self.labels_start = self.tooltip_height * (1 - self.bottom_panel_fraction)
|
||||
|
||||
dark_panel = BL_UI_Widget(
|
||||
0,
|
||||
self.labels_start,
|
||||
self.tooltip_width,
|
||||
self.tooltip_height * self.bottom_panel_fraction,
|
||||
self.tooltip_info_height,
|
||||
)
|
||||
dark_panel.bg_color = (0.0, 0.0, 0.0, 0.7)
|
||||
self.tooltip_dark_panel = dark_panel
|
||||
@@ -549,8 +597,9 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.asset_name = name_label
|
||||
self.tooltip_widgets.append(name_label)
|
||||
|
||||
self.gravatar_size = int(
|
||||
self.tooltip_height * self.bottom_panel_fraction - self.tooltip_margin
|
||||
self.gravatar_size = max(
|
||||
int(self.tooltip_info_height - 2 * self.tooltip_margin),
|
||||
self.asset_name_text_size,
|
||||
)
|
||||
|
||||
authors_name = self.new_text(
|
||||
@@ -566,8 +615,8 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.tooltip_widgets.append(authors_name)
|
||||
|
||||
gravatar_image = BL_UI_Image(
|
||||
self.tooltip_width - self.gravatar_size,
|
||||
self.tooltip_height - self.gravatar_size,
|
||||
self.tooltip_width - self.gravatar_size - self.tooltip_margin,
|
||||
self.tooltip_height - self.gravatar_size - self.tooltip_margin,
|
||||
1,
|
||||
1,
|
||||
)
|
||||
@@ -575,8 +624,8 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
gravatar_image.set_image(img_path)
|
||||
gravatar_image.set_image_size(
|
||||
(
|
||||
self.gravatar_size - 1 * self.tooltip_margin,
|
||||
self.gravatar_size - 1 * self.tooltip_margin,
|
||||
self.gravatar_size,
|
||||
self.gravatar_size,
|
||||
)
|
||||
)
|
||||
gravatar_image.set_image_position((0, 0))
|
||||
@@ -617,7 +666,14 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
height=self.asset_name_text_size,
|
||||
text_size=self.asset_name_text_size,
|
||||
)
|
||||
price_label.text_color = (1.0, 0.8, 0.2, 1.0) # Golden color for price
|
||||
price_label.background = True
|
||||
price_label.padding = (3, 4)
|
||||
price_label.text_color = (
|
||||
1.0,
|
||||
0.8,
|
||||
0.2,
|
||||
1.0,
|
||||
) # Golden color for price
|
||||
self.tooltip_widgets.append(price_label)
|
||||
self.price_label = price_label
|
||||
|
||||
@@ -728,14 +784,30 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
"""Calculate all important sizes for the tooltip"""
|
||||
region = context.region
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
ui_scale = bpy.context.preferences.view.ui_scale
|
||||
ui_scale = self.get_ui_scale()
|
||||
|
||||
base_panel_height = self.tooltip_base_size_pixels * (
|
||||
1 + self.bottom_panel_fraction
|
||||
)
|
||||
|
||||
if hasattr(self, "tooltip_panel"):
|
||||
tooltip_y_offset = abs(region.height - self.tooltip_panel.y_screen)
|
||||
tooltip_y_available_height = abs(
|
||||
region.height - self.tooltip_panel.y_screen
|
||||
)
|
||||
# if tooltip is above, we need to reduce it's size if its y is out of region height
|
||||
if self.tooltip_panel.y_screen <= 0:
|
||||
tooltip_y_available_height = (
|
||||
base_panel_height * ui_scale + self.tooltip_panel.y_screen
|
||||
)
|
||||
self.tooltip_panel.set_location(self.tooltip_panel.x, 0)
|
||||
|
||||
else:
|
||||
tooltip_y_offset = abs(region.height - (self.bar_height + self.bar_y))
|
||||
tooltip_y_available_height = abs(
|
||||
region.height - (self.bar_height + self.bar_y)
|
||||
)
|
||||
|
||||
self.tooltip_scale = min(
|
||||
1.0, tooltip_y_offset / (self.tooltip_base_size_pixels * ui_scale)
|
||||
1.0, tooltip_y_available_height / (base_panel_height * ui_scale)
|
||||
)
|
||||
self.asset_name_text_size = int(
|
||||
0.039 * self.tooltip_base_size_pixels * ui_scale * self.tooltip_scale
|
||||
@@ -750,14 +822,33 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
|
||||
if ui_props.asset_type == "HDR":
|
||||
self.tooltip_width = self.tooltip_size * 2
|
||||
self.tooltip_height = self.tooltip_size
|
||||
self.tooltip_image_height = self.tooltip_size
|
||||
else:
|
||||
self.tooltip_width = self.tooltip_size
|
||||
self.tooltip_height = self.tooltip_size
|
||||
self.tooltip_image_height = self.tooltip_size
|
||||
|
||||
self.gravatar_size = int(
|
||||
self.tooltip_height * self.bottom_panel_fraction - self.tooltip_margin
|
||||
self.tooltip_info_height = max(
|
||||
int(self.tooltip_image_height * self.bottom_panel_fraction),
|
||||
self.asset_name_text_size * 3,
|
||||
)
|
||||
self.labels_start = self.tooltip_image_height
|
||||
self.tooltip_height = self.tooltip_image_height + self.tooltip_info_height
|
||||
|
||||
self.gravatar_size = max(
|
||||
int(self.tooltip_info_height - 2 * self.tooltip_margin),
|
||||
self.asset_name_text_size,
|
||||
)
|
||||
|
||||
def get_ui_scale(self):
|
||||
"""Get the UI scale"""
|
||||
ui_scale = bpy.context.preferences.view.ui_scale
|
||||
pixel_size = bpy.context.preferences.system.pixel_size
|
||||
if pixel_size > 1:
|
||||
# for a reason unknown,
|
||||
# the pixel size is modified only on mac
|
||||
# where pixel size is 2.0
|
||||
ui_scale = pixel_size
|
||||
return ui_scale
|
||||
|
||||
def update_assetbar_sizes(self, context):
|
||||
"""Calculate all important sizes for the asset bar"""
|
||||
@@ -766,8 +857,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
user_preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
ui_scale = bpy.context.preferences.view.ui_scale
|
||||
|
||||
ui_scale = self.get_ui_scale()
|
||||
# assetbar scaling
|
||||
self.button_margin = int(0 * ui_scale)
|
||||
self.assetbar_margin = int(2 * ui_scale)
|
||||
@@ -793,6 +883,10 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.bar_x = int(
|
||||
tools_width + self.button_margin + ui_props.bar_x_offset * ui_scale
|
||||
)
|
||||
# self.bar_y = region.height - ui_props.bar_y_offset * ui_scale
|
||||
|
||||
self.bar_y = int(ui_props.bar_y_offset * ui_scale)
|
||||
|
||||
self.bar_end = int(ui_width + 180 * ui_scale + self.other_button_size)
|
||||
self.bar_width = int(region.width - self.bar_x - self.bar_end)
|
||||
|
||||
@@ -810,6 +904,16 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
if search_results is not None and self.wcount > 0:
|
||||
if user_preferences.assetbar_expanded:
|
||||
max_rows = user_preferences.maximized_assetbar_rows
|
||||
available_height = (
|
||||
region.height
|
||||
- self.bar_y
|
||||
- 2 * self.assetbar_margin
|
||||
- self.other_button_size
|
||||
)
|
||||
max_rows_by_height = math.floor(available_height / self.button_size)
|
||||
max_rows = (
|
||||
min(max_rows, max_rows_by_height) if max_rows_by_height > 0 else 1
|
||||
)
|
||||
else:
|
||||
max_rows = 1
|
||||
self.hcount = min(
|
||||
@@ -821,8 +925,6 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
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 = int(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
|
||||
@@ -886,26 +988,28 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.tooltip_panel.width = self.tooltip_width
|
||||
self.tooltip_panel.height = self.tooltip_height
|
||||
self.tooltip_image.width = self.tooltip_width
|
||||
self.tooltip_image.height = self.tooltip_height
|
||||
self.tooltip_image.height = self.tooltip_image_height
|
||||
|
||||
self.labels_start = self.tooltip_height * (1 - self.bottom_panel_fraction)
|
||||
self.labels_start = self.tooltip_image_height
|
||||
|
||||
self.tooltip_image.set_image_size((self.tooltip_width, self.tooltip_height))
|
||||
self.tooltip_image.set_image_size(
|
||||
(self.tooltip_width, self.tooltip_image_height)
|
||||
)
|
||||
self.tooltip_image.set_location(0, 0)
|
||||
|
||||
self.gravatar_image.set_location(
|
||||
self.tooltip_width - self.gravatar_size,
|
||||
self.tooltip_height - self.gravatar_size,
|
||||
self.tooltip_width - self.gravatar_size - self.tooltip_margin,
|
||||
self.tooltip_height - self.gravatar_size - self.tooltip_margin,
|
||||
)
|
||||
self.gravatar_image.set_image_size(
|
||||
(
|
||||
self.gravatar_size - 1 * self.tooltip_margin,
|
||||
self.gravatar_size - 1 * self.tooltip_margin,
|
||||
self.gravatar_size,
|
||||
self.gravatar_size,
|
||||
)
|
||||
)
|
||||
|
||||
self.authors_name.set_location(
|
||||
self.tooltip_width - self.gravatar_size - self.tooltip_margin,
|
||||
self.tooltip_width - self.gravatar_size - (self.tooltip_margin * 2),
|
||||
self.tooltip_height - self.author_text_size - self.tooltip_margin,
|
||||
)
|
||||
self.authors_name.text_size = self.author_text_size
|
||||
@@ -922,9 +1026,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
0,
|
||||
self.labels_start,
|
||||
)
|
||||
self.tooltip_dark_panel.height = (
|
||||
self.tooltip_height * self.bottom_panel_fraction
|
||||
)
|
||||
self.tooltip_dark_panel.height = self.tooltip_info_height
|
||||
self.tooltip_dark_panel.width = self.tooltip_width
|
||||
|
||||
self.quality_label.set_location(
|
||||
@@ -942,6 +1044,15 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
(self.asset_name_text_size, self.asset_name_text_size)
|
||||
)
|
||||
|
||||
# right after the asset name
|
||||
self.price_label.set_location(
|
||||
self.tooltip_margin,
|
||||
self.labels_start + (self.tooltip_margin * 3) + self.asset_name.height,
|
||||
)
|
||||
self.price_label.width = self.tooltip_width - 2 * self.tooltip_margin
|
||||
self.price_label.height = self.asset_name_text_size
|
||||
self.price_label.text_size = self.asset_name_text_size
|
||||
|
||||
def update_layout(self, context, event):
|
||||
"""update UI sizes after their recalculation"""
|
||||
self.update_assetbar_layout(context)
|
||||
@@ -1044,6 +1155,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.button_bg_color = (0.2, 0.2, 0.2, 1.0)
|
||||
self.button_hover_color = (0.8, 0.8, 0.8, 1.0)
|
||||
self.button_selected_color = (0.5, 0.5, 0.5, 1.0)
|
||||
self.button_selected_color_dim = (0.3, 0.3, 0.3, 1.0)
|
||||
|
||||
self.buttons = []
|
||||
self.asset_buttons = []
|
||||
@@ -1072,7 +1184,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.other_button_size, # Same height as tab buttons
|
||||
)
|
||||
# dark blue
|
||||
self.tab_area_bg.bg_color = (0.2, 0.25, 0.4, 1.0)
|
||||
self.tab_area_bg.bg_color = colors.TOP_BAR_BLUE
|
||||
|
||||
# Add widgets to panel - add tab background first so it's behind everything
|
||||
self.widgets_panel.append(self.tab_area_bg)
|
||||
@@ -1162,8 +1274,11 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
# Add tab navigation elements
|
||||
button_size = self.other_button_size
|
||||
margin = int(button_size * 0.05)
|
||||
space = int(button_size * 0.4)
|
||||
tab_icon_size = int(button_size * 0.7) # Size for the asset type icon
|
||||
tab_width = button_size * 4 # Wider tabs to accommodate icon
|
||||
tab_width = (
|
||||
button_size * 4 + tab_icon_size
|
||||
) # Widen the tabs to accommodate type icon
|
||||
|
||||
# Back/Forward history buttons
|
||||
self.history_back_button = BL_UI_Button(
|
||||
@@ -1199,10 +1314,14 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
tabs = global_vars.TABS["tabs"]
|
||||
tab_x_start = margin * 4 + button_size * 3 # Starting x position of first tab
|
||||
|
||||
tabs_end_x = 0
|
||||
|
||||
for i, tab in enumerate(tabs):
|
||||
is_active = i == global_vars.TABS["active_tab"]
|
||||
|
||||
# Calculate positions
|
||||
tab_x = tab_x_start + i * (
|
||||
tab_width + button_size + margin
|
||||
tab_width + button_size + margin + space
|
||||
) # Space for tab and close button
|
||||
|
||||
# Tab button
|
||||
@@ -1212,13 +1331,15 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
tab_width, # Width of tab
|
||||
button_size,
|
||||
)
|
||||
tab_button.bg_color = self.button_bg_color
|
||||
if i == global_vars.TABS["active_tab"]:
|
||||
tab_button.bg_color = self.button_selected_color
|
||||
|
||||
tab_button.hover_bg_color = self.button_hover_color
|
||||
tab_button.text = tab["name"]
|
||||
tab_button.text_size = button_size * 0.5
|
||||
tab_button.text_color = self.text_color
|
||||
tab_button.bg_color = self.button_bg_color
|
||||
if is_active:
|
||||
tab_button.bg_color = self.button_selected_color
|
||||
|
||||
tab_button.tab_index = i # Store tab index
|
||||
tab_button.set_mouse_down(self.switch_tab) # Add click handler
|
||||
self.tab_buttons.append(tab_button)
|
||||
@@ -1226,7 +1347,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
# Set asset type icon as tab button image
|
||||
tab_button.set_image_size((tab_icon_size, tab_icon_size))
|
||||
tab_button.set_image_position(
|
||||
(margin, (button_size - tab_icon_size) / 2)
|
||||
(margin * 2, (button_size - tab_icon_size) / 2)
|
||||
) # Center vertically
|
||||
|
||||
# Only create close button if there's more than one tab
|
||||
@@ -1243,22 +1364,24 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
close_tab.text = "×" # Set text after creation
|
||||
close_tab.text_size = button_size * 0.8
|
||||
close_tab.text_color = self.text_color
|
||||
if is_active:
|
||||
close_tab.bg_color = self.button_selected_color_dim
|
||||
|
||||
close_tab.tab_index = i # Store tab index
|
||||
# if there's only one tab, the button closes asset bar instead of closing tab
|
||||
if len(tabs) > 1:
|
||||
close_tab.set_mouse_down(self.remove_tab) # Add click handler
|
||||
else:
|
||||
close_tab.set_mouse_down(self.cancel_press)
|
||||
|
||||
self.close_tab_buttons.append(close_tab)
|
||||
|
||||
tabs_end_x = close_x + button_size
|
||||
|
||||
# New tab button - position after all tabs and close buttons
|
||||
if len(tabs) > 0:
|
||||
last_tab_index = len(tabs) - 1
|
||||
last_tab_x = tab_x_start + last_tab_index * (
|
||||
tab_width + button_size + margin
|
||||
)
|
||||
new_tab_x = (
|
||||
last_tab_x + tab_width + button_size + margin * 2
|
||||
space + tabs_end_x + margin * 2
|
||||
) # After last tab and its close button
|
||||
else:
|
||||
new_tab_x = tab_x_start # If no tabs, start at the beginning
|
||||
@@ -1302,8 +1425,6 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
active_tab["history_index"] < len(active_tab["history"]) - 1
|
||||
)
|
||||
|
||||
# self.update_buttons()
|
||||
|
||||
def set_element_images(self):
|
||||
"""set ui elements images, has to be done after init of UI."""
|
||||
# img_fp = paths.get_addon_thumbnail_path("vs_rejected.png")
|
||||
@@ -1345,7 +1466,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
icon_path = paths.get_addon_thumbnail_path(
|
||||
f"asset_type_{asset_type}.png"
|
||||
)
|
||||
if not os.path.exists(icon_path):
|
||||
if not paths.icon_path_exists(icon_path):
|
||||
icon_path = paths.get_addon_thumbnail_path(
|
||||
"asset_type_model.png"
|
||||
)
|
||||
@@ -1444,7 +1565,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
"""Initialize the asset bar operator."""
|
||||
self.tooltip_base_size_pixels = 512
|
||||
self.tooltip_scale = 1.0
|
||||
self.bottom_panel_fraction = 0.15
|
||||
self.bottom_panel_fraction = 0.18
|
||||
self.needs_tooltip_update = False
|
||||
self.update_ui_size(bpy.context)
|
||||
|
||||
@@ -1679,9 +1800,13 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
price_color = asset_data["tooltip_data"].get(
|
||||
"price_color", (1.0, 0.8, 0.2, 1.0)
|
||||
)
|
||||
price_background = asset_data["tooltip_data"].get(
|
||||
"price_background", (0.2, 0.2, 0.2, 0.0)
|
||||
)
|
||||
self.price_label.text = price_text
|
||||
self.price_label.text_color = price_color
|
||||
self.price_label.visible = bool(price_text)
|
||||
self.price_label.bg_color = price_background
|
||||
|
||||
# preview comments for validators
|
||||
self.update_comments_for_validators(asset_data)
|
||||
@@ -1721,7 +1846,22 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
- properties_width
|
||||
),
|
||||
)
|
||||
tooltip_y = int(widget.y_screen + widget.height)
|
||||
|
||||
# Calculate space above and below the button
|
||||
ui_scale = self.get_ui_scale()
|
||||
full_tooltip_height = self.tooltip_panel.height
|
||||
space_above = widget.y_screen
|
||||
space_below = bpy.context.region.height - (widget.y_screen + widget.height)
|
||||
# If space below is insufficient (would make tooltip < 70% size), position above
|
||||
if (
|
||||
space_below < full_tooltip_height
|
||||
and space_below < full_tooltip_height * 0.7
|
||||
and space_below < space_above
|
||||
):
|
||||
tooltip_y = int(widget.y_screen - full_tooltip_height)
|
||||
else:
|
||||
tooltip_y = int(widget.y_screen + widget.height)
|
||||
|
||||
# need to set image here because of context issues.
|
||||
img_path = paths.get_addon_thumbnail_path("star_grey.png")
|
||||
self.quality_star.set_image(img_path)
|
||||
@@ -1730,7 +1870,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.tooltip_panel.set_location(tooltip_x, tooltip_y)
|
||||
self.update_tooltip_size(bpy.context)
|
||||
self.update_tooltip_layout(bpy.context)
|
||||
self.tooltip_panel.set_location(tooltip_x, tooltip_y)
|
||||
self.tooltip_panel.set_location(self.tooltip_panel.x, self.tooltip_panel.y)
|
||||
self.tooltip_panel.layout_widgets()
|
||||
# show bookmark button - always on mouse enter
|
||||
if widget.bookmark_button:
|
||||
@@ -2317,6 +2457,10 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
return # Already on this tab and history step
|
||||
# make original tab original background color
|
||||
self.tab_buttons[global_vars.TABS["active_tab"]].bg_color = self.button_bg_color
|
||||
# make also tab close button original background color
|
||||
self.close_tab_buttons[global_vars.TABS["active_tab"]].bg_color = (
|
||||
self.button_bg_color
|
||||
)
|
||||
|
||||
global_vars.TABS["active_tab"] = tab_index
|
||||
global_vars.TABS["tabs"][tab_index]["history_index"] = history_index
|
||||
@@ -2354,14 +2498,24 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
|
||||
# Update history button visibility
|
||||
active_tab = global_vars.TABS["tabs"][tab_index]
|
||||
|
||||
self.history_back_button.visible = active_tab["history_index"] > 0
|
||||
self.history_forward_button.visible = (
|
||||
active_tab["history_index"] < len(active_tab["history"]) - 1
|
||||
)
|
||||
|
||||
# make active tab a bit darker
|
||||
if len(self.tab_buttons) > tab_index:
|
||||
self.tab_buttons[tab_index].bg_color = self.button_selected_color
|
||||
# update tab colors
|
||||
for tab_button in self.tab_buttons:
|
||||
c_tab_index = tab_button.tab_index
|
||||
if c_tab_index == tab_index:
|
||||
tab_button.bg_color = self.button_selected_color
|
||||
self.close_tab_buttons[tab_index].bg_color = (
|
||||
self.button_selected_color_dim
|
||||
)
|
||||
|
||||
else:
|
||||
tab_button.bg_color = self.button_bg_color
|
||||
self.close_tab_buttons[c_tab_index].bg_color = self.button_bg_color
|
||||
|
||||
# update filters
|
||||
search.update_filters()
|
||||
|
||||
@@ -1467,8 +1467,18 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
Tuple[None, None, None],
|
||||
]:
|
||||
"""Find the window, region and area under the mouse cursor."""
|
||||
# Iterate windows backwards, so we go from the top-most window to the bottommost window
|
||||
for window in reversed(bpy.context.window_manager.windows):
|
||||
|
||||
wins = bpy.context.window_manager.windows[:]
|
||||
# reverse the list, seemed to work well at least on windows.
|
||||
wins.reverse()
|
||||
context_win = bpy.context.window
|
||||
|
||||
# let's prioritize the context window
|
||||
if context_win is not None:
|
||||
wins.remove(context_win)
|
||||
wins.insert(0, context_win)
|
||||
|
||||
for window in wins:
|
||||
# first let's test if it's in this window, so we know we shall continue
|
||||
window_x = window.x * self.resolution_factor
|
||||
window_y = window.y * self.resolution_factor
|
||||
|
||||
@@ -27,6 +27,8 @@ from . import utils
|
||||
|
||||
RENDER_OBTYPES = ["MESH", "CURVE", "SURFACE", "METABALL", "TEXT"]
|
||||
|
||||
_BLE_5_PLUS = bpy.app.version >= (5, 0, 0)
|
||||
|
||||
|
||||
def check_material(props, mat):
|
||||
e = bpy.context.scene.render.engine
|
||||
@@ -217,17 +219,39 @@ def check_rig(props, obs):
|
||||
props.rig = True
|
||||
|
||||
|
||||
def has_keyframes(obj):
|
||||
"""Checks if object has animation data with keyframes.
|
||||
|
||||
This function only checks for keyframes,
|
||||
may return false negatives for objects animated with constraints, drivers, etc.
|
||||
"""
|
||||
if obj.animation_data is None:
|
||||
return False
|
||||
|
||||
a = obj.animation_data.action
|
||||
if a is None:
|
||||
return False
|
||||
|
||||
# should work from at least Blender4.2+
|
||||
if _BLE_5_PLUS:
|
||||
# combined fcurves ranges
|
||||
# check if start and end frames are different
|
||||
if a.curve_frame_range[0] != a.curve_frame_range[1]:
|
||||
return True
|
||||
else:
|
||||
# older Blender versions
|
||||
for c in a.fcurves:
|
||||
if len(c.keyframe_points) > 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
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 has_keyframes(ob):
|
||||
animated = True
|
||||
break
|
||||
if animated:
|
||||
props.animated = True
|
||||
|
||||
|
||||
@@ -209,8 +209,17 @@ def start_model_thumbnailer(
|
||||
blender_user_scripts_dir = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
) # scripts/addons/blenderkit/autothumb.py
|
||||
|
||||
env = {"BLENDER_USER_SCRIPTS": str(blender_user_scripts_dir)}
|
||||
env.update(os.environ)
|
||||
|
||||
# both must be enabled
|
||||
if (
|
||||
user_preferences.experimental_features
|
||||
and user_preferences.ignore_env_for_thumbnails
|
||||
):
|
||||
env = None
|
||||
|
||||
proc = subprocess.Popen(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -218,7 +227,7 @@ def start_model_thumbnailer(
|
||||
creationflags=utils.get_process_flags(),
|
||||
env=env,
|
||||
)
|
||||
bk_logger.info(f"Started Blender executing {SCRIPT_NAME} on file {datafile}")
|
||||
bk_logger.info("Started Blender executing %s on file %s", SCRIPT_NAME, datafile)
|
||||
eval_path_computing = f"bpy.data.objects['{json_args['asset_name']}'].blenderkit.is_generating_thumbnail"
|
||||
eval_path_state = f"bpy.data.objects['{json_args['asset_name']}'].blenderkit.thumbnail_generating_state"
|
||||
eval_path = f"bpy.data.objects['{json_args['asset_name']}']"
|
||||
@@ -284,8 +293,16 @@ def start_material_thumbnailer(
|
||||
blender_user_scripts_dir = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
) # scripts/addons/blenderkit/autothumb.py
|
||||
|
||||
env = {"BLENDER_USER_SCRIPTS": str(blender_user_scripts_dir)}
|
||||
env.update(os.environ)
|
||||
|
||||
if (
|
||||
user_preferences.experimental_features
|
||||
and user_preferences.ignore_env_for_thumbnails
|
||||
):
|
||||
env = None
|
||||
|
||||
proc = subprocess.Popen(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -293,7 +310,7 @@ def start_material_thumbnailer(
|
||||
creationflags=utils.get_process_flags(),
|
||||
env=env,
|
||||
)
|
||||
bk_logger.info(f"Started Blender executing {SCRIPT_NAME} on file {datafile}")
|
||||
bk_logger.info("Started Blender executing %s on file %s", SCRIPT_NAME, datafile)
|
||||
|
||||
eval_path_computing = f"bpy.data.materials['{json_args['asset_name']}'].blenderkit.is_generating_thumbnail"
|
||||
eval_path_state = f"bpy.data.materials['{json_args['asset_name']}'].blenderkit.thumbnail_generating_state"
|
||||
|
||||
@@ -164,11 +164,20 @@ if __name__ == "__main__":
|
||||
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
|
||||
|
||||
# this option was moved in Blender 5.0 from cycles directly to modifier
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
for mod in ob.modifiers:
|
||||
if mod.type == "SUBSURF":
|
||||
if data["adaptive_subdivision"] == True:
|
||||
mod.use_adaptive_subdivision = True
|
||||
else:
|
||||
mod.use_adaptive_subdivision = False
|
||||
else:
|
||||
ob.cycles.use_adaptive_subdivision = False
|
||||
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(
|
||||
@@ -179,7 +188,13 @@ if __name__ == "__main__":
|
||||
)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
s.cycles.volume_step_size = tscale * 0.1
|
||||
# this option was removed in Blender 5.0
|
||||
# but we have option to set biased volumes
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
# usually small speedup with little quality loss
|
||||
s.cycles.volume_biased = True
|
||||
else:
|
||||
s.cycles.volume_step_size = tscale * 0.1
|
||||
|
||||
if thumbnail_use_gpu is True:
|
||||
bpy.context.scene.cycles.device = "GPU"
|
||||
|
||||
@@ -71,6 +71,9 @@ def threadread(tcom: ThreadCom):
|
||||
return # process terminated
|
||||
inline = tcom.proc.stdout.readline()
|
||||
inline = inline.decode("utf-8")
|
||||
# ignore empty lines
|
||||
if inline.strip() == "":
|
||||
continue
|
||||
bk_logger.info(inline.strip())
|
||||
progress = re.findall(r"progress\{(.*?)\}", inline)
|
||||
if len(progress) > 0:
|
||||
|
||||
@@ -139,8 +139,8 @@ def login(signup: bool) -> None:
|
||||
|
||||
|
||||
def generate_pkce_pair() -> tuple[str, str]:
|
||||
"""Generate PKCE pair - a code verifier and code challange.
|
||||
The challange should be sent first to the server, the verifier is used in next steps to verify identity (handles Client).
|
||||
"""Generate PKCE pair - a code verifier and code challenge.
|
||||
The challenge should be sent first to the server, the verifier is used in next steps to verify identity (handles Client).
|
||||
"""
|
||||
rand = random.SystemRandom()
|
||||
code_verifier = "".join(rand.choices(string.ascii_letters + string.digits, k=128))
|
||||
@@ -162,8 +162,6 @@ def write_tokens(auth_token, refresh_token, oauth_response):
|
||||
override_extension_draw.ensure_repository(api_key=auth_token)
|
||||
override_extension_draw.clear_repo_cache()
|
||||
|
||||
#
|
||||
|
||||
|
||||
def ensure_token_refresh() -> bool:
|
||||
"""Check if API token needs refresh, call refresh and return True if so.
|
||||
|
||||
+35
-2
@@ -20,7 +20,7 @@
|
||||
bl_info = {
|
||||
"name": "BlenderKit Online Asset Library",
|
||||
"author": "Vilem Duha, Petr Dlouhy, A. Gajdosik",
|
||||
"version": (3, 17, 0, 251008), # X.Y.Z.yymmdd
|
||||
"version": (3, 18, 0, 251121), # X.Y.Z.yymmdd
|
||||
"blender": (3, 0, 0),
|
||||
"location": "View3D > Properties > BlenderKit",
|
||||
"description": "Boost your workflow with drag&drop assets from the community driven library.",
|
||||
@@ -28,7 +28,7 @@ bl_info = {
|
||||
"tracker_url": "https://github.com/BlenderKit/blenderkit/issues",
|
||||
"category": "3D View",
|
||||
}
|
||||
VERSION = (3, 17, 0, 251008)
|
||||
VERSION = (3, 18, 0, 251121)
|
||||
|
||||
import logging
|
||||
import random
|
||||
@@ -297,6 +297,11 @@ def asset_type_callback(self, context):
|
||||
6,
|
||||
),
|
||||
]
|
||||
|
||||
# Only add addon option for Blender 4.2+
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
items.append(("ADDON", "Add-ons", "Find add-ons", "PLUGIN", 7))
|
||||
else:
|
||||
items = [
|
||||
("MODEL", "Model", "Upload a model", "OBJECT_DATAMODE", 0),
|
||||
@@ -314,6 +319,11 @@ def asset_type_callback(self, context):
|
||||
),
|
||||
]
|
||||
|
||||
# Only add addon option for Blender 4.2+
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
items.append(("ADDON", "Add-on", "Upload an addon", "PLUGIN", 7))
|
||||
|
||||
return items
|
||||
|
||||
|
||||
@@ -1164,6 +1174,19 @@ class BlenderKitGeoToolSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
||||
pass
|
||||
|
||||
|
||||
class BlenderKitAddonSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
||||
search_installed: BoolProperty(
|
||||
name="Installed Only",
|
||||
description="Show only addons that are already installed in Blender",
|
||||
default=False,
|
||||
update=lambda self, context: (
|
||||
search.refresh_search()
|
||||
if context.window_manager.blenderkitUI.asset_type == "ADDON"
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class BlenderKitHDRUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
texture_resolution_max: IntProperty(
|
||||
name="Texture Resolution Max",
|
||||
@@ -2401,6 +2424,12 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
update=search.search_update,
|
||||
) # In future we can subsets like sexualized, pornography or violence subset. And allow users choose what is part of NSFW.
|
||||
|
||||
temp_enabled_addons: StringProperty(
|
||||
name="Temporarily Enabled Addons",
|
||||
description="JSON string of temporarily enabled addon package IDs that should be disabled on next session",
|
||||
default="[]",
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if self.api_key.strip() == "":
|
||||
@@ -2507,6 +2536,7 @@ classes = (
|
||||
BlenderKitBrushUploadProps,
|
||||
BlenderKitGeoToolSearchProps,
|
||||
BlenderKitNodeGroulUploadProps,
|
||||
BlenderKitAddonSearchProps,
|
||||
)
|
||||
|
||||
|
||||
@@ -2573,6 +2603,9 @@ def register():
|
||||
bpy.types.NodeTree.blenderkit = PointerProperty( # for uploads, not now...
|
||||
type=BlenderKitNodeGroulUploadProps
|
||||
)
|
||||
bpy.types.WindowManager.blenderkit_addon = PointerProperty(
|
||||
type=BlenderKitAddonSearchProps
|
||||
)
|
||||
if bpy.app.factory_startup is False:
|
||||
user_preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
global_vars.PREFS = utils.get_preferences_as_dict()
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
include:
|
||||
- "**/*.py"
|
||||
|
||||
exclude_dirs:
|
||||
- "tests"
|
||||
- ".venv"
|
||||
- "__pycache__"
|
||||
|
||||
skips:
|
||||
- "B404" # https://bandit.readthedocs.io/en/1.7.10/blacklists/blacklist_imports.html#b404-import-subprocess
|
||||
- "B603" # https://bandit.readthedocs.io/en/1.7.10/plugins/b603_subprocess_without_shell_equals_true.html
|
||||
- "B608" # https://bandit.readthedocs.io/en/1.7.10/plugins/b608_hardcoded_sql_expressions.html
|
||||
+17
@@ -101,6 +101,7 @@ def append_nodegroup(
|
||||
nodegroup.use_fake_user = fake_user
|
||||
|
||||
# Create target object automatically for geometry nodegroups when no target is provided
|
||||
auto_created_target: Optional[bpy.types.Object] = None
|
||||
if nodegroup.bl_rna.identifier == "GeometryNodeTree" and not target_object:
|
||||
# Create a default mesh cube
|
||||
bpy.ops.mesh.primitive_cube_add(
|
||||
@@ -109,6 +110,7 @@ def append_nodegroup(
|
||||
target_obj = bpy.context.active_object
|
||||
target_obj.name = "GeometryNodeTarget"
|
||||
target_object = target_obj.name
|
||||
auto_created_target = target_obj
|
||||
|
||||
# Make sure it's selected and active
|
||||
bpy.context.view_layer.objects.active = target_obj
|
||||
@@ -245,6 +247,21 @@ def append_nodegroup(
|
||||
added_to_editor = True
|
||||
break
|
||||
|
||||
# Ensure automatically created targets receive the nodegroup as modifier
|
||||
if auto_created_target:
|
||||
gn_mod = None
|
||||
for mod in auto_created_target.modifiers:
|
||||
if mod.type == "NODES":
|
||||
gn_mod = mod
|
||||
break
|
||||
if not gn_mod:
|
||||
gn_mod = auto_created_target.modifiers.new(
|
||||
name=nodegroup.name, type="NODES"
|
||||
)
|
||||
gn_mod.node_group = nodegroup
|
||||
auto_created_target.select_set(True)
|
||||
bpy.context.view_layer.objects.active = auto_created_target
|
||||
|
||||
return nodegroup, added_to_editor
|
||||
|
||||
|
||||
|
||||
+393
-298
File diff suppressed because it is too large
Load Diff
+989
-773
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -82,7 +82,7 @@ def get_texture_ui(tpath, iname):
|
||||
|
||||
|
||||
def check_thumbnail(props, imgpath):
|
||||
# TODO implement check if the file exists, if size is corect etc. needs some care
|
||||
# TODO implement check if the file exists, if size is correct etc. needs some care
|
||||
if imgpath == "":
|
||||
props.has_thumbnail = False
|
||||
return None
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ if __name__ == "__main__":
|
||||
asset_data["files"][0]["file_name"] = file_name
|
||||
if not has_url:
|
||||
bg_blender.progress(
|
||||
"couldn't download asset for thumnbail re-rendering"
|
||||
"couldn't download asset for thumbnail re-rendering"
|
||||
)
|
||||
exit()
|
||||
# download first, or rather make sure if it's already downloaded
|
||||
|
||||
+1
-1
@@ -163,7 +163,7 @@ if __name__ == "__main__":
|
||||
asset_data["files"][0]["file_name"] = file_name
|
||||
if has_url is not True:
|
||||
bg_blender.progress(
|
||||
"couldn't download asset for thumnbail re-rendering"
|
||||
"couldn't download asset for thumbnail re-rendering"
|
||||
)
|
||||
bg_blender.progress("downloading asset")
|
||||
fpath = bg_utils.download_asset_file(
|
||||
|
||||
+5
-6
@@ -31,6 +31,8 @@ from bpy.props import BoolProperty
|
||||
|
||||
from . import client_lib, client_tasks, datas, global_vars, reports, tasks_queue, utils
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
from . import override_extension_draw
|
||||
|
||||
CLIENT_ID = "IdFRwa3SGA8eMpzhRVFMg5Ts8sPK93xBjif93x0F"
|
||||
REFRESH_RESERVE = 60 * 60 * 24 * 3 # 3 days
|
||||
@@ -103,10 +105,9 @@ def clean_login_data():
|
||||
preferences.api_key_timeout = 0
|
||||
global_vars.BKIT_PROFILE = datas.MineProfile()
|
||||
# Cleanup also the api key in the extensions repository setting and clean the cache
|
||||
from . import override_extension_draw
|
||||
|
||||
override_extension_draw.ensure_repository(api_key="")
|
||||
override_extension_draw.clear_repo_cache()
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
override_extension_draw.ensure_repository(api_key="")
|
||||
override_extension_draw.clear_repo_cache()
|
||||
|
||||
|
||||
def logout() -> None:
|
||||
@@ -158,8 +159,6 @@ def write_tokens(auth_token, refresh_token, oauth_response):
|
||||
preferences.api_key = auth_token # triggers api_key update function
|
||||
# write token also to extensions repository setting and clear the cache
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
from . import override_extension_draw
|
||||
|
||||
override_extension_draw.ensure_repository(api_key=auth_token)
|
||||
override_extension_draw.clear_repo_cache()
|
||||
|
||||
|
||||
+5
-5
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"last_check": "2025-12-01 11:02:25.858363",
|
||||
"backup_date": "October-27-2025",
|
||||
"last_check": "2026-01-12 10:24:10.400844",
|
||||
"backup_date": "December-1-2025",
|
||||
"update_ready": true,
|
||||
"ignore": false,
|
||||
"just_restored": false,
|
||||
"just_updated": false,
|
||||
"version_text": {
|
||||
"link": "https://github.com/BlenderKit/BlenderKit/releases/download/v3.18.0.251121/blenderkit-v3.18.0.251121.zip",
|
||||
"link": "https://github.com/BlenderKit/BlenderKit/releases/download/v3.18.1.251219/blenderkit-v3.18.1.251219.zip",
|
||||
"version": [
|
||||
3,
|
||||
18,
|
||||
0,
|
||||
251121
|
||||
1,
|
||||
251219
|
||||
]
|
||||
}
|
||||
}
|
||||
+27
-20
@@ -1,4 +1,6 @@
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
@@ -6,9 +8,14 @@ import gpu
|
||||
|
||||
from .. import image_utils, ui_bgl
|
||||
from .bl_ui_widget import BL_UI_Widget
|
||||
from .bl_ui_image import BL_UI_Image
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BL_UI_Button(BL_UI_Widget):
|
||||
"""Image Button for assets in asset bar."""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
super().__init__(x, y, width, height)
|
||||
self._text_color = (1.0, 1.0, 1.0, 1.0)
|
||||
@@ -89,7 +96,7 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
except Exception as e:
|
||||
self.__image = None
|
||||
|
||||
def set_image_colorspace(self, colorspace):
|
||||
def set_image_colorspace(self, colorspace: str = ""):
|
||||
image_utils.set_colorspace(self.__image, colorspace)
|
||||
|
||||
def set_image(self, rel_filepath):
|
||||
@@ -98,22 +105,10 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
try:
|
||||
if self.__image is None or self.__image.filepath != rel_filepath:
|
||||
imgname = f".{os.path.basename(rel_filepath)}"
|
||||
img = bpy.data.images.get(imgname)
|
||||
if img is not None:
|
||||
self.__image = img
|
||||
else:
|
||||
self.__image = bpy.data.images.load(
|
||||
rel_filepath, check_existing=True
|
||||
)
|
||||
self.__image.name = imgname
|
||||
self.__image = image_utils.IMG(name=imgname, filepath=rel_filepath)
|
||||
|
||||
self.__image.gl_load()
|
||||
|
||||
if self.__image and len(self.__image.pixels) == 0:
|
||||
self.__image.reload()
|
||||
self.__image.gl_load()
|
||||
except Exception as e:
|
||||
print(f"BL_UI_BUTTON set_image() error: {e}")
|
||||
except Exception:
|
||||
bk_logger.exception("BL_UI_BUTTON set_image() error:")
|
||||
self.__image = None
|
||||
|
||||
def get_image_path(self):
|
||||
@@ -185,7 +180,7 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
y_screen_flip = self.get_area_height() - self.y_screen
|
||||
off_x, off_y = self.__image_position
|
||||
sx, sy = self.__image_size
|
||||
ui_bgl.draw_image(
|
||||
ui_bgl.draw_image_runtime(
|
||||
self.x_screen + off_x,
|
||||
y_screen_flip - off_y - sy,
|
||||
sx,
|
||||
@@ -206,10 +201,22 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
self.__state = 1
|
||||
try:
|
||||
self.mouse_down_func(self)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
except Exception:
|
||||
bk_logger.exception("BL_UI_BUTTON mouse_down() error:")
|
||||
|
||||
traceback.print_exc()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
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):
|
||||
try:
|
||||
self.mouse_down_right_func(self)
|
||||
except Exception:
|
||||
bk_logger.exception("BL_UI_BUTTON mouse_down_right() error:")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
+9
-5
@@ -1,7 +1,11 @@
|
||||
import traceback
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BL_UI_OT_draw_operator(Operator):
|
||||
bl_idname = "object.bl_ui_ot_draw_operator"
|
||||
@@ -23,7 +27,7 @@ class BL_UI_OT_draw_operator(Operator):
|
||||
for widget in self.widgets:
|
||||
widget.init(context)
|
||||
|
||||
def on_invoke(self, context, event):
|
||||
def on_invoke(self, context, event) -> Optional[bool]:
|
||||
pass
|
||||
|
||||
def on_finish(self, context):
|
||||
@@ -105,7 +109,7 @@ class BL_UI_OT_draw_operator(Operator):
|
||||
|
||||
|
||||
def draw_callback_px_separated(self, op, context):
|
||||
# separated only for puprpose of profiling
|
||||
# separated only for purpose of profiling
|
||||
try:
|
||||
# hide during animation playback, to improve performance
|
||||
if context.screen.is_animation_playing:
|
||||
@@ -113,5 +117,5 @@ def draw_callback_px_separated(self, op, context):
|
||||
if context.area.as_pointer() == self.active_area_pointer:
|
||||
for widget in self.widgets:
|
||||
widget.draw()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
except Exception:
|
||||
bk_logger.exception("Error in draw_callback_px_separated: ")
|
||||
|
||||
+15
-19
@@ -1,12 +1,21 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
|
||||
from .. import image_utils, ui_bgl
|
||||
from .bl_ui_widget import BL_UI_Widget
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BL_UI_Image(BL_UI_Widget):
|
||||
"""A simple image widget.
|
||||
|
||||
Used to display bigger thumbnail with additional info,
|
||||
while hover over a button.
|
||||
"""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
super().__init__(x, y, width, height)
|
||||
|
||||
@@ -36,25 +45,12 @@ class BL_UI_Image(BL_UI_Widget):
|
||||
try:
|
||||
if self.__image is None or self.__image.filepath != rel_filepath:
|
||||
imgname = f".{os.path.basename(rel_filepath)}"
|
||||
img = bpy.data.images.get(imgname)
|
||||
if img is not None:
|
||||
self.__image = img
|
||||
else:
|
||||
self.__image = bpy.data.images.load(
|
||||
rel_filepath, check_existing=True
|
||||
)
|
||||
self.__image.name = imgname
|
||||
|
||||
self.__image.gl_load()
|
||||
|
||||
if self.__image and len(self.__image.pixels) == 0:
|
||||
self.__image.reload()
|
||||
self.__image.gl_load()
|
||||
self.__image = image_utils.IMG(name=imgname, filepath=rel_filepath)
|
||||
except Exception as e:
|
||||
print(f"BL_UI_BUTTON: exception in set_image(): {e}")
|
||||
bk_logger.exception("BL_UI_BUTTON: exception in set_image(): %s", e)
|
||||
self.__image = None
|
||||
|
||||
def set_image_colorspace(self, colorspace):
|
||||
def set_image_colorspace(self, colorspace: str = ""):
|
||||
image_utils.set_colorspace(self.__image, colorspace)
|
||||
|
||||
def get_image_path(self):
|
||||
@@ -69,9 +65,9 @@ class BL_UI_Image(BL_UI_Widget):
|
||||
def draw(self):
|
||||
if not self._is_visible:
|
||||
return
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
self.shader.bind()
|
||||
|
||||
self.batch_panel.draw(self.shader)
|
||||
|
||||
self.draw_image()
|
||||
@@ -81,7 +77,7 @@ class BL_UI_Image(BL_UI_Widget):
|
||||
y_screen_flip = self.get_area_height() - self.y_screen
|
||||
off_x, off_y = self.__image_position
|
||||
sx, sy = self.__image_size
|
||||
ui_bgl.draw_image(
|
||||
ui_bgl.draw_image_runtime(
|
||||
self.x_screen + off_x,
|
||||
y_screen_flip - off_y - sy,
|
||||
sx,
|
||||
|
||||
+9
-7
@@ -20,6 +20,11 @@ class BL_UI_Widget:
|
||||
self._is_visible = True
|
||||
self._is_active = True # if the widget needs to be disabled
|
||||
|
||||
if bpy.app.version < (4, 0, 0):
|
||||
self.shader = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
|
||||
else:
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
def set_location(self, x, y):
|
||||
# if self.x != x or self.y != y or self.x_screen != x or self.y_screen != y:
|
||||
# bpy.context.region.tag_redraw()
|
||||
@@ -71,6 +76,8 @@ class BL_UI_Widget:
|
||||
if not self._is_visible:
|
||||
return
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", self._bg_color)
|
||||
|
||||
@@ -97,11 +104,6 @@ class BL_UI_Widget:
|
||||
(self.x_screen + self.width, y_screen_flip),
|
||||
)
|
||||
|
||||
if bpy.app.version < (4, 0, 0):
|
||||
self.shader = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
|
||||
else:
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
self.batch_panel = batch_for_shader(
|
||||
self.shader, "TRIS", {"pos": vertices}, indices=indices
|
||||
)
|
||||
@@ -187,8 +189,8 @@ class BL_UI_Widget:
|
||||
):
|
||||
# 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)
|
||||
# print ('x screen ',self.x_screen,'x ', x, 'width', self.width)
|
||||
# print ('widget y', widget_y,'y', y, 'height',self.height)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ schema_version = "1.0.0"
|
||||
|
||||
id = "blenderkit"
|
||||
type = "add-on"
|
||||
version = "3.17.0-251008" # X.Y.Z-YYYYMMDD, must have a dash instead of a dot
|
||||
version = "3.18.0-251121" # X.Y.Z-YYYYMMDD, must have a dash instead of a dot
|
||||
|
||||
name = "BlenderKit Online Asset Library"
|
||||
tagline = "Drag & drop of assets from the community driven library"
|
||||
|
||||
+1
@@ -124,6 +124,7 @@ def handle_categories_task(task: client_tasks.Task):
|
||||
"BRUSH": ["brush"],
|
||||
"NODEGROUP": ["nodegroup"],
|
||||
"PRINTABLE": ["printable"],
|
||||
"ADDON": ["addon"],
|
||||
}
|
||||
|
||||
if task.status == "finished":
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+11
-6
@@ -679,6 +679,8 @@ def start_blenderkit_client():
|
||||
|
||||
def decide_client_binary_name() -> str:
|
||||
"""Decide the name of the BlenderKit-Client binary based on the current operating system and architecture.
|
||||
We unify the OS and CPU architecture naming to make it more accessible for general public.
|
||||
Darwin is renamed to MacOS. The CPU architecture is aligned to x86_64 or arm64.
|
||||
Possible return values:
|
||||
- blenderkit-client-windows-x86_64.exe
|
||||
- blenderkit-client-windows-arm64.exe
|
||||
@@ -687,14 +689,17 @@ def decide_client_binary_name() -> str:
|
||||
- blenderkit-client-macos-x86_64
|
||||
- blenderkit-client-macos-arm64
|
||||
"""
|
||||
os_name = platform.system()
|
||||
architecture = platform.machine()
|
||||
if os_name == "Darwin": # more user-friendly name for macOS
|
||||
os_name = platform.system().lower()
|
||||
if os_name == "darwin": # more user-friendly name for macOS
|
||||
os_name = "macos"
|
||||
if architecture == "AMD64": # fix for windows
|
||||
architecture = "x86_64"
|
||||
|
||||
if os_name == "Windows":
|
||||
architecture = platform.machine().lower()
|
||||
if architecture == "amd64": # align Windows convention
|
||||
architecture = "x86_64"
|
||||
elif architecture == "aarch64": # align Linux convention
|
||||
architecture = "arm64"
|
||||
|
||||
if os_name == "windows":
|
||||
return f"blenderkit-client-{os_name}-{architecture}.exe".lower()
|
||||
|
||||
return f"blenderkit-client-{os_name}-{architecture}".lower()
|
||||
|
||||
+13
-2
@@ -24,7 +24,18 @@ WHITE = (1, 1, 1, 0.9)
|
||||
TEXT = (0.9, 0.9, 0.9, 0.6)
|
||||
GREEN = (0.9, 1, 0.9, 0.6)
|
||||
RED = (1, 0.5, 0.5, 0.8)
|
||||
|
||||
|
||||
BLUE = (0.8, 0.8, 1, 0.8)
|
||||
|
||||
PURPLE = (0.8, 0.4, 1.0, 1.0) # Full Plan purple
|
||||
GREEN_FREE = (0.4, 0.8, 0.4, 1.0) # Green for free addons
|
||||
"""Color for validator reports."""
|
||||
|
||||
GRAY = (0.7, 0.7, 0.7, 0.6)
|
||||
"""Default color for debug reports."""
|
||||
|
||||
# pure colors
|
||||
PURE_WHITE = (1, 1, 1, 1)
|
||||
PURE_BLACK = (0, 0, 0, 1)
|
||||
PURE_GREEN = (0, 1, 0, 1)
|
||||
PURE_RED = (1, 0, 0, 1)
|
||||
PURE_BLUE = (0, 0, 1, 1)
|
||||
|
||||
+2
-1
@@ -99,7 +99,8 @@ class BlenderKitDisclaimerOperator(BL_UI_OT_draw_operator):
|
||||
self.hover_bg_color = (0.127, 0.034, 1, 1.0)
|
||||
self.text_color = (0.9, 0.9, 0.9, 1)
|
||||
|
||||
print("@ BlenderKitDisclaimerOperator.__init__ message is: ", self.message)
|
||||
bk_logger.info("%s", self.message)
|
||||
|
||||
pix_size = get_text_size(
|
||||
font_id=1,
|
||||
text=self.message,
|
||||
|
||||
+765
-6
@@ -16,16 +16,22 @@
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import addon_utils
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
|
||||
from . import (
|
||||
append_link,
|
||||
client_lib,
|
||||
client_tasks,
|
||||
global_vars,
|
||||
paths,
|
||||
reports,
|
||||
resolutions,
|
||||
@@ -34,10 +40,10 @@ from . import (
|
||||
utils,
|
||||
)
|
||||
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
import bpy
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
from . import override_extension_draw
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
@@ -48,6 +54,232 @@ from bpy.props import (
|
||||
StringProperty,
|
||||
)
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_blenderkit_repository():
|
||||
"""Find the BlenderKit extensions repository index.
|
||||
|
||||
Returns:
|
||||
int: Repository index if found, -1 otherwise
|
||||
"""
|
||||
enabled_repos = [
|
||||
repo for repo in bpy.context.preferences.extensions.repos if repo.enabled
|
||||
]
|
||||
for i, repo in enumerate(enabled_repos):
|
||||
if (
|
||||
repo.remote_url and global_vars.SERVER in repo.remote_url
|
||||
) or "blenderkit" in repo.name.lower():
|
||||
return repo, i
|
||||
return None, -1
|
||||
|
||||
|
||||
def get_addon_installation_status(asset_data):
|
||||
"""Get the installation and enablement status of an addon.
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
"installed": bool,
|
||||
"enabled": bool,
|
||||
"pkg_id": str,
|
||||
"cached_pkg": dict or None
|
||||
}
|
||||
"""
|
||||
|
||||
# Get the correct package ID
|
||||
extension_id = asset_data.get("dictParameters", {}).get("extensionId")
|
||||
if not extension_id:
|
||||
return {
|
||||
"installed": False,
|
||||
"enabled": False,
|
||||
"pkg_id": None,
|
||||
"cached_pkg": None,
|
||||
}
|
||||
|
||||
# Check if addon is installed and enabled using Blender's addon system
|
||||
|
||||
# Method 1: Check if it's in the enabled addons list
|
||||
# For new extension system, addons have format: bl_ext.repository_name.package_name
|
||||
enabled_addons = [addon.module for addon in bpy.context.preferences.addons]
|
||||
|
||||
# Check direct match first
|
||||
is_enabled = extension_id in enabled_addons
|
||||
|
||||
# If not found, check for extension format: bl_ext.www_blenderkit_com.package_name
|
||||
if not is_enabled:
|
||||
extension_module_name = f"bl_ext.www_blenderkit_com.{extension_id}"
|
||||
is_enabled = extension_module_name in enabled_addons
|
||||
bk_logger.info(
|
||||
f"Checking extension format: {extension_module_name} -> enabled: {is_enabled}"
|
||||
)
|
||||
|
||||
# Also try other possible repository name formats
|
||||
if not is_enabled:
|
||||
for addon_module in enabled_addons:
|
||||
if addon_module.endswith(
|
||||
f".{extension_id}"
|
||||
) and addon_module.startswith("bl_ext."):
|
||||
is_enabled = True
|
||||
bk_logger.info(
|
||||
f"Found enabled addon with extension format: {addon_module}"
|
||||
)
|
||||
break
|
||||
|
||||
# Method 2: Check if it's installed (may be disabled) using addon_utils
|
||||
is_installed = False
|
||||
try:
|
||||
for addon_module in addon_utils.modules():
|
||||
# Check direct match
|
||||
if addon_module.__name__ == extension_id:
|
||||
is_installed = True
|
||||
break
|
||||
# Check extension format match
|
||||
elif addon_module.__name__.endswith(
|
||||
f".{extension_id}"
|
||||
) and addon_module.__name__.startswith("bl_ext."):
|
||||
is_installed = True
|
||||
bk_logger.info(
|
||||
f"Found installed addon with extension format: {addon_module.__name__}"
|
||||
)
|
||||
break
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Error checking addon_utils.modules(): {e}")
|
||||
|
||||
# If found through addon_utils, we know it's installed
|
||||
# But we need to double-check enabled status using the correct module name
|
||||
if is_installed and not is_enabled:
|
||||
# Try to find the correct module name format for this addon
|
||||
try:
|
||||
for addon_module in addon_utils.modules():
|
||||
if addon_module.__name__ == extension_id or (
|
||||
addon_module.__name__.endswith(f".{extension_id}")
|
||||
and addon_module.__name__.startswith("bl_ext.")
|
||||
):
|
||||
# Check if this specific module name is enabled
|
||||
is_enabled = addon_module.__name__ in enabled_addons
|
||||
if is_enabled:
|
||||
bk_logger.info(f"Found enabled addon: {addon_module.__name__}")
|
||||
break
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Error double-checking enabled status: {e}")
|
||||
|
||||
# Method 3: If not found through traditional addon system, check extensions system
|
||||
if not is_installed:
|
||||
try:
|
||||
override_extension_draw.ensure_repo_cache()
|
||||
bk_ext_cache = bpy.context.window_manager.get(
|
||||
"blenderkit_extensions_repo_cache", {}
|
||||
)
|
||||
|
||||
for cache_key, pkg_data in bk_ext_cache.items():
|
||||
if isinstance(pkg_data, dict) and pkg_data.get("id") == extension_id:
|
||||
# Check if it's actually installed in the extension system
|
||||
is_installed = pkg_data.get("installed", False)
|
||||
# For extensions, enabled status might be in the cache
|
||||
if is_installed and not is_enabled:
|
||||
is_enabled = pkg_data.get("enabled", False)
|
||||
break
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Error checking extension cache: {e}")
|
||||
|
||||
# Method 4: Check through Blender's extension repositories directly
|
||||
if not is_installed:
|
||||
try:
|
||||
|
||||
# Look for BlenderKit repository and check its packages
|
||||
for repo in bpy.context.preferences.extensions.repos:
|
||||
if not repo.enabled:
|
||||
continue
|
||||
if not (
|
||||
(repo.remote_url and global_vars.SERVER in repo.remote_url)
|
||||
or "blenderkit" in repo.name.lower()
|
||||
):
|
||||
continue
|
||||
|
||||
# This is a BlenderKit repository, try to find our package
|
||||
# Note: The actual package checking would require deeper access to the repository data
|
||||
# For now, we'll rely on the previous methods
|
||||
break
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Error checking extension repositories: {e}")
|
||||
|
||||
# Debug: Show some enabled addons for reference
|
||||
blenderkit_addons = [
|
||||
addon
|
||||
for addon in enabled_addons
|
||||
if "blenderkit" in addon.lower() or addon.endswith(extension_id)
|
||||
]
|
||||
if blenderkit_addons:
|
||||
bk_logger.info(f"Found BlenderKit-related enabled addons: {blenderkit_addons}")
|
||||
|
||||
bk_logger.info(
|
||||
f"Addon status check for '{extension_id}': installed={is_installed}, enabled={is_enabled}"
|
||||
)
|
||||
|
||||
return {
|
||||
"installed": is_installed,
|
||||
"enabled": is_enabled,
|
||||
"pkg_id": extension_id,
|
||||
"cached_pkg": None, # Not using cached_pkg anymore
|
||||
}
|
||||
|
||||
|
||||
def install_addon_from_local_file(asset_data, file_path, enable_on_install=True):
|
||||
"""Install an addon from a local zip file using Blender's extensions API.
|
||||
|
||||
Args:
|
||||
asset_data: Asset metadata dictionary
|
||||
file_path: Path to the downloaded zip file
|
||||
enable_on_install: If True, enable the addon after installation (default: True)
|
||||
"""
|
||||
|
||||
addon_name = asset_data.get("name", "Unknown Addon")
|
||||
|
||||
if bpy.app.version < (4, 2, 0):
|
||||
error_msg = f"Addon installation requires Blender 4.2 or newer. Current version: {'.'.join(map(str, bpy.app.version[:2]))}"
|
||||
reports.add_report(error_msg, type="ERROR")
|
||||
raise Exception(error_msg)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
error_msg = f"Addon file not found: {file_path}"
|
||||
reports.add_report(error_msg, type="ERROR")
|
||||
raise Exception(error_msg)
|
||||
|
||||
bk_logger.info(f"Installing addon '{addon_name}' from local file: {file_path}")
|
||||
|
||||
status = get_addon_installation_status(asset_data)
|
||||
if status["installed"]:
|
||||
reports.add_report(f"Addon '{addon_name}' is already installed", type="INFO")
|
||||
return
|
||||
|
||||
# Find the BlenderKit repository to install the addon to
|
||||
repo, repo_index = get_blenderkit_repository()
|
||||
if repo is None:
|
||||
error_msg = "BlenderKit repository not found. Please ensure the BlenderKit extensions repository is enabled in preferences."
|
||||
reports.add_report(error_msg, type="ERROR")
|
||||
raise Exception(error_msg)
|
||||
|
||||
# Install from file to the BlenderKit repository
|
||||
result = bpy.ops.extensions.package_install_files(
|
||||
repo=repo.module,
|
||||
filepath=file_path,
|
||||
enable_on_install=enable_on_install,
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(f"Installation failed - operation returned: {result}")
|
||||
|
||||
post_install_status = get_addon_installation_status(asset_data)
|
||||
if not post_install_status["installed"]:
|
||||
raise Exception(
|
||||
f"Installation verification failed: '{addon_name}' was not installed. "
|
||||
f"This may be due to version compatibility issues or other requirements not being met."
|
||||
)
|
||||
|
||||
status_text = "enabled" if enable_on_install else "disabled"
|
||||
reports.add_report(
|
||||
f"Successfully installed addon '{addon_name}' ({status_text})", type="INFO"
|
||||
)
|
||||
|
||||
|
||||
download_tasks = {}
|
||||
|
||||
@@ -117,6 +349,68 @@ def check_unused():
|
||||
l.user_clear()
|
||||
|
||||
|
||||
def get_temp_enabled_addons():
|
||||
"""Get list of temporarily enabled addons from preferences."""
|
||||
|
||||
try:
|
||||
prefs = bpy.context.preferences.addons[__package__].preferences
|
||||
temp_addons_json = prefs.temp_enabled_addons
|
||||
return json.loads(temp_addons_json)
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Error reading temporary addons from preferences: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def set_temp_enabled_addons(addon_list):
|
||||
"""Save list of temporarily enabled addons to preferences."""
|
||||
|
||||
try:
|
||||
prefs = bpy.context.preferences.addons[__package__].preferences
|
||||
prefs.temp_enabled_addons = json.dumps(addon_list)
|
||||
bk_logger.info(f"Saved {len(addon_list)} temporary addons to preferences")
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Error saving temporary addons to preferences: {e}")
|
||||
|
||||
|
||||
def add_temp_enabled_addon(pkg_id):
|
||||
"""Add an addon to the temporary enabled list."""
|
||||
temp_enabled = get_temp_enabled_addons()
|
||||
if pkg_id not in temp_enabled:
|
||||
temp_enabled.append(pkg_id)
|
||||
set_temp_enabled_addons(temp_enabled)
|
||||
bk_logger.info(f"Added {pkg_id} to temporary addons list")
|
||||
|
||||
|
||||
def cleanup_temp_enabled_addons():
|
||||
"""Disable temporarily enabled addons."""
|
||||
|
||||
try:
|
||||
temp_enabled = get_temp_enabled_addons()
|
||||
|
||||
if not temp_enabled:
|
||||
bk_logger.info("No temporarily enabled addons to clean up")
|
||||
return
|
||||
|
||||
bk_logger.info(f"Cleaning up {len(temp_enabled)} temporarily enabled addons")
|
||||
|
||||
# Disable all temporarily enabled addons using preferences API
|
||||
for pkg_id in temp_enabled:
|
||||
try:
|
||||
full_module_name = f"bl_ext.www_blenderkit_com.{pkg_id}"
|
||||
bpy.ops.preferences.addon_disable(module=full_module_name)
|
||||
bk_logger.info(f"Disabled temporarily enabled addon: {pkg_id}")
|
||||
except Exception as e:
|
||||
bk_logger.warning(
|
||||
f"Failed to disable temporarily enabled addon {pkg_id}: {e}"
|
||||
)
|
||||
|
||||
# Clear the list in preferences
|
||||
set_temp_enabled_addons([])
|
||||
bk_logger.info("Temporary addon cleanup completed")
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Error during temporary addon cleanup: {e}")
|
||||
|
||||
|
||||
@persistent
|
||||
def scene_save(context):
|
||||
"""Do cleanup of blenderkit props and send a message to the server about assets used."""
|
||||
@@ -131,6 +425,49 @@ def scene_save(context):
|
||||
client_lib.report_usages(report_data)
|
||||
|
||||
|
||||
def refresh_addon_search_results_status():
|
||||
"""Refresh installation status in addon search results after installation operations."""
|
||||
|
||||
try:
|
||||
# Get current search results
|
||||
sr = search.get_search_results()
|
||||
if not sr:
|
||||
return
|
||||
|
||||
# Check if we're currently viewing addons
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if ui_props.asset_type != "ADDON":
|
||||
return
|
||||
|
||||
# Update installation status for all addon search results
|
||||
for asset_data in sr:
|
||||
if asset_data.get("assetType") == "addon":
|
||||
try:
|
||||
status = get_addon_installation_status(asset_data)
|
||||
is_installed = status.get("installed", False)
|
||||
is_enabled = status.get("enabled", False)
|
||||
|
||||
# Update the status in search results
|
||||
asset_data["downloaded"] = 100 if is_installed else 0
|
||||
asset_data["enabled"] = is_enabled
|
||||
|
||||
except Exception as e:
|
||||
bk_logger.warning(
|
||||
f"Could not refresh status for addon {asset_data.get('name', 'Unknown')}: {e}"
|
||||
)
|
||||
asset_data["downloaded"] = 0
|
||||
asset_data["enabled"] = False
|
||||
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Error refreshing addon search results status: {e}")
|
||||
|
||||
|
||||
@persistent
|
||||
def scene_load_pre(context):
|
||||
"""Clean up temporarily enabled addons before loading new file."""
|
||||
cleanup_temp_enabled_addons()
|
||||
|
||||
|
||||
@persistent
|
||||
def scene_load(context):
|
||||
"""Restart broken downloads on scene load."""
|
||||
@@ -636,7 +973,8 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
|
||||
|
||||
asset_data["resolution"] = kwargs["resolution"]
|
||||
udpate_asset_data_in_dicts(asset_data)
|
||||
update_asset_metadata(asset_main, asset_data)
|
||||
if asset_main is not None:
|
||||
update_asset_metadata(asset_main, asset_data)
|
||||
|
||||
bpy.ops.ed.undo_push(
|
||||
"INVOKE_REGION_WIN", message="add %s to scene" % asset_data["name"]
|
||||
@@ -949,6 +1287,25 @@ def download_post(task: client_tasks.Task) -> None:
|
||||
return
|
||||
|
||||
orig_task.update(task.data)
|
||||
|
||||
# For addons, install from the downloaded file instead of appending
|
||||
if at == "addon":
|
||||
if file_paths:
|
||||
# Check if addon should be enabled after installation (default: True)
|
||||
enable_on_install = task.data.get("enable_on_install", True)
|
||||
install_addon_from_local_file(
|
||||
task.data["asset_data"],
|
||||
file_paths[-1],
|
||||
enable_on_install=enable_on_install,
|
||||
)
|
||||
|
||||
else:
|
||||
bk_logger.error("No file paths available for addon installation")
|
||||
reports.add_report(
|
||||
"Addon download completed but no file found", type="ERROR"
|
||||
)
|
||||
return
|
||||
|
||||
try_finished_append(
|
||||
file_paths=file_paths, **task.data
|
||||
) # exception is handled in calling function
|
||||
@@ -966,7 +1323,6 @@ def download(asset_data, **kwargs):
|
||||
report = f"Maximum retries exceeded for {asset_data['name']}"
|
||||
sprops.report = report
|
||||
reports.add_report(report, type="ERROR")
|
||||
|
||||
bk_logger.debug(sprops.report)
|
||||
return
|
||||
|
||||
@@ -980,6 +1336,8 @@ def download(asset_data, **kwargs):
|
||||
# inject resolution into prefs.
|
||||
prefs = utils.get_preferences_as_dict()
|
||||
prefs["resolution"] = kwargs.get("resolution", "original")
|
||||
if "unpack_files" in kwargs: # for add-on download
|
||||
prefs["unpack_files"] = kwargs["unpack_files"]
|
||||
|
||||
data = {
|
||||
"asset_data": asset_data,
|
||||
@@ -1271,10 +1629,391 @@ asset_types = (
|
||||
("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"),
|
||||
("ADDON", "Addon", "addon"),
|
||||
)
|
||||
|
||||
|
||||
class BlenderkitAddonManagerOperator(bpy.types.Operator):
|
||||
"""Manage BlenderKit addon installation, enabling, and disabling"""
|
||||
|
||||
bl_idname = "scene.blenderkit_addon_manager"
|
||||
bl_label = "Addon Manager"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
asset_data: bpy.props.StringProperty() # JSON encoded asset data
|
||||
action: bpy.props.EnumProperty(
|
||||
items=[
|
||||
("INSTALL", "Install", "Install the addon"),
|
||||
("UNINSTALL", "Uninstall", "Uninstall the addon"),
|
||||
("ENABLE", "Enable", "Enable the addon"),
|
||||
("DISABLE", "Disable", "Disable the addon"),
|
||||
("TEMP_ENABLE", "Enable Temporarily", "Enable until end of session"),
|
||||
]
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
try:
|
||||
asset_data = json.loads(self.asset_data)
|
||||
except:
|
||||
reports.add_report("Invalid asset data", type="ERROR")
|
||||
return {"CANCELLED"}
|
||||
|
||||
addon_name = asset_data.get("name", "Unknown Addon")
|
||||
status = get_addon_installation_status(asset_data)
|
||||
pkg_id = status["pkg_id"]
|
||||
|
||||
# For non-install actions, we need the repository and pkg_id
|
||||
repo_index = -1
|
||||
if self.action != "INSTALL":
|
||||
if not pkg_id:
|
||||
reports.add_report("No extension ID found for this addon", type="ERROR")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Find the BlenderKit repository
|
||||
repo, repo_index = get_blenderkit_repository()
|
||||
if repo is None:
|
||||
reports.add_report("BlenderKit repository not found", type="ERROR")
|
||||
return {"CANCELLED"}
|
||||
|
||||
try:
|
||||
if self.action == "INSTALL":
|
||||
# Trigger download which will automatically install after completion
|
||||
reports.add_report(f"Downloading addon '{addon_name}'...", type="INFO")
|
||||
|
||||
# Check if addon is already downloading
|
||||
if check_downloading(asset_data):
|
||||
reports.add_report(
|
||||
f"Addon '{addon_name}' is already being downloaded", type="INFO"
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
# Start the download
|
||||
download(asset_data, resolution="blend")
|
||||
return {"FINISHED"}
|
||||
|
||||
elif self.action == "UNINSTALL":
|
||||
result = bpy.ops.extensions.package_uninstall(
|
||||
repo_index=repo_index, pkg_id=pkg_id
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(
|
||||
f"Uninstallation failed - operation returned: {result}"
|
||||
)
|
||||
reports.add_report(
|
||||
f"Successfully uninstalled '{addon_name}'", type="INFO"
|
||||
)
|
||||
self.report({"INFO"}, f"Successfully uninstalled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
|
||||
elif self.action == "ENABLE":
|
||||
result = bpy.ops.extensions.package_enable(
|
||||
repo_index=repo_index, pkg_id=pkg_id
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(f"Enable failed - operation returned: {result}")
|
||||
reports.add_report(f"Successfully enabled '{addon_name}'", type="INFO")
|
||||
self.report({"INFO"}, f"Successfully enabled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
|
||||
elif self.action == "DISABLE":
|
||||
result = bpy.ops.extensions.package_disable(
|
||||
repo_index=repo_index, pkg_id=pkg_id
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(f"Disable failed - operation returned: {result}")
|
||||
reports.add_report(f"Successfully disabled '{addon_name}'", type="INFO")
|
||||
self.report({"INFO"}, f"Successfully disabled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
|
||||
elif self.action == "TEMP_ENABLE":
|
||||
result = bpy.ops.extensions.package_enable(
|
||||
repo_index=repo_index, pkg_id=pkg_id
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(
|
||||
f"Temporary enable failed - operation returned: {result}"
|
||||
)
|
||||
# Store the package for later disabling
|
||||
wm = context.window_manager
|
||||
temp_enabled = wm.get("blenderkit_temp_enabled_addons", [])
|
||||
if pkg_id not in temp_enabled:
|
||||
temp_enabled.append(pkg_id)
|
||||
wm["blenderkit_temp_enabled_addons"] = temp_enabled
|
||||
reports.add_report(
|
||||
f"Temporarily enabled '{addon_name}' (will disable on session end)",
|
||||
type="INFO",
|
||||
)
|
||||
self.report({"INFO"}, f"Temporarily enabled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to {self.action.lower()} '{addon_name}': {e}"
|
||||
reports.add_report(error_msg, type="ERROR")
|
||||
self.report({"ERROR"}, error_msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BlenderkitAddonChoiceOperator(bpy.types.Operator):
|
||||
"""Show addon management options popup"""
|
||||
|
||||
bl_idname = "scene.blenderkit_addon_choice"
|
||||
bl_label = "Addon Options"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
asset_data: bpy.props.StringProperty() # JSON encoded asset data
|
||||
|
||||
# Actions for not installed addons
|
||||
action_not_installed: bpy.props.EnumProperty(
|
||||
name="Action",
|
||||
description="Choose what to do with this addon",
|
||||
items=[
|
||||
(
|
||||
"INSTALL_AND_ENABLE",
|
||||
"Install and Enable",
|
||||
"Install the addon and enable it immediately",
|
||||
"CHECKBOX_HLT",
|
||||
0,
|
||||
),
|
||||
(
|
||||
"INSTALL_AND_TEMP_ENABLE",
|
||||
"Install and Enable Temporarily",
|
||||
"Install and enable until end of session",
|
||||
"TIME",
|
||||
1,
|
||||
),
|
||||
(
|
||||
"INSTALL_ONLY",
|
||||
"Install Only",
|
||||
"Install the addon but keep it disabled",
|
||||
"IMPORT",
|
||||
2,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# Actions for installed and enabled addons
|
||||
action_installed_enabled: bpy.props.EnumProperty(
|
||||
name="Action",
|
||||
description="Choose what to do with this addon",
|
||||
items=[
|
||||
("DISABLE", "Disable", "Disable the addon", "CHECKBOX_DEHLT", 0),
|
||||
("UNINSTALL", "Uninstall", "Completely remove the addon", "CANCEL", 1),
|
||||
],
|
||||
)
|
||||
|
||||
# Actions for installed but disabled addons
|
||||
action_installed_disabled: bpy.props.EnumProperty(
|
||||
name="Action",
|
||||
description="Choose what to do with this addon",
|
||||
items=[
|
||||
("ENABLE", "Enable", "Enable the addon permanently", "CHECKBOX_HLT", 0),
|
||||
(
|
||||
"TEMP_ENABLE",
|
||||
"Enable Temporarily",
|
||||
"Enable until end of session",
|
||||
"TIME",
|
||||
1,
|
||||
),
|
||||
("UNINSTALL", "Uninstall", "Completely remove the addon", "CANCEL", 2),
|
||||
],
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
|
||||
layout = self.layout
|
||||
|
||||
try:
|
||||
asset_data = json.loads(self.asset_data)
|
||||
except:
|
||||
layout.label(text="Invalid asset data")
|
||||
return
|
||||
|
||||
addon_name = asset_data.get("name", "Unknown Addon")
|
||||
status = get_addon_installation_status(asset_data)
|
||||
|
||||
layout.label(text=f"Addon: {addon_name}")
|
||||
layout.separator()
|
||||
|
||||
layout = layout.column()
|
||||
# Show current status and appropriate action enum
|
||||
if not status["installed"]:
|
||||
layout.label(text="Status: Not Installed", icon="QUESTION")
|
||||
layout.separator()
|
||||
layout.prop(self, "action_not_installed", expand=True)
|
||||
elif status["enabled"]:
|
||||
layout.label(text="Status: Installed and Enabled", icon="CHECKMARK")
|
||||
layout.separator()
|
||||
layout.prop(self, "action_installed_enabled", expand=True)
|
||||
else:
|
||||
layout.label(text="Status: Installed but Disabled", icon="X")
|
||||
layout.separator()
|
||||
layout.prop(self, "action_installed_disabled", expand=True)
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Set default values for each enum
|
||||
self.action_not_installed = "INSTALL_AND_ENABLE"
|
||||
self.action_installed_enabled = "DISABLE"
|
||||
self.action_installed_disabled = "ENABLE"
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self, width=350)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
try:
|
||||
asset_data = json.loads(self.asset_data)
|
||||
except:
|
||||
reports.add_report("Invalid asset data", type="ERROR")
|
||||
return {"CANCELLED"}
|
||||
|
||||
addon_name = asset_data.get("name", "Unknown Addon")
|
||||
status = get_addon_installation_status(asset_data)
|
||||
pkg_id = status["pkg_id"]
|
||||
|
||||
# Get the selected action based on addon status
|
||||
if not status["installed"]:
|
||||
selected_action = self.action_not_installed
|
||||
elif status["enabled"]:
|
||||
selected_action = self.action_installed_enabled
|
||||
else:
|
||||
selected_action = self.action_installed_disabled
|
||||
|
||||
# For non-install actions, we need the repository and pkg_id
|
||||
repo_index = -1
|
||||
if selected_action not in (
|
||||
"INSTALL_AND_ENABLE",
|
||||
"INSTALL_AND_TEMP_ENABLE",
|
||||
"INSTALL_ONLY",
|
||||
):
|
||||
if not pkg_id:
|
||||
reports.add_report("No extension ID found for this addon", type="ERROR")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Find the BlenderKit repository
|
||||
repo, repo_index = get_blenderkit_repository()
|
||||
if repo is None:
|
||||
reports.add_report("BlenderKit repository not found", type="ERROR")
|
||||
return {"CANCELLED"}
|
||||
|
||||
try:
|
||||
if selected_action in (
|
||||
"INSTALL_AND_ENABLE",
|
||||
"INSTALL_AND_TEMP_ENABLE",
|
||||
"INSTALL_ONLY",
|
||||
):
|
||||
# Trigger download which will automatically install and enable after completion
|
||||
reports.add_report(f"Downloading addon '{addon_name}'...", type="INFO")
|
||||
|
||||
# Check if addon is already downloading
|
||||
if check_downloading(asset_data):
|
||||
reports.add_report(
|
||||
f"Addon '{addon_name}' is already being downloaded", type="INFO"
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
if selected_action == "INSTALL_AND_TEMP_ENABLE":
|
||||
add_temp_enabled_addon(pkg_id)
|
||||
|
||||
# Enable on install for both INSTALL_AND_ENABLE and INSTALL_AND_TEMP_ENABLE
|
||||
enable_on_install = selected_action != "INSTALL_ONLY"
|
||||
# Start the download, disable unpacking
|
||||
download(
|
||||
asset_data,
|
||||
resolution="blend",
|
||||
unpack_files=False,
|
||||
enable_on_install=enable_on_install,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
elif selected_action == "UNINSTALL":
|
||||
result = bpy.ops.extensions.package_uninstall(
|
||||
repo_index=repo_index, pkg_id=pkg_id
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(
|
||||
f"Uninstallation failed - operation returned: {result}"
|
||||
)
|
||||
reports.add_report(
|
||||
f"Successfully uninstalled '{addon_name}'", type="INFO"
|
||||
)
|
||||
self.report({"INFO"}, f"Successfully uninstalled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
|
||||
elif selected_action == "ENABLE":
|
||||
# Enable using preferences API
|
||||
full_module_name = f"bl_ext.www_blenderkit_com.{pkg_id}"
|
||||
try:
|
||||
result = bpy.ops.preferences.addon_enable(module=full_module_name)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(f"Enable operation failed - returned: {result}")
|
||||
reports.add_report(
|
||||
f"Successfully enabled '{addon_name}'", type="INFO"
|
||||
)
|
||||
self.report({"INFO"}, f"Successfully enabled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Failed to enable addon: {e}")
|
||||
reports.add_report(
|
||||
f"Failed to enable '{addon_name}': {e}", type="ERROR"
|
||||
)
|
||||
|
||||
elif selected_action == "DISABLE":
|
||||
# Disable using preferences API
|
||||
full_module_name = f"bl_ext.www_blenderkit_com.{pkg_id}"
|
||||
try:
|
||||
result = bpy.ops.preferences.addon_disable(module=full_module_name)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(
|
||||
f"Disable operation failed - returned: {result}"
|
||||
)
|
||||
reports.add_report(
|
||||
f"Successfully disabled '{addon_name}'", type="INFO"
|
||||
)
|
||||
self.report({"INFO"}, f"Successfully disabled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Failed to disable addon: {e}")
|
||||
reports.add_report(
|
||||
f"Failed to disable '{addon_name}': {e}", type="ERROR"
|
||||
)
|
||||
|
||||
elif selected_action == "TEMP_ENABLE":
|
||||
# Temporarily enable using preferences API
|
||||
full_module_name = f"bl_ext.www_blenderkit_com.{pkg_id}"
|
||||
try:
|
||||
result = bpy.ops.preferences.addon_enable(module=full_module_name)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(
|
||||
f"Temporary enable operation failed - returned: {result}"
|
||||
)
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Failed to temp enable addon: {e}")
|
||||
reports.add_report(
|
||||
f"Failed to enable '{addon_name}': {e}", type="ERROR"
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Store the package for later disabling
|
||||
add_temp_enabled_addon(pkg_id)
|
||||
reports.add_report(
|
||||
f"Temporarily enabled '{addon_name}' (will disable on session end)",
|
||||
type="INFO",
|
||||
)
|
||||
self.report({"INFO"}, f"Temporarily enabled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Addon operation failed for '{addon_name}': {e}")
|
||||
error_msg = f"Failed to {selected_action.lower().replace('_', ' ')} '{addon_name}': {e}"
|
||||
reports.add_report(error_msg, type="ERROR")
|
||||
self.report({"ERROR"}, error_msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BlenderkitKillDownloadOperator(bpy.types.Operator):
|
||||
"""Kill a download"""
|
||||
|
||||
@@ -1315,6 +2054,10 @@ def available_resolutions_callback(self, context):
|
||||
|
||||
def has_asset_files(asset_data):
|
||||
"""Check if asset has files."""
|
||||
# Addons are handled separately by the extension system
|
||||
if asset_data["assetType"] == "addon":
|
||||
return True
|
||||
|
||||
for f in asset_data["files"]:
|
||||
if f["fileType"] in ("blend", "zip_file"):
|
||||
return True
|
||||
@@ -1491,6 +2234,14 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
asset_type = self.asset_data["assetType"]
|
||||
|
||||
# Handle addon assets with popup
|
||||
if asset_type == "addon":
|
||||
|
||||
bpy.ops.scene.blenderkit_addon_choice(
|
||||
"INVOKE_DEFAULT", asset_data=json.dumps(self.asset_data)
|
||||
)
|
||||
return {"FINISHED"}
|
||||
if (
|
||||
(asset_type == "model" or asset_type == "material")
|
||||
and (bpy.context.mode != "OBJECT")
|
||||
@@ -1636,12 +2387,20 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
|
||||
def register_download():
|
||||
bpy.utils.register_class(BlenderkitDownloadOperator)
|
||||
bpy.utils.register_class(BlenderkitKillDownloadOperator)
|
||||
# bpy.utils.register_class(BlenderkitAddonManagerOperator) # Replaced by BlenderkitAddonChoiceOperator
|
||||
bpy.utils.register_class(BlenderkitAddonChoiceOperator)
|
||||
bpy.app.handlers.load_post.append(scene_load)
|
||||
bpy.app.handlers.save_pre.append(scene_save)
|
||||
bpy.app.handlers.load_post.append(scene_load_pre)
|
||||
|
||||
|
||||
def unregister_download():
|
||||
bpy.utils.unregister_class(BlenderkitDownloadOperator)
|
||||
bpy.utils.unregister_class(BlenderkitKillDownloadOperator)
|
||||
# bpy.utils.unregister_class(BlenderkitAddonManagerOperator) # Replaced by BlenderkitAddonChoiceOperator
|
||||
bpy.utils.unregister_class(BlenderkitAddonChoiceOperator)
|
||||
bpy.app.handlers.load_post.remove(scene_load)
|
||||
bpy.app.handlers.save_pre.remove(scene_save)
|
||||
bpy.app.handlers.load_post.remove(scene_load_pre)
|
||||
# Clean up any remaining temporarily enabled addons
|
||||
cleanup_temp_enabled_addons()
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ from typing import Any, Optional
|
||||
from . import datas
|
||||
|
||||
|
||||
CLIENT_VERSION = "v1.6.0"
|
||||
CLIENT_VERSION = "v1.7.0"
|
||||
CLIENT_ACCESSIBLE = False
|
||||
"""Is Client accessible? Can add-on access it and call stuff which uses it?"""
|
||||
CLIENT_RUNNING = False
|
||||
|
||||
+70
-7
@@ -18,10 +18,26 @@
|
||||
|
||||
import os
|
||||
import time
|
||||
from functools import lru_cache
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class IMG:
|
||||
name: str
|
||||
filepath: str
|
||||
|
||||
def gl_load(self):
|
||||
"""Imitates bpy.types.Image.gl_load() behavior."""
|
||||
return None
|
||||
|
||||
|
||||
def get_orig_render_settings():
|
||||
rs = bpy.context.scene.render
|
||||
ims = rs.image_settings
|
||||
@@ -91,21 +107,68 @@ def set_colorspace(img, colorspace: str = ""):
|
||||
if colorspace == "":
|
||||
colorspace = guess_colorspace()
|
||||
|
||||
if colorspace == "Non-Color":
|
||||
img.colorspace_settings.is_data = True
|
||||
else:
|
||||
img.colorspace_settings.name = colorspace
|
||||
except Exception as e:
|
||||
print(f"Colorspace {colorspace} not found: {e}")
|
||||
if hasattr(img, "colorspace_settings") and colorspace:
|
||||
if colorspace == "Non-Color":
|
||||
img.colorspace_settings.is_data = True
|
||||
else:
|
||||
img.colorspace_settings.name = colorspace
|
||||
|
||||
except Exception:
|
||||
bk_logger.exception("Colorspace '%s' not found: ", colorspace)
|
||||
|
||||
|
||||
def guess_colorspace():
|
||||
@lru_cache(maxsize=1)
|
||||
def list_available_image_colorspaces():
|
||||
"""Lists available color spaces in blender by creating a temporary image if needed.
|
||||
|
||||
Returns:
|
||||
List of color space names.
|
||||
"""
|
||||
# Check if there are existing images
|
||||
temp_image = None
|
||||
if bpy.data.images:
|
||||
img = bpy.data.images[0]
|
||||
else:
|
||||
# Create temporary image
|
||||
temp_image = bpy.data.images.new(
|
||||
"TempImage_ForColorSpaceList", width=1, height=1
|
||||
)
|
||||
img = temp_image
|
||||
|
||||
# Get available color spaces
|
||||
color_spaces = [
|
||||
cs.identifier
|
||||
for cs in img.colorspace_settings.bl_rna.properties["name"].enum_items
|
||||
]
|
||||
|
||||
# Clean up temporary image if created
|
||||
if temp_image:
|
||||
bpy.data.images.remove(temp_image)
|
||||
|
||||
return color_spaces
|
||||
|
||||
|
||||
def guess_colorspace() -> str:
|
||||
"""Tries to guess the colorspace from the current display device and available color spaces."""
|
||||
display_device = bpy.context.scene.display_settings.display_device
|
||||
if display_device == "sRGB":
|
||||
return "sRGB"
|
||||
if display_device == "ACES":
|
||||
return "aces"
|
||||
|
||||
# detect available color spaces on image data
|
||||
all_clr_spaces = list_available_image_colorspaces()
|
||||
|
||||
# try to match display device with color space
|
||||
for cs in all_clr_spaces:
|
||||
if display_device.lower() in cs.lower():
|
||||
return cs
|
||||
|
||||
# fallback
|
||||
if "sRGB" in all_clr_spaces:
|
||||
return "sRGB"
|
||||
return ""
|
||||
|
||||
|
||||
def analyze_image_is_true_hdr(image):
|
||||
import numpy
|
||||
|
||||
+60
-35
@@ -9,6 +9,7 @@ The original method is then called from the new method, with the same arguments,
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
|
||||
from . import icons
|
||||
|
||||
@@ -20,6 +21,8 @@ from bpy.types import Operator
|
||||
|
||||
EXTENSIONS_API_URL = "https://www.blenderkit.com/api/v1/extensions/"
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --- New Modal Operator ---
|
||||
class BK_OT_buy_extension_and_watch(Operator):
|
||||
@@ -56,7 +59,7 @@ class BK_OT_buy_extension_and_watch(Operator):
|
||||
# Open the URL
|
||||
try:
|
||||
bpy.ops.wm.url_open(url=self.url)
|
||||
print(f"BlenderKit: Opening buy URL: {self.url}")
|
||||
bk_logger.info("Opening buy URL: %s.", self.url)
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Could not open URL: {e}")
|
||||
# Don't cancel, maybe the user still wants the refresh?
|
||||
@@ -72,10 +75,11 @@ class BK_OT_buy_extension_and_watch(Operator):
|
||||
self._last_refresh_time = (
|
||||
self._start_time
|
||||
) # Initialize to avoid immediate refresh
|
||||
print(
|
||||
f"BlenderKit: Started watching repository index {self.repo_index} for updates."
|
||||
bk_logger.info(
|
||||
"Started watching repository index %s for updates.", self.repo_index
|
||||
)
|
||||
context.area.tag_redraw() # Update UI to show operator is running if needed
|
||||
if context and context.area:
|
||||
context.area.tag_redraw() # Update UI to show operator is running if needed
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def modal(self, context, event):
|
||||
@@ -84,19 +88,19 @@ class BK_OT_buy_extension_and_watch(Operator):
|
||||
# --- Exit Conditions ---
|
||||
# 1. User closed Preferences or changed area
|
||||
if context.area is None or context.area.type != "PREFERENCES":
|
||||
print("BlenderKit: Preferences window closed or changed, stopping watcher.")
|
||||
bk_logger.info("Preferences window closed or changed, stopping watcher.")
|
||||
self.cancel(context)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# 2. Timeout
|
||||
if current_time - self._start_time > self._max_duration:
|
||||
print("BlenderKit: Watcher timed out, stopping.")
|
||||
bk_logger.info("Watcher timed out, stopping.")
|
||||
self.cancel(context)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# 3. User cancellation
|
||||
if event.type in {"RIGHTMOUSE", "ESC"}:
|
||||
print("BlenderKit: Watcher cancelled by user.")
|
||||
bk_logger.info("Watcher cancelled by user.")
|
||||
self.cancel(context)
|
||||
return {"CANCELLED"}
|
||||
|
||||
@@ -104,24 +108,25 @@ class BK_OT_buy_extension_and_watch(Operator):
|
||||
if event.type == "TIMER":
|
||||
# Check if refresh interval has passed
|
||||
if current_time - self._last_refresh_time >= self._refresh_interval:
|
||||
print(
|
||||
f"BlenderKit: Refresh interval reached, attempting sync for repo index {self.repo_index}..."
|
||||
bk_logger.info(
|
||||
"Refresh interval reached, attempting sync for repo index %s...",
|
||||
self.repo_index,
|
||||
)
|
||||
try:
|
||||
# Check if repo still exists at that index
|
||||
if self.repo_index < len(context.preferences.extensions.repos):
|
||||
bpy.ops.extensions.repo_sync(repo_index=self.repo_index)
|
||||
print(
|
||||
f"BlenderKit: repo_sync called for index {self.repo_index}."
|
||||
bk_logger.info(
|
||||
"repo_sync called for index %s.", self.repo_index
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"BlenderKit: Repository index {self.repo_index} no longer valid."
|
||||
bk_logger.info(
|
||||
"Repository index %s no longer valid.", self.repo_index
|
||||
)
|
||||
# Optionally cancel here if repo is gone
|
||||
except Exception as e:
|
||||
except:
|
||||
# This might fail if another operation is in progress
|
||||
print(f"BlenderKit: extensions.repo_sync failed: {e}")
|
||||
bk_logger.exception("extensions.repo_sync failed.")
|
||||
finally:
|
||||
self._last_refresh_time = (
|
||||
current_time # Reset timer regardless of success
|
||||
@@ -134,13 +139,32 @@ class BK_OT_buy_extension_and_watch(Operator):
|
||||
wm = context.window_manager
|
||||
wm.event_timer_remove(self._timer)
|
||||
self._timer = None
|
||||
print("BlenderKit: Watcher timer removed.")
|
||||
context.area.tag_redraw() # Update UI
|
||||
bk_logger.info("Watcher timer removed.")
|
||||
if context and context.area:
|
||||
context.area.tag_redraw() # Update UI
|
||||
|
||||
|
||||
# --- End New Modal Operator ---
|
||||
|
||||
|
||||
def redraw_preferences_once():
|
||||
"""Tag the redraw on the Blender preferences.
|
||||
Meant to be registered as a timer, runs just once.
|
||||
"""
|
||||
for window in bpy.context.window_manager.windows:
|
||||
screen = window.screen
|
||||
if not screen:
|
||||
continue
|
||||
for area in screen.areas:
|
||||
if area.type != "PREFERENCES":
|
||||
continue
|
||||
for region in area.regions:
|
||||
if region.type in {"UI", "WINDOW"}:
|
||||
region.tag_redraw()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extension_draw_item_blenderkit(
|
||||
layout,
|
||||
*,
|
||||
@@ -163,14 +187,17 @@ def extension_draw_item_blenderkit(
|
||||
cache_reloaded = ensure_repo_cache()
|
||||
if cache_reloaded:
|
||||
# If cache was just reloaded, tag UI for redraw
|
||||
layout.tag_redraw()
|
||||
print("BlenderKit: Cache reloaded, tagging layout for redraw.")
|
||||
# as UILayout doesn't have tag_redraw we call a custom function
|
||||
if bpy.app.timers.is_registered(redraw_preferences_once):
|
||||
bpy.app.timers.unregister(redraw_preferences_once)
|
||||
bpy.app.timers.register(redraw_preferences_once, first_interval=0.01)
|
||||
bk_logger.info("Cache reloaded, tagging preferences for redraw.")
|
||||
|
||||
# check if the cache is already in the window manager
|
||||
if "blenderkit_extensions_repo_cache" not in bpy.context.window_manager:
|
||||
# Log if cache is missing after trying to ensure it
|
||||
print(
|
||||
"BlenderKit: Extension cache not available in window_manager after ensure_repo_cache call."
|
||||
bk_logger.info(
|
||||
"Extension cache not available in window_manager after ensure_repo_cache call."
|
||||
)
|
||||
# Optionally draw a minimal representation or return early to avoid errors
|
||||
# For now, just return to avoid potential errors accessing bk_ext_cache
|
||||
@@ -510,7 +537,7 @@ def clear_repo_cache():
|
||||
|
||||
|
||||
def ensure_repo_cache():
|
||||
"""
|
||||
r"""
|
||||
Reads the .json file blender stores in \extensions\www_blenderkit_com\.blender_ext
|
||||
and parses it to a dict from json, we can use it then for drawing purposes and have the extra data BlenderKit api provides.
|
||||
Checks the modification time of the cache file and reloads it if necessary.
|
||||
@@ -525,10 +552,10 @@ def ensure_repo_cache():
|
||||
# If repo doesn't exist, clear cache if it exists in window manager
|
||||
if cache_key in wm:
|
||||
del wm[cache_key]
|
||||
print(f"BlenderKit: Cleared stale extension cache for missing repository.")
|
||||
bk_logger.info("Cleared stale extension cache for missing repository.")
|
||||
if mtime_key in wm:
|
||||
del wm[mtime_key]
|
||||
print(f"BlenderKit Debug: Repository not found, exiting check.")
|
||||
bk_logger.debug("Repository not found, exiting check.")
|
||||
return False # No repo, nothing loaded
|
||||
|
||||
# get the path to the cache file which is in repository directory under /.blender_ext/index.json
|
||||
@@ -541,13 +568,11 @@ def ensure_repo_cache():
|
||||
if os.path.exists(cache_file):
|
||||
current_mtime = os.path.getmtime(cache_file)
|
||||
except OSError as e: # Handle potential race condition or permission issue
|
||||
print(
|
||||
f"BlenderKit: Warning - Could not get modification time for {cache_file}: {e}"
|
||||
)
|
||||
bk_logger.exception("Could not get modification time for %s.", cache_file)
|
||||
# Clear cache if we can't verify its freshness? Safer approach.
|
||||
if cache_key in wm:
|
||||
del wm[cache_key]
|
||||
print(f"BlenderKit: Cleared extension cache due to mtime access error.")
|
||||
bk_logger.info("Cleared extension cache due to mtime access error.")
|
||||
if mtime_key in wm:
|
||||
del wm[mtime_key]
|
||||
return False # Error, nothing loaded
|
||||
@@ -601,7 +626,7 @@ def ensure_repo_cache():
|
||||
): # Ensure pkg is a dict and 'id' key exists
|
||||
new_cache[pkg["id"][:32]] = pkg
|
||||
else:
|
||||
print(f"BlenderKit: Skipping invalid package entry in cache: {pkg}")
|
||||
bk_logger.info("Skipping invalid package entry in cache: %s.", pkg)
|
||||
|
||||
wm[cache_key] = new_cache
|
||||
wm[mtime_key] = current_mtime # Update mtime only on successful load
|
||||
@@ -609,21 +634,21 @@ def ensure_repo_cache():
|
||||
reloaded_flag = True # Mark that we reloaded successfully
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print(
|
||||
f"BlenderKit: Error decoding JSON from {cache_file}. Cache not loaded/updated."
|
||||
bk_logger.warning(
|
||||
"Error decoding JSON from %s. Cache not loaded/updated.", cache_file
|
||||
)
|
||||
# Clear potentially corrupt cache? Or leave old one? Clearing is safer.
|
||||
if cache_key in wm:
|
||||
del wm[cache_key]
|
||||
print("BlenderKit: Cleared cache due to JSON error.")
|
||||
bk_logger.info("Cleared cache due to JSON error.")
|
||||
if mtime_key in wm:
|
||||
del wm[mtime_key]
|
||||
except Exception as e:
|
||||
print(f"BlenderKit: Error reading or processing cache file {cache_file}: {e}")
|
||||
except Exception:
|
||||
bk_logger.exception("Error reading or processing cache file %s.", cache_file)
|
||||
# Clear potentially corrupt cache?
|
||||
if cache_key in wm:
|
||||
del wm[cache_key]
|
||||
print("BlenderKit: Cleared cache due to file processing error.")
|
||||
bk_logger.info("Cleared cache due to file processing error.")
|
||||
if mtime_key in wm:
|
||||
del wm[mtime_key]
|
||||
|
||||
|
||||
+2
@@ -45,6 +45,7 @@ BLENDERKIT_MATERIAL_UPLOAD_INSTRUCTIONS_URL = (
|
||||
BLENDERKIT_BRUSH_UPLOAD_INSTRUCTIONS_URL = f"{global_vars.SERVER}/docs/uploading-brush/"
|
||||
BLENDERKIT_HDR_UPLOAD_INSTRUCTIONS_URL = f"{global_vars.SERVER}/docs/uploading-hdr/"
|
||||
BLENDERKIT_SCENE_UPLOAD_INSTRUCTIONS_URL = f"{global_vars.SERVER}/docs/uploading-scene/"
|
||||
BLENDERKIT_ADDON_UPLOAD_INSTRUCTIONS_URL = f"{global_vars.SERVER}/add-ons-upload-beta/"
|
||||
BLENDERKIT_LOGIN_URL = f"{global_vars.SERVER}/accounts/login"
|
||||
BLENDERKIT_SIGNUP_URL = f"{global_vars.SERVER}/accounts/register"
|
||||
|
||||
@@ -152,6 +153,7 @@ def get_download_dirs(asset_type):
|
||||
"hdr": "hdrs",
|
||||
"nodegroup": "nodegroups",
|
||||
"printable": "printables",
|
||||
"addon": "addons",
|
||||
}
|
||||
|
||||
dirs = []
|
||||
|
||||
+281
-58
@@ -2,44 +2,62 @@
|
||||
# It is not intended for manual editing.
|
||||
|
||||
[metadata]
|
||||
groups = ["default"]
|
||||
groups = ["default", "dev"]
|
||||
strategy = ["inherit_metadata"]
|
||||
lock_version = "4.5.0"
|
||||
content_hash = "sha256:c76a62dd151343ab8bade20c52ba9ab72a8c97afba4d30e648c98eb624af333e"
|
||||
content_hash = "sha256:e66407ebe96aea59816d07e7d141f2564da883f1073e87bb85c544e60644b85a"
|
||||
|
||||
[[metadata.targets]]
|
||||
requires_python = ">=3.10"
|
||||
requires_python = ">=3.10,<3.13"
|
||||
|
||||
[[package]]
|
||||
name = "bandit"
|
||||
version = "1.8.6"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Security oriented static analyser for python code."
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"PyYAML>=5.3.1",
|
||||
"colorama>=0.3.9; platform_system == \"Windows\"",
|
||||
"rich",
|
||||
"stevedore>=1.20.0",
|
||||
]
|
||||
files = [
|
||||
{file = "bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0"},
|
||||
{file = "bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "24.1.0"
|
||||
requires_python = ">=3.8"
|
||||
version = "25.9.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "The uncompromising code formatter."
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"click>=8.0.0",
|
||||
"mypy-extensions>=0.4.3",
|
||||
"packaging>=22.0",
|
||||
"pathspec>=0.9.0",
|
||||
"platformdirs>=2",
|
||||
"pytokens>=0.1.10",
|
||||
"tomli>=1.1.0; python_version < \"3.11\"",
|
||||
"typing-extensions>=4.0.1; python_version < \"3.11\"",
|
||||
]
|
||||
files = [
|
||||
{file = "black-24.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94d5280d020dadfafc75d7cae899609ed38653d3f5e82e7ce58f75e76387ed3d"},
|
||||
{file = "black-24.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aaf9aa85aaaa466bf969e7dd259547f4481b712fe7ee14befeecc152c403ee05"},
|
||||
{file = "black-24.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec489cae76eac3f7573629955573c3a0e913641cafb9e3bfc87d8ce155ebdb29"},
|
||||
{file = "black-24.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5a0100b4bdb3744dd68412c3789f472d822dc058bb3857743342f8d7f93a5a7"},
|
||||
{file = "black-24.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6cc5a6ba3e671cfea95a40030b16a98ee7dc2e22b6427a6f3389567ecf1b5262"},
|
||||
{file = "black-24.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0e367759062dcabcd9a426d12450c6d61faf1704a352a49055a04c9f9ce8f5a"},
|
||||
{file = "black-24.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be305563ff4a2dea813f699daaffac60b977935f3264f66922b1936a5e492ee4"},
|
||||
{file = "black-24.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a8977774929b5db90442729f131221e58cc5d8208023c6af9110f26f75b6b20"},
|
||||
{file = "black-24.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d74d4d0da276fbe3b95aa1f404182562c28a04402e4ece60cf373d0b902f33a0"},
|
||||
{file = "black-24.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39addf23f7070dbc0b5518cdb2018468ac249d7412a669b50ccca18427dba1f3"},
|
||||
{file = "black-24.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:827a7c0da520dd2f8e6d7d3595f4591aa62ccccce95b16c0e94bb4066374c4c2"},
|
||||
{file = "black-24.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:0cd59d01bf3306ff7e3076dd7f4435fcd2fafe5506a6111cae1138fc7de52382"},
|
||||
{file = "black-24.1.0-py3-none-any.whl", hash = "sha256:5134a6f6b683aa0a5592e3fd61dd3519d8acd953d93e2b8b76f9981245b65594"},
|
||||
{file = "black-24.1.0.tar.gz", hash = "sha256:30fbf768cd4f4576598b1db0202413fafea9a227ef808d1a12230c643cefe9fc"},
|
||||
{file = "black-25.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7"},
|
||||
{file = "black-25.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92"},
|
||||
{file = "black-25.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713"},
|
||||
{file = "black-25.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1"},
|
||||
{file = "black-25.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa"},
|
||||
{file = "black-25.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d"},
|
||||
{file = "black-25.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608"},
|
||||
{file = "black-25.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f"},
|
||||
{file = "black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0"},
|
||||
{file = "black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4"},
|
||||
{file = "black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e"},
|
||||
{file = "black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a"},
|
||||
{file = "black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae"},
|
||||
{file = "black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -126,17 +144,16 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
requires_python = ">=3.7"
|
||||
version = "8.3.0"
|
||||
requires_python = ">=3.10"
|
||||
summary = "Composable command line interface toolkit"
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"colorama; platform_system == \"Windows\"",
|
||||
"importlib-metadata; python_version < \"3.8\"",
|
||||
]
|
||||
files = [
|
||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||
{file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"},
|
||||
{file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -144,13 +161,24 @@ name = "colorama"
|
||||
version = "0.4.6"
|
||||
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
summary = "Cross-platform colored terminal text."
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
marker = "platform_system == \"Windows\""
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docstring-parser-fork"
|
||||
version = "0.0.14"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Parse Python docstrings in reST, Google and Numpydoc format"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "docstring_parser_fork-0.0.14-py3-none-any.whl", hash = "sha256:4c544f234ef2cc2749a3df32b70c437d77888b1099143a1ad5454452c574b9af"},
|
||||
{file = "docstring_parser_fork-0.0.14.tar.gz", hash = "sha256:a2743a63d8d36c09650594f7b4ab5b2758fee8629dcf794d1b221b23179baa5c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
@@ -164,13 +192,38 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.13.2"
|
||||
requires_python = ">=3.8.0"
|
||||
version = "7.0.0"
|
||||
requires_python = ">=3.10.0"
|
||||
summary = "A Python utility / library to sort Python imports."
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
|
||||
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
|
||||
{file = "isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1"},
|
||||
{file = "isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
requires_python = ">=3.10"
|
||||
summary = "Python port of markdown-it. Markdown parsing, done right!"
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"mdurl~=0.1",
|
||||
]
|
||||
files = [
|
||||
{file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"},
|
||||
{file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
requires_python = ">=3.7"
|
||||
summary = "Markdown URL utilities"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -178,7 +231,7 @@ name = "mypy"
|
||||
version = "1.13.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Optional static typing for Python"
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"mypy-extensions>=1.0.0",
|
||||
"tomli>=1.1.0; python_version < \"3.11\"",
|
||||
@@ -211,13 +264,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
requires_python = ">=3.5"
|
||||
version = "1.1.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Type system extensions for programs checked with the mypy type checker."
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
{file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
|
||||
{file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -256,13 +309,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
version = "25.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Core utilities for Python packages"
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
|
||||
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -270,7 +323,7 @@ name = "pathspec"
|
||||
version = "0.12.1"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Utility library for gitignore style pattern matching of file paths."
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||
@@ -278,13 +331,89 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.6"
|
||||
requires_python = ">=3.8"
|
||||
version = "4.5.0"
|
||||
requires_python = ">=3.10"
|
||||
summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
|
||||
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
|
||||
{file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"},
|
||||
{file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydoclint"
|
||||
version = "0.7.3"
|
||||
requires_python = ">=3.9"
|
||||
summary = "A Python docstring linter that checks arguments, returns, yields, and raises sections"
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"click>=8.1.0",
|
||||
"docstring-parser-fork>=0.0.12",
|
||||
"tomli>=2.0.1; python_version < \"3.11\"",
|
||||
]
|
||||
files = [
|
||||
{file = "pydoclint-0.7.3-py3-none-any.whl", hash = "sha256:a656b0e863565644670ded19a4b506450364e4f1f5e8ff7705d6ba8bb5a82982"},
|
||||
{file = "pydoclint-0.7.3.tar.gz", hash = "sha256:3351d5eeb19f8831d992714f71f5ea1175af649503d39b9da0071445a4002138"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Pygments is a syntax highlighting package written in Python."
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
|
||||
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytokens"
|
||||
version = "0.2.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "A Fast, spec compliant Python 3.13+ tokenizer that runs on older Pythons."
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8"},
|
||||
{file = "pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
requires_python = ">=3.8"
|
||||
summary = "YAML parser and emitter for Python"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"},
|
||||
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -304,16 +433,110 @@ files = [
|
||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.2.0"
|
||||
requires_python = ">=3.8.0"
|
||||
summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"markdown-it-py>=2.2.0",
|
||||
"pygments<3.0.0,>=2.13.0",
|
||||
]
|
||||
files = [
|
||||
{file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"},
|
||||
{file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.1"
|
||||
requires_python = ">=3.7"
|
||||
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b"},
|
||||
{file = "ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224"},
|
||||
{file = "ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192"},
|
||||
{file = "ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd"},
|
||||
{file = "ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020"},
|
||||
{file = "ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5"},
|
||||
{file = "ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d"},
|
||||
{file = "ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6"},
|
||||
{file = "ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1"},
|
||||
{file = "ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44"},
|
||||
{file = "ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stevedore"
|
||||
version = "5.5.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Manage dynamic plugins for Python applications"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "stevedore-5.5.0-py3-none-any.whl", hash = "sha256:18363d4d268181e8e8452e71a38cd77630f345b2ef6b4a8d5614dac5ee0d18cf"},
|
||||
{file = "stevedore-5.5.0.tar.gz", hash = "sha256:d31496a4f4df9825e1a1e4f1f74d19abb0154aff311c3b376fcc89dae8fccd73"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "A lil' TOML parser"
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
marker = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"},
|
||||
{file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"},
|
||||
{file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"},
|
||||
{file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -332,13 +555,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
groups = ["default"]
|
||||
version = "4.15.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Backported and Experimental Type Hints for Python 3.9+"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+63
-6
@@ -1,12 +1,23 @@
|
||||
[project]
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.10,<3.13" # in order to support numpy<2.0.0
|
||||
dependencies = [
|
||||
"requests>=2.18.4",
|
||||
"types-requests>=2.31.0.5",
|
||||
"numpy<2.0.0",
|
||||
"black==24.1.0",
|
||||
"isort==5.13.2",
|
||||
"numpy>=1.21.0,<2.0.0",
|
||||
]
|
||||
|
||||
# these will not be included in build distributions
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
## dev dependencies
|
||||
"black==25.9.0",
|
||||
"isort==7.0.0",
|
||||
"mypy==1.13.0",
|
||||
|
||||
## I would like to enable these in the future
|
||||
"bandit>=1.8.2", # code is currently invalid for bandit
|
||||
"ruff>=0.14.1", # code is currently invalid for ruff
|
||||
"pydoclint>=0.7.3", # code is currently invalid for pydoclint
|
||||
]
|
||||
|
||||
[tool.isort]
|
||||
@@ -23,11 +34,50 @@ exclude = '''
|
||||
'''
|
||||
|
||||
[tool.ruff]
|
||||
exclude = ["lib", "out", "addon_updater.py", "addon_updater_ops.py"]
|
||||
target-version = "py310"
|
||||
line-length = 120
|
||||
include = ["pyproject.toml",
|
||||
"**/*.py",
|
||||
"*.py"
|
||||
]
|
||||
exclude = [
|
||||
"lib",
|
||||
"out",
|
||||
"addon_updater.py",
|
||||
"addon_updater_ops.py",
|
||||
"_debug/**",
|
||||
"_bck/**",
|
||||
"*.tmp",
|
||||
"__pycache__/**",
|
||||
".venv/**",
|
||||
"*.pyi",
|
||||
"sentry_sdk",
|
||||
]
|
||||
ignore = [
|
||||
"E501", # Line too long
|
||||
]
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.format]
|
||||
line-ending = "auto"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["F", "E", "W", "C90", "I", "N", "D", "UP", "S1", "S2", "S3", "S5", "BLE", "FBT",
|
||||
"B", "A", "C4", "COM", "DTZ", "T10", "FA", "ISC", "ICN", "LOG", "G", "INP", "PIE", "T20", "Q", "RSE", "RET", "SLF",
|
||||
"SLOT", "SIM", "TID",
|
||||
"TC001", "TC004", "TC005", "TC010",
|
||||
"INT", "ARG", "FIX", "ERA", "PL", "TRY", "FLY", "PERF", "RUF"
|
||||
]
|
||||
|
||||
ignore = [
|
||||
"D105", # https://docs.astral.sh/ruff/rules/undocumented-magic-method/
|
||||
"D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/
|
||||
"PLC0415", # https://docs.astral.sh/ruff/rules/import-outside-top-level/
|
||||
"RET504", # https://docs.astral.sh/ruff/rules/unnecessary-assign/
|
||||
]
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
|
||||
[tool.mypy]
|
||||
exclude = ['test_*', 'out', 'lib']
|
||||
@@ -46,3 +96,10 @@ module = [
|
||||
]
|
||||
ignore_missing_imports = true # Ignore missing type hints for bpy
|
||||
|
||||
[tool.pydoclint]
|
||||
style = "google"
|
||||
arg-type-hints-in-docstring = false
|
||||
check-return-types = false
|
||||
ignore-private-args = true
|
||||
ignore-underscore-args = true
|
||||
exclude = "\\.venv|\\sentry_sdk"
|
||||
|
||||
+8
-1
@@ -21,6 +21,7 @@ from logging import getLogger
|
||||
from os.path import basename
|
||||
from re import search
|
||||
from time import time
|
||||
from typing import Literal
|
||||
|
||||
import bpy
|
||||
|
||||
@@ -32,7 +33,12 @@ reports = []
|
||||
|
||||
|
||||
# check for same reports and just make them longer by the timeout.
|
||||
def add_report(text="", timeout=-1, type="INFO", details=""):
|
||||
def add_report(
|
||||
text: str = "",
|
||||
timeout: float = -1,
|
||||
type: Literal["INFO", "ERROR", "VALIDATOR"] = "INFO",
|
||||
details: str = "",
|
||||
) -> None:
|
||||
"""Add text report to GUI. Function checks for same reports and make them longer by the timeout.
|
||||
Also log the text and details into the console with levels: ERROR=RED, INFO=GREEN, VALIDATOR=BLUE.
|
||||
When timeout is not specified, default 15s will be used for ERROR, 5s for INFO/VALIDATOR.
|
||||
@@ -41,6 +47,7 @@ def add_report(text="", timeout=-1, type="INFO", details=""):
|
||||
text = text.strip()
|
||||
full_message = text
|
||||
details = details.strip()
|
||||
color = colors.GRAY
|
||||
if details != "":
|
||||
full_message = f"{text} {details}"
|
||||
|
||||
|
||||
+108
-6
@@ -38,6 +38,7 @@ from . import (
|
||||
client_tasks,
|
||||
comments_utils,
|
||||
datas,
|
||||
download,
|
||||
global_vars,
|
||||
image_utils,
|
||||
paths,
|
||||
@@ -166,6 +167,8 @@ def check_clipboard():
|
||||
target_asset_type = "PRINTABLE"
|
||||
elif asset_type_string.find("nodegroup") > -1:
|
||||
target_asset_type = "NODEGROUP"
|
||||
elif asset_type_string.find("addon") > -1 or asset_type_string.find("add-on") > -1:
|
||||
target_asset_type = "ADDON"
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if ui_props.asset_type != target_asset_type:
|
||||
ui_props.asset_type = target_asset_type # switch asset type before placing keywords, so it does not search under wrong asset type
|
||||
@@ -177,7 +180,8 @@ def check_clipboard():
|
||||
# TODO: type annotate and check this crazy function!
|
||||
# Are we sure it behaves correctly on network issues, malfunctioning search etc?
|
||||
def parse_result(r) -> dict:
|
||||
"""Needed to generate some extra data in the result(by now)
|
||||
"""Parse search result into an asset_data by tweaking some of its parameters.
|
||||
We need to generate some extra data in the result (for now).
|
||||
Parameters
|
||||
----------
|
||||
r - search result, also called asset_data
|
||||
@@ -192,10 +196,6 @@ def parse_result(r) -> dict:
|
||||
utils.p("asset with no files-size")
|
||||
|
||||
asset_type = r["assetType"]
|
||||
# TODO remove this condition so all assets are parsed?
|
||||
if len(r["files"]) == 0:
|
||||
return {}
|
||||
|
||||
adata = r["author"]
|
||||
social_networks = datas.parse_social_networks(adata.pop("socialNetworks", []))
|
||||
author = datas.UserProfile(**adata, socialNetworks=social_networks)
|
||||
@@ -389,6 +389,20 @@ def handle_search_task(task: client_tasks.Task) -> bool:
|
||||
if comments is None:
|
||||
client_lib.get_comments(asset_data["assetBaseId"])
|
||||
|
||||
# Apply addon-specific status checking and filtering if needed
|
||||
if ui_props.asset_type == "ADDON":
|
||||
# Always process addon search results to store installation status
|
||||
result_field = filter_addon_search_results(
|
||||
result_field, filter_installed_only=False
|
||||
)
|
||||
|
||||
addon_props = bpy.context.window_manager.blenderkit_addon
|
||||
if addon_props.search_installed:
|
||||
# Filter to only show installed addons
|
||||
result_field = [
|
||||
asset for asset in result_field if asset.get("downloaded", 0) > 0
|
||||
]
|
||||
|
||||
# Store results in history step
|
||||
history_step["search_results"] = result_field
|
||||
history_step["search_results_orig"] = task.result
|
||||
@@ -769,7 +783,11 @@ def decide_ordering(query: dict) -> list:
|
||||
# for validators, sort uploaded from oldest
|
||||
order.append("last_blend_upload")
|
||||
else:
|
||||
order.append("-last_blend_upload")
|
||||
if query.get("asset_type") == "addon":
|
||||
# addons don't have athe blend so need to sort by created
|
||||
order.append("-created")
|
||||
else:
|
||||
order.append("-last_blend_upload")
|
||||
elif (
|
||||
query.get("author_id") is not None
|
||||
or query.get("query", "").find("+author_id:") > -1
|
||||
@@ -929,6 +947,68 @@ def build_query_nodegroup(
|
||||
return build_query_common(query, props, ui_props)
|
||||
|
||||
|
||||
def build_query_addon(props, ui_props) -> dict:
|
||||
"""Pure function to construct search query dict for addons."""
|
||||
query = {"asset_type": "addon"}
|
||||
return build_query_common(query, props, ui_props)
|
||||
|
||||
|
||||
def filter_addon_search_results(search_results, filter_installed_only=False):
|
||||
"""
|
||||
Filter addon search results based on local installation status.
|
||||
This is called after search results arrive since installation info isn't stored on server.
|
||||
Also stores installation and enablement status in the search results data.
|
||||
|
||||
Args:
|
||||
search_results: List of addon asset data from search
|
||||
filter_installed_only: If True, only return installed addons
|
||||
|
||||
Returns:
|
||||
Filtered list of add-on assets with installation status stored
|
||||
"""
|
||||
|
||||
filtered_results = []
|
||||
|
||||
for asset in search_results:
|
||||
if asset.get("assetType") != "addon":
|
||||
# Skip non-addon assets (shouldn't happen in addon search but safety check)
|
||||
if not filter_installed_only:
|
||||
filtered_results.append(asset)
|
||||
continue
|
||||
|
||||
# Check installation and enablement status for addon
|
||||
try:
|
||||
status = download.get_addon_installation_status(asset)
|
||||
is_installed = status.get("installed", False)
|
||||
is_enabled = status.get("enabled", False)
|
||||
|
||||
# Store installation status in asset data using existing 'downloaded' field
|
||||
# Use 100 for installed, 0 for not installed (matching existing pattern)
|
||||
asset["downloaded"] = 100 if is_installed else 0
|
||||
|
||||
# Store enablement status in new 'enabled' field
|
||||
asset["enabled"] = is_enabled
|
||||
|
||||
if filter_installed_only:
|
||||
if is_installed:
|
||||
filtered_results.append(asset)
|
||||
else:
|
||||
filtered_results.append(asset)
|
||||
|
||||
except Exception as e:
|
||||
# If we can't determine status, mark as not installed/enabled
|
||||
bk_logger.warning(
|
||||
f"Could not determine installation status for addon {asset.get('name', 'Unknown')}: {e}"
|
||||
)
|
||||
asset["downloaded"] = 0
|
||||
asset["enabled"] = False
|
||||
|
||||
if not filter_installed_only:
|
||||
filtered_results.append(asset)
|
||||
|
||||
return filtered_results
|
||||
|
||||
|
||||
def add_search_process(
|
||||
query, get_next: bool, page_size: int, next_url: str, history_id: str
|
||||
):
|
||||
@@ -1106,6 +1186,12 @@ def search(get_next=False, query=None, author_id=""):
|
||||
ui_props=bpy.context.window_manager.blenderkitUI,
|
||||
)
|
||||
|
||||
if ui_props.asset_type == "ADDON":
|
||||
query = build_query_addon(
|
||||
props=bpy.context.window_manager.blenderkit_addon,
|
||||
ui_props=bpy.context.window_manager.blenderkitUI,
|
||||
)
|
||||
|
||||
# crop long searches
|
||||
if query.get("query"):
|
||||
if len(query["query"]) > 50:
|
||||
@@ -1232,6 +1318,8 @@ def update_filters():
|
||||
sprops.use_filters = sprops.true_hdr
|
||||
elif ui_props.asset_type == "NODEGROUP":
|
||||
sprops.use_filters = fcommon
|
||||
elif ui_props.asset_type == "ADDON":
|
||||
sprops.use_filters = fcommon
|
||||
return True
|
||||
|
||||
|
||||
@@ -1280,6 +1368,9 @@ def detect_asset_type_from_keywords(keywords: str) -> tuple[str, str]:
|
||||
"nodegroup": "NODEGROUP",
|
||||
"node": "NODEGROUP",
|
||||
"printable": "PRINTABLE",
|
||||
"addon": "ADDON",
|
||||
"add-on": "ADDON",
|
||||
"extension": "ADDON",
|
||||
}
|
||||
|
||||
# Convert to lowercase for matching
|
||||
@@ -1357,6 +1448,8 @@ def search_update(self, context):
|
||||
target_asset_type = "NODEGROUP"
|
||||
elif asset_type_string.find("printable") > -1:
|
||||
target_asset_type = "PRINTABLE"
|
||||
elif asset_type_string.find("addon") > -1:
|
||||
target_asset_type = "ADDON"
|
||||
|
||||
if ui_props.asset_type != target_asset_type:
|
||||
ui_props.search_keywords = ""
|
||||
@@ -1655,6 +1748,8 @@ def get_ui_state():
|
||||
store_props = store_scene_props
|
||||
elif asset_type == "PRINTABLE":
|
||||
store_props = store_model_props
|
||||
elif asset_type == "ADDON":
|
||||
store_props = [] # Addons don't need to store specific props
|
||||
|
||||
search_props = utils.get_search_props()
|
||||
|
||||
@@ -1664,6 +1759,13 @@ def get_ui_state():
|
||||
if prop_name != "rna_type":
|
||||
ui_state["search_props"][prop_name] = getattr(search_props, prop_name)
|
||||
|
||||
# Store addon-specific search properties
|
||||
if ui_props.asset_type == "ADDON":
|
||||
addon_props = bpy.context.window_manager.blenderkit_addon
|
||||
ui_state["addon_props"] = {
|
||||
"search_installed": addon_props.search_installed,
|
||||
}
|
||||
|
||||
return ui_state
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -225,7 +225,7 @@ class ParticlesDropDialog(bpy.types.Operator):
|
||||
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 too many particles,"
|
||||
"The number of particles is calculated automatically, but if there are too many particles,"
|
||||
" BlenderKit can do the following steps to make sure Blender continues to run:\n"
|
||||
"\n1.Switch to bounding box view of the particles."
|
||||
"\n2.Turn down number of particles that are shown in the view."
|
||||
|
||||
+261
-26
@@ -16,16 +16,161 @@
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import blf
|
||||
import gpu
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
from bpy import app
|
||||
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
from .image_utils import IMG
|
||||
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
cached_images = {}
|
||||
|
||||
cached_gpu_textures = {}
|
||||
|
||||
_cached_image_shader: Optional[gpu.types.GPUShader] = None
|
||||
|
||||
|
||||
VERTEX_SHADER_LEGACY = """
|
||||
uniform mat4 ModelViewProjectionMatrix;
|
||||
in vec2 pos;
|
||||
in vec2 texCoord;
|
||||
out vec2 uv;
|
||||
|
||||
void main()
|
||||
{
|
||||
uv = texCoord;
|
||||
gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0, 1.0);
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
FRAGMENT_SHADER_LEGACY = """
|
||||
in vec2 uv;
|
||||
out vec4 fragColor;
|
||||
uniform sampler2D image;
|
||||
uniform float transparency;
|
||||
uniform int color_space_mode;
|
||||
|
||||
vec3 linear_to_srgb(vec3 linear_color)
|
||||
{
|
||||
vec3 cutoff = vec3(0.0031308);
|
||||
vec3 lower = linear_color * 12.92;
|
||||
vec3 higher = 1.055 * pow(max(linear_color, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;
|
||||
return mix(lower, higher, step(cutoff, linear_color));
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
vec4 color = texture(image, uv);
|
||||
if (color_space_mode == 1) {
|
||||
color.rgb = linear_to_srgb(color.rgb);
|
||||
}
|
||||
color.a *= transparency;
|
||||
fragColor = color;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def create_image_shader_info():
|
||||
"""Return GPU shader info for the runtime image shader."""
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
shader_info.vertex_in(0, "VEC2", "pos")
|
||||
shader_info.vertex_in(1, "VEC2", "texCoord")
|
||||
|
||||
stage_iface = gpu.types.GPUStageInterfaceInfo("uv_iface")
|
||||
stage_iface.smooth("VEC2", "uv")
|
||||
shader_info.vertex_out(stage_iface)
|
||||
|
||||
shader_info.push_constant("MAT4", "ModelViewProjectionMatrix")
|
||||
shader_info.push_constant("FLOAT", "transparency")
|
||||
shader_info.push_constant("INT", "color_space_mode")
|
||||
shader_info.sampler(0, "FLOAT_2D", "image")
|
||||
|
||||
shader_info.fragment_out(0, "VEC4", "fragColor")
|
||||
shader_info.vertex_source(
|
||||
"""
|
||||
void main()
|
||||
{
|
||||
uv = texCoord;
|
||||
gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0, 1.0);
|
||||
}
|
||||
"""
|
||||
)
|
||||
shader_info.fragment_source(
|
||||
"""
|
||||
void main()
|
||||
{
|
||||
vec4 color = texture(image, uv);
|
||||
if (color_space_mode == 1) {
|
||||
vec3 cutoff = vec3(0.0031308);
|
||||
vec3 lower = color.rgb * 12.92;
|
||||
vec3 higher = 1.055 * pow(max(color.rgb, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;
|
||||
color.rgb = mix(lower, higher, step(cutoff, color.rgb));
|
||||
}
|
||||
color.a *= transparency;
|
||||
fragColor = color;
|
||||
}
|
||||
"""
|
||||
)
|
||||
return shader_info
|
||||
|
||||
|
||||
def create_image_shader():
|
||||
"""Return a cached shader that supports transparency across Blender versions.
|
||||
Features:
|
||||
- sRGB conversion for UI overlays
|
||||
- transparency
|
||||
"""
|
||||
global _cached_image_shader
|
||||
|
||||
if _cached_image_shader is not None:
|
||||
return _cached_image_shader
|
||||
|
||||
shader = None
|
||||
|
||||
create_info_supported = (
|
||||
hasattr(gpu, "shader")
|
||||
and hasattr(gpu.shader, "create_from_info")
|
||||
and hasattr(gpu.types, "GPUShaderCreateInfo")
|
||||
)
|
||||
|
||||
if create_info_supported:
|
||||
try:
|
||||
shader_info = create_image_shader_info()
|
||||
shader = gpu.shader.create_from_info(shader_info)
|
||||
except Exception: # noqa: BLE001
|
||||
bk_logger.exception("Failed to create image shader")
|
||||
shader = None
|
||||
|
||||
if shader is None:
|
||||
try:
|
||||
shader = gpu.types.GPUShader(VERTEX_SHADER_LEGACY, FRAGMENT_SHADER_LEGACY)
|
||||
except Exception: # noqa: BLE001
|
||||
bk_logger.exception("Failed to create image shader")
|
||||
|
||||
if shader is None:
|
||||
# fallback to builtin shader
|
||||
# mainly for MacOS builds that have issues with custom shaders
|
||||
if app.version < (4, 0, 0):
|
||||
shader = gpu.shader.from_builtin("2D_IMAGE")
|
||||
else:
|
||||
shader = gpu.shader.from_builtin("IMAGE")
|
||||
|
||||
_cached_image_shader = shader
|
||||
return shader
|
||||
|
||||
|
||||
def draw_rect(x, y, width, height, color):
|
||||
"""Used for drawing 2D rectangle backgrounds."""
|
||||
xmax = x + width
|
||||
ymax = y + height
|
||||
points = (
|
||||
@@ -48,6 +193,31 @@ def draw_rect(x, y, width, height, color):
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
def draw_rect_outline(x, y, width, height, color, line_width=1.0):
|
||||
"""Used for drawing 2D rectangle outlines."""
|
||||
xmax = x + width
|
||||
ymax = y + height
|
||||
coords = (
|
||||
(x, y), # (x, y)
|
||||
(x, ymax), # (x, y)
|
||||
(xmax, ymax), # (x, y)
|
||||
(xmax, y), # (x, y)
|
||||
)
|
||||
indices = ((0, 1), (1, 2), (2, 3), (3, 0))
|
||||
|
||||
if app.version < (4, 0, 0):
|
||||
shader = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
|
||||
else:
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
batch = batch_for_shader(shader, "LINES", {"pos": coords}, indices=indices)
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.line_width_set(line_width)
|
||||
shader.bind()
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
def draw_line2d(x1, y1, x2, y2, width, color):
|
||||
"""Used for drawing line from dragged thumbnail to the 3D bounding box."""
|
||||
coords = ((x1, y1), (x2, y2))
|
||||
@@ -115,30 +285,59 @@ def draw_lines(vertices, indices, color):
|
||||
|
||||
|
||||
def draw_rect_3d(coords, color):
|
||||
"""Used for drawing 3D rectangle backgrounds."""
|
||||
indices = [(0, 1, 2), (2, 3, 0)]
|
||||
if app.version < (4, 0, 0):
|
||||
shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
else:
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
batch = batch_for_shader(shader, "TRIS", {"pos": coords}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
shader.bind()
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
cached_images = {}
|
||||
def _resolve_color_space_mode() -> int:
|
||||
"""Return shader color conversion mode for the current drawing context.
|
||||
|
||||
area over non-3D means UI overlay, so we need to apply sRGB conversion."""
|
||||
area = getattr(bpy.context, "area", None)
|
||||
if area is None:
|
||||
return 0
|
||||
|
||||
# Blender 5.0+ node editors already expect linear data, so avoid extra conversion there
|
||||
node_editor_types = {"NODE_EDITOR", "VIEW_3D"}
|
||||
|
||||
if area.type in node_editor_types:
|
||||
return 0
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
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))
|
||||
def draw_image_runtime(
|
||||
x: float,
|
||||
y: float,
|
||||
width: float,
|
||||
height: float,
|
||||
image: Union[bpy.types.Image, IMG],
|
||||
transparency: Optional[float] = 1.0,
|
||||
crop: Tuple[float, float, float, float] = (0, 0, 1, 1),
|
||||
batch: Optional[gpu.types.GPUBatch] = None,
|
||||
) -> Optional[gpu.types.GPUBatch]:
|
||||
"""Draws an image at given location with given size.
|
||||
|
||||
try:
|
||||
image.name
|
||||
except:
|
||||
print("Image is invalid- draw function")
|
||||
return
|
||||
Returns:
|
||||
The batch object if successful, or None if the image is invalid.
|
||||
"""
|
||||
if not image.name or not image.filepath:
|
||||
return None
|
||||
|
||||
ci = cached_images.get(image.filepath)
|
||||
image_shader = create_image_shader()
|
||||
|
||||
texture = None
|
||||
ci = cached_images.get(image.filepath + "GPU_TEXTURE")
|
||||
if ci is not None:
|
||||
if (
|
||||
ci["x"] == x
|
||||
@@ -149,6 +348,7 @@ def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1), batc
|
||||
batch = ci["batch"]
|
||||
image_shader = ci["image_shader"]
|
||||
texture = ci["texture"]
|
||||
|
||||
if not batch:
|
||||
coords = [(x, y), (x + width, y), (x, y + height), (x + width, y + height)]
|
||||
|
||||
@@ -161,16 +361,14 @@ def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1), batc
|
||||
|
||||
indices = [(0, 1, 2), (2, 1, 3)]
|
||||
|
||||
if app.version < (4, 0, 0):
|
||||
image_shader = gpu.shader.from_builtin("2D_IMAGE")
|
||||
else:
|
||||
image_shader = gpu.shader.from_builtin("IMAGE")
|
||||
batch = batch_for_shader(
|
||||
image_shader, "TRIS", {"pos": coords, "texCoord": uvs}, indices=indices
|
||||
)
|
||||
texture = gpu.texture.from_image(image)
|
||||
|
||||
texture = path_to_gpu_texture(image.filepath)
|
||||
|
||||
# tell shader to use the image that is bound to image unit 0
|
||||
cached_images[image.filepath] = {
|
||||
cached_images[image.filepath + "GPU_TEXTURE"] = {
|
||||
"x": x,
|
||||
"y": y,
|
||||
"width": width,
|
||||
@@ -179,19 +377,56 @@ def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1), batc
|
||||
"image_shader": image_shader,
|
||||
"texture": texture,
|
||||
}
|
||||
# send image to gpu if it isn't there already
|
||||
if image.gl_load():
|
||||
raise Exception()
|
||||
|
||||
# texture = gpu.texture.from_image(image)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
image_shader.bind()
|
||||
image_shader.uniform_sampler("image", texture)
|
||||
batch.draw(image_shader)
|
||||
if batch is None:
|
||||
return None
|
||||
|
||||
if image_shader and texture:
|
||||
color_space_mode = _resolve_color_space_mode()
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
image_shader.bind()
|
||||
image_shader.uniform_sampler("image", texture)
|
||||
|
||||
# may not be available in simple shader
|
||||
try:
|
||||
# set floats
|
||||
image_shader.uniform_float("transparency", transparency)
|
||||
|
||||
# set color space mode
|
||||
image_shader.uniform_int("color_space_mode", color_space_mode)
|
||||
batch.draw(image_shader)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return batch
|
||||
|
||||
|
||||
def path_to_gpu_texture(path: str) -> Optional[gpu.types.GPUTexture]:
|
||||
"""Convert a Blender image to a GPU texture.
|
||||
|
||||
Returns:
|
||||
The GPU texture if successful, or None if the image is invalid.
|
||||
"""
|
||||
# check if exists and is file [prevent exception for missing files]
|
||||
if path in cached_gpu_textures:
|
||||
return cached_gpu_textures[path]
|
||||
|
||||
if not os.path.exists(path) or not os.path.isfile(path):
|
||||
# do not spam log with warnings, just return None
|
||||
return None
|
||||
img = bpy.data.images.load(path, check_existing=False)
|
||||
img.gl_load()
|
||||
|
||||
tex = gpu.texture.from_image(img)
|
||||
cached_gpu_textures[path] = tex
|
||||
|
||||
# # Clean up Blender image
|
||||
bpy.data.images.remove(img)
|
||||
return tex
|
||||
|
||||
|
||||
def get_text_size(font_id=0, text="", text_size=16, dpi=72):
|
||||
if app.version < (4, 0, 0):
|
||||
blf.size(font_id, text_size, dpi)
|
||||
|
||||
+232
-67
@@ -16,10 +16,8 @@
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import time
|
||||
from webbrowser import open_new_tab
|
||||
@@ -87,6 +85,9 @@ def draw_upload_common(layout, props, asset_type, context):
|
||||
url = (
|
||||
paths.BLENDERKIT_MODEL_UPLOAD_INSTRUCTIONS_URL
|
||||
) # Reuse model instructions since prints are similar
|
||||
if asset_type == "ADDON":
|
||||
asset_type_text = asset_type
|
||||
url = paths.BLENDERKIT_ADDON_UPLOAD_INSTRUCTIONS_URL
|
||||
op = layout.operator(
|
||||
"wm.url_open", text=f"Read {asset_type} upload instructions", icon="QUESTION"
|
||||
)
|
||||
@@ -225,6 +226,19 @@ def draw_panel_hdr_search(self, context):
|
||||
utils.label_multiline(layout, text=props.report)
|
||||
|
||||
|
||||
def draw_panel_addon_search(self, context):
|
||||
wm = context.window_manager
|
||||
ui_props = wm.blenderkitUI
|
||||
addon_props = wm.blenderkit_addon
|
||||
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.prop(ui_props, "search_keywords", text="", icon="VIEWZOOM")
|
||||
draw_assetbar_show_hide(row, addon_props)
|
||||
|
||||
utils.label_multiline(layout, text=addon_props.report)
|
||||
|
||||
|
||||
def draw_panel_nodegroup_upload(self, context):
|
||||
layout = self.layout
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
@@ -458,6 +472,8 @@ def draw_model_context_menu(self, context):
|
||||
layout = self.layout
|
||||
|
||||
o = utils.get_active_model()
|
||||
if not o:
|
||||
return
|
||||
if o.get("asset_data") is None:
|
||||
utils.label_multiline(
|
||||
layout,
|
||||
@@ -1563,6 +1579,32 @@ class VIEW3D_PT_blenderkit_advanced_nodegroup_search(Panel):
|
||||
draw_common_filters(self.layout, ui_props)
|
||||
|
||||
|
||||
class VIEW3D_PT_blenderkit_advanced_addon_search(Panel):
|
||||
bl_category = "BlenderKit"
|
||||
bl_idname = "VIEW3D_PT_blenderkit_advanced_addon_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):
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if not global_vars.CLIENT_RUNNING:
|
||||
return False
|
||||
return ui_props.down_up == "SEARCH" and ui_props.asset_type == "ADDON"
|
||||
|
||||
def draw(self, context):
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
draw_common_filters(self.layout, ui_props)
|
||||
layout = self.layout
|
||||
addon_props = bpy.context.window_manager.blenderkit_addon
|
||||
# Add installed filter for addons
|
||||
row = layout.row()
|
||||
row.prop(addon_props, "search_installed", text="Installed Only")
|
||||
|
||||
|
||||
class VIEW3D_PT_blenderkit_advanced_printable_search(Panel):
|
||||
bl_category = "BlenderKit"
|
||||
bl_idname = "VIEW3D_PT_blenderkit_advanced_printable_search"
|
||||
@@ -1819,6 +1861,9 @@ class VIEW3D_PT_blenderkit_unified(Panel):
|
||||
if ui_props.asset_type == "NODEGROUP":
|
||||
return draw_panel_nodegroup_search(self, context)
|
||||
|
||||
if ui_props.asset_type == "ADDON":
|
||||
return draw_panel_addon_search(self, context)
|
||||
|
||||
def draw_upload(self, context, layout, ui_props):
|
||||
obj = utils.get_active_asset()
|
||||
props = getattr(obj, "blenderkit", None)
|
||||
@@ -1861,6 +1906,15 @@ class VIEW3D_PT_blenderkit_unified(Panel):
|
||||
if ui_props.asset_type == "NODEGROUP":
|
||||
return draw_panel_nodegroup_upload(self, context)
|
||||
|
||||
if ui_props.asset_type == "ADDON":
|
||||
layout.label(text="Add-on uploads are managed through")
|
||||
layout.label(text="the BlenderKit website.")
|
||||
op = layout.operator(
|
||||
"wm.url_open", text="Go to BlenderKit Website", icon="URL"
|
||||
)
|
||||
op.url = paths.BLENDERKIT_ADDON_UPLOAD_INSTRUCTIONS_URL
|
||||
return
|
||||
|
||||
|
||||
class BlenderKitWelcomeOperator(bpy.types.Operator):
|
||||
"""Login online on BlenderKit webpage"""
|
||||
@@ -2125,10 +2179,11 @@ def draw_asset_context_menu(
|
||||
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
|
||||
if o is not None:
|
||||
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
|
||||
@@ -2438,8 +2493,8 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
icon=icon,
|
||||
emboss=emboss,
|
||||
)
|
||||
# additional questionmark icon where it's important?
|
||||
# Embossed elements are visibly clickable, so we don't need the questionmark icon
|
||||
# additional 'question mark' icon where it's important?
|
||||
# Embossed elements are visibly clickable, so we don't need the 'question mark' icon
|
||||
if url != "" and not emboss:
|
||||
split = split.split()
|
||||
op = split.operator("wm.blenderkit_url", text="", icon="QUESTION")
|
||||
@@ -2512,15 +2567,15 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
box.separator()
|
||||
|
||||
if self.asset_data.get("license") == "cc_zero":
|
||||
t = "CC Zero "
|
||||
text = "CC Zero "
|
||||
icon = pcoll["cc0"]
|
||||
else:
|
||||
t = "Royalty free"
|
||||
text = "Royalty free"
|
||||
icon = pcoll["royalty_free"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"License",
|
||||
t,
|
||||
text,
|
||||
# icon_value=icon.icon_id,
|
||||
url=f"{global_vars.SERVER}/docs/licenses/",
|
||||
tooltip="All BlenderKit assets are available for commercial use. \n"
|
||||
@@ -2614,8 +2669,8 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
self.draw_asset_parameter(box, key="modelStyle", pretext="Style")
|
||||
|
||||
if utils.get_param(self.asset_data, "dimensionX"):
|
||||
t = utils.fmt_dimensions(mparams)
|
||||
self.draw_property(box, "Size", t)
|
||||
text = utils.fmt_dimensions(mparams)
|
||||
self.draw_property(box, "Size", text)
|
||||
if self.asset_data.get("filesSize"):
|
||||
fs = self.asset_data["filesSize"] * 1024
|
||||
# multiply because the number is reduced when search is done to avoind C intiger limit with large files
|
||||
@@ -2655,38 +2710,147 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
|
||||
# self.draw_property(box, 'Tags', self.asset_data['tags']) #TODO make them clickable!
|
||||
|
||||
# Free/Full plan or private Access
|
||||
# Free/Full plan or private Access - with special handling for addons
|
||||
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"
|
||||
)
|
||||
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=paths.BLENDERKIT_PLANS_URL,
|
||||
)
|
||||
|
||||
# Special pricing display for addons
|
||||
if self.asset_data.get("assetType") == "addon":
|
||||
|
||||
can_download = self.asset_data.get("canDownload")
|
||||
is_free = self.asset_data.get("isFree")
|
||||
|
||||
# Get pricing info from extensions cache
|
||||
base_price = self.asset_data.get("basePrice")
|
||||
is_for_sale = self.asset_data.get("isForSale")
|
||||
|
||||
if self.asset_data["isPrivate"]:
|
||||
text = "Private"
|
||||
self.draw_property(box, "Access", text, icon="LOCKED")
|
||||
elif is_for_sale and not can_download and base_price:
|
||||
text = f"${base_price} (Not purchased)"
|
||||
icon = pcoll["full"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Price",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip="This addon is for sale but you haven't purchased it yet",
|
||||
)
|
||||
elif is_for_sale and can_download and base_price:
|
||||
text = f"${base_price} (Purchased)"
|
||||
icon = pcoll["full"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Price",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip="You have purchased this addon",
|
||||
)
|
||||
elif not is_free and not is_for_sale:
|
||||
text = "Full plan required"
|
||||
icon = pcoll["full"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Access",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip=plans_tooltip,
|
||||
url=paths.BLENDERKIT_PLANS_URL,
|
||||
)
|
||||
else:
|
||||
text = "Free"
|
||||
icon = pcoll["free"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Access",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip="This addon is free to use",
|
||||
)
|
||||
|
||||
# Display Blender version requirements for addons
|
||||
dict_params = self.asset_data.get("dictParameters", {})
|
||||
min_version = dict_params.get("blenderVersionMin")
|
||||
max_version = dict_params.get("blenderVersionMax")
|
||||
if min_version:
|
||||
min_version_tuple = tuple(map(int, min_version.split(".")))
|
||||
if max_version:
|
||||
max_version_tuple = tuple(map(int, max_version.split(".")))
|
||||
|
||||
if min_version or max_version:
|
||||
version_text = ""
|
||||
if min_version and max_version:
|
||||
version_text = f"{min_version} - {max_version}"
|
||||
elif min_version:
|
||||
version_text = f"{min_version}+"
|
||||
elif max_version:
|
||||
version_text = f"≤ {max_version}"
|
||||
|
||||
# Check if current Blender version is compatible
|
||||
current_version = (
|
||||
f"{bpy.app.version[0]}.{bpy.app.version[1]}.{bpy.app.version[2]}"
|
||||
)
|
||||
is_compatible = True
|
||||
|
||||
if min_version:
|
||||
if bpy.app.version < min_version_tuple:
|
||||
is_compatible = False
|
||||
|
||||
if max_version and is_compatible:
|
||||
if bpy.app.version > max_version_tuple:
|
||||
is_compatible = False
|
||||
|
||||
# Display version requirement with appropriate warning
|
||||
if not is_compatible:
|
||||
box.alert = True
|
||||
self.draw_property(
|
||||
box,
|
||||
"Blender versions",
|
||||
f"{version_text} (Incompatible!)",
|
||||
icon="ERROR",
|
||||
tooltip=f"This addon requires Blender {version_text}, but you're using {current_version}",
|
||||
)
|
||||
box.alert = False
|
||||
else:
|
||||
self.draw_property(
|
||||
box,
|
||||
"Blender versions",
|
||||
version_text,
|
||||
icon="CHECKMARK",
|
||||
tooltip=f"This addon is compatible with your Blender version ({current_version})",
|
||||
)
|
||||
else:
|
||||
t = "Full plan"
|
||||
icon = pcoll["full"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Access",
|
||||
t,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip=plans_tooltip,
|
||||
url=paths.BLENDERKIT_PLANS_URL,
|
||||
)
|
||||
# Regular asset access display
|
||||
if self.asset_data["isPrivate"]:
|
||||
text = "Private"
|
||||
self.draw_property(box, "Access", text, icon="LOCKED")
|
||||
elif self.asset_data["isFree"]:
|
||||
text = "Free plan"
|
||||
icon = pcoll["free"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Access",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip=plans_tooltip,
|
||||
url=paths.BLENDERKIT_PLANS_URL,
|
||||
)
|
||||
else:
|
||||
text = "Full plan"
|
||||
icon = pcoll["full"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Access",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip=plans_tooltip,
|
||||
url=paths.BLENDERKIT_PLANS_URL,
|
||||
)
|
||||
|
||||
if utils.profile_is_validator():
|
||||
date = self.asset_data["created"][:10]
|
||||
@@ -2730,7 +2894,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
)
|
||||
|
||||
# Add TwinBru specific parameters for material assets
|
||||
# only if they have twinbruReference in the dictparameters
|
||||
# only if they have 'twinbruReference' in the 'dictParameters'
|
||||
if self.asset_data.get("dictParameters").get("twinbruReference"):
|
||||
box.separator()
|
||||
box.label(text="TwinBru physical material categories")
|
||||
@@ -3220,10 +3384,15 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
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))
|
||||
|
||||
if not utils.user_is_owner(asset_data=self.asset_data):
|
||||
if (
|
||||
not utils.user_is_owner(asset_data=self.asset_data)
|
||||
and self.asset_data.get("assetType") != "addon"
|
||||
):
|
||||
# Draw ratings, but not for owners of assets - doesn't make sense.
|
||||
# also addons are now disabled until we figure out how to handle them.
|
||||
ratings_box = left_column.box()
|
||||
self.prefill_ratings()
|
||||
ratings.draw_ratings_menu(self, context, ratings_box)
|
||||
@@ -3412,35 +3581,18 @@ class ClearSearchKeywords(bpy.types.Operator):
|
||||
|
||||
|
||||
class ClosePopupButton(bpy.types.Operator):
|
||||
"""Close popup window"""
|
||||
"""Close the popup window. It can also be closed by pressing Esc or clicking outside it."""
|
||||
|
||||
bl_idname = "view3d.close_popup_button"
|
||||
bl_label = "Close popup"
|
||||
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)
|
||||
"""Force the (containing, parent) popup to close.
|
||||
This was done by emulating Esc or hacking mouse, but stopped working in B5.
|
||||
But can be effectively done by just tweaking screen: https://blender.stackexchange.com/a/329900
|
||||
"""
|
||||
bpy.context.window.screen = bpy.context.window.screen
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -3492,9 +3644,13 @@ class UrlPopupDialog(bpy.types.Operator):
|
||||
layout.active_default = True
|
||||
op = layout.operator("wm.url_open", text=self.link_text, icon="QUESTION")
|
||||
if not utils.user_logged_in():
|
||||
if self.message.find("purchased") != -1:
|
||||
text = "purchased"
|
||||
else:
|
||||
text = "subscribed"
|
||||
utils.label_multiline(
|
||||
layout,
|
||||
text="Already subscribed? Log in to access your account.",
|
||||
text=f"Already {text}? Log in to access your account.",
|
||||
width=300,
|
||||
)
|
||||
|
||||
@@ -3695,6 +3851,7 @@ def header_search_draw(self, context):
|
||||
"HDR": wm.blenderkit_HDR,
|
||||
"SCENE": wm.blenderkit_scene,
|
||||
"NODEGROUP": wm.blenderkit_nodegroup,
|
||||
"ADDON": wm.blenderkit_addon,
|
||||
}
|
||||
props = props_dict[ui_props.asset_type]
|
||||
pcoll = icons.icon_collections["main"]
|
||||
@@ -3708,6 +3865,7 @@ def header_search_draw(self, context):
|
||||
"HDR": "WORLD",
|
||||
"SCENE": "SCENE_DATA",
|
||||
"NODEGROUP": "NODETREE",
|
||||
"ADDON": "PLUGIN",
|
||||
}
|
||||
|
||||
asset_type_icon = icons_dict[ui_props.asset_type]
|
||||
@@ -3845,6 +4003,12 @@ def header_search_draw(self, context):
|
||||
text="",
|
||||
icon_value=icon_id,
|
||||
)
|
||||
elif ui_props.asset_type == "ADDON":
|
||||
layout.popover(
|
||||
panel="VIEW3D_PT_blenderkit_advanced_addon_search",
|
||||
text="",
|
||||
icon_value=icon_id,
|
||||
)
|
||||
elif ui_props.asset_type == "PRINTABLE":
|
||||
layout.popover(
|
||||
panel="VIEW3D_PT_blenderkit_advanced_printable_search",
|
||||
@@ -4039,7 +4203,7 @@ class NodegroupDropDialog(bpy.types.Operator):
|
||||
# When adding as a node, use node positioning; when adding as modifier, use 3D positioning
|
||||
if self.add_mode == "NODE":
|
||||
bpy.ops.scene.blenderkit_download(
|
||||
True,
|
||||
"EXEC_DEFAULT",
|
||||
asset_index=self.asset_search_index,
|
||||
node_x=self.node_x,
|
||||
node_y=self.node_y,
|
||||
@@ -4050,7 +4214,7 @@ class NodegroupDropDialog(bpy.types.Operator):
|
||||
)
|
||||
else: # MODIFIER mode
|
||||
bpy.ops.scene.blenderkit_download(
|
||||
True,
|
||||
"EXEC_DEFAULT",
|
||||
asset_index=self.asset_search_index,
|
||||
model_location=self.snapped_location,
|
||||
model_rotation=self.snapped_rotation,
|
||||
@@ -4080,6 +4244,7 @@ classes = (
|
||||
VIEW3D_PT_blenderkit_advanced_HDR_search,
|
||||
VIEW3D_PT_blenderkit_advanced_brush_search,
|
||||
VIEW3D_PT_blenderkit_advanced_nodegroup_search,
|
||||
VIEW3D_PT_blenderkit_advanced_addon_search,
|
||||
VIEW3D_PT_blenderkit_advanced_printable_search,
|
||||
VIEW3D_PT_blenderkit_categories,
|
||||
VIEW3D_PT_blenderkit_import_settings,
|
||||
|
||||
+40
-16
@@ -304,6 +304,11 @@ def get_search_props():
|
||||
if not hasattr(wm, "blenderkit_nodegroup"):
|
||||
return
|
||||
props = wm.blenderkit_nodegroup
|
||||
|
||||
if uiprops.asset_type == "ADDON":
|
||||
if not hasattr(wm, "blenderkit_addon"):
|
||||
return
|
||||
props = wm.blenderkit_addon
|
||||
return props
|
||||
|
||||
|
||||
@@ -357,6 +362,8 @@ def get_active_asset():
|
||||
return get_active_brush()
|
||||
elif ui_props.asset_type == "NODEGROUP":
|
||||
return get_active_nodegroup()
|
||||
elif ui_props.asset_type == "ADDON":
|
||||
return None # Addons don't have an active asset concept
|
||||
|
||||
return None
|
||||
|
||||
@@ -394,6 +401,8 @@ def get_upload_props():
|
||||
b = get_active_nodegroup()
|
||||
if b is not None:
|
||||
return b.blenderkit
|
||||
elif ui_props.asset_type == "ADDON":
|
||||
return None # Addons don't have upload props
|
||||
return None
|
||||
|
||||
|
||||
@@ -647,10 +656,11 @@ def img_to_preview(img, copy_original=False):
|
||||
def get_hidden_image(
|
||||
tpath, bdata_name, force_reload: bool = False, colorspace: str = ""
|
||||
):
|
||||
"""Get hidden image by name. If not found, load it from tpath."""
|
||||
if bdata_name[0] == ".":
|
||||
hidden_name = bdata_name
|
||||
else:
|
||||
hidden_name = ".%s" % bdata_name
|
||||
hidden_name = f".{bdata_name}"
|
||||
img = bpy.data.images.get(hidden_name) # type: ignore[union-attr]
|
||||
|
||||
if tpath.startswith("//"):
|
||||
@@ -687,14 +697,14 @@ def get_hidden_image(
|
||||
|
||||
|
||||
def get_thumbnail(name):
|
||||
"""Get addon thumbnail image by name."""
|
||||
p = paths.get_addon_thumbnail_path(name)
|
||||
name = ".%s" % name
|
||||
name = f".{name}"
|
||||
img = bpy.data.images.get(name)
|
||||
if img == None:
|
||||
if img is None:
|
||||
img = bpy.data.images.load(p, check_existing=True)
|
||||
image_utils.set_colorspace(img)
|
||||
img.name = name
|
||||
img.name = name
|
||||
|
||||
return img
|
||||
|
||||
@@ -1224,6 +1234,9 @@ def user_is_owner(asset_data: Optional[dict] = None) -> bool:
|
||||
|
||||
def asset_from_newer_blender_version(asset_data, blender_version=None):
|
||||
"""Check if asset is from a newer blender version, to avoid incompatibility. Give info if difference is in major, minor or patch version."""
|
||||
# addons don't have a blender version, so we return False
|
||||
if asset_data["assetType"] == "addon":
|
||||
return False, ""
|
||||
asset_ver = asset_data["sourceAppVersion"].split(".")
|
||||
if blender_version is None:
|
||||
blender_version = bpy.app.version
|
||||
@@ -1266,27 +1279,38 @@ def guard_from_crash():
|
||||
|
||||
|
||||
def get_largest_area(context=None, area_type="VIEW_3D"):
|
||||
"""Get the largest area of the given type."""
|
||||
maxsurf = 0
|
||||
maxa = None
|
||||
maxw = None
|
||||
region = None
|
||||
if context is None:
|
||||
windows = bpy.data.window_managers[0].windows
|
||||
if bpy.context.window is not None:
|
||||
windows = [bpy.context.window]
|
||||
else:
|
||||
windows = bpy.data.window_managers.windows
|
||||
else:
|
||||
windows = context.window_manager.windows
|
||||
for w in 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
|
||||
if bpy.context.area is not None and bpy.context.area.type == area_type:
|
||||
maxa = bpy.context.area
|
||||
maxw = bpy.context.window
|
||||
maxsurf = maxa.width * maxa.height
|
||||
region = maxa.regions[-1]
|
||||
else:
|
||||
areas = w.screen.areas
|
||||
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
|
||||
|
||||
region = a.regions[-1]
|
||||
# for r in a.regions:
|
||||
# if r.type == 'WINDOW':
|
||||
# region = r
|
||||
region = a.regions[-1]
|
||||
# for r in a.regions:
|
||||
# if r.type == 'WINDOW':
|
||||
# region = r
|
||||
|
||||
if maxw is None or maxa is None:
|
||||
return None, None, None
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"last_check": "2025-12-22 09:58:29.267931",
|
||||
"backup_date": "December-1-2025",
|
||||
"last_check": "2026-02-12 10:20:22.908483",
|
||||
"backup_date": "January-12-2026",
|
||||
"update_ready": false,
|
||||
"ignore": false,
|
||||
"just_restored": false,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import blf
|
||||
import bpy
|
||||
import gpu
|
||||
|
||||
from typing import Tuple, Union
|
||||
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
from .bl_ui_widget import BL_UI_Widget
|
||||
|
||||
@@ -17,6 +22,9 @@ class BL_UI_Label(BL_UI_Widget):
|
||||
self.multiline = False
|
||||
self.row_height = 20
|
||||
|
||||
self.padding: Union[Tuple[float, float], float] = 0
|
||||
self.background = False
|
||||
|
||||
@property
|
||||
def text_color(self):
|
||||
return self._text_color
|
||||
@@ -61,6 +69,30 @@ class BL_UI_Label(BL_UI_Widget):
|
||||
blf.size(font_id, self._text_size, 72)
|
||||
else:
|
||||
blf.size(font_id, self._text_size)
|
||||
lines = self._text.split("\n") if self.multiline else [self._text]
|
||||
if not lines:
|
||||
return
|
||||
|
||||
default_line_height = self.row_height if self.multiline else self._text_size
|
||||
line_metrics = []
|
||||
max_line_width = 0.0
|
||||
total_height = 0.0
|
||||
|
||||
for line in lines:
|
||||
width, height = blf.dimensions(font_id, line)
|
||||
if height == 0:
|
||||
height = default_line_height
|
||||
line_height = (
|
||||
self.row_height if self.multiline else max(height, self._text_size)
|
||||
)
|
||||
if line_height == 0:
|
||||
line_height = default_line_height
|
||||
line_metrics.append((line, width, line_height))
|
||||
max_line_width = max(max_line_width, width)
|
||||
total_height += line_height
|
||||
|
||||
if not line_metrics:
|
||||
return
|
||||
|
||||
textpos_y = area_height - self.y_screen - self.height
|
||||
|
||||
@@ -76,16 +108,55 @@ class BL_UI_Label(BL_UI_Widget):
|
||||
if self._valign == "CENTER":
|
||||
y -= height // 2
|
||||
# bottom could be here but there's no reason for it
|
||||
|
||||
first_line_height = line_metrics[0][2]
|
||||
|
||||
if self.background and (max_line_width > 0 or total_height > 0):
|
||||
pad_x, pad_y = self._padding_tuple()
|
||||
text_top = y + first_line_height
|
||||
text_bottom = text_top - total_height
|
||||
left = x - pad_x
|
||||
right = x + max_line_width + pad_x
|
||||
top = text_top + pad_y
|
||||
bottom = text_bottom - pad_y
|
||||
self._draw_background_rect(left, right, bottom, top)
|
||||
|
||||
current_y = y
|
||||
if not self.multiline:
|
||||
blf.position(font_id, x, y, 0)
|
||||
|
||||
blf.position(font_id, x, current_y, 0)
|
||||
blf.color(font_id, r, g, b, a)
|
||||
|
||||
blf.draw(font_id, self._text)
|
||||
else:
|
||||
lines = self._text.split("\n")
|
||||
for line in lines:
|
||||
blf.position(font_id, x, y, 0)
|
||||
for line, _, line_height in line_metrics:
|
||||
blf.position(font_id, x, current_y, 0)
|
||||
blf.color(font_id, r, g, b, a)
|
||||
blf.draw(font_id, line)
|
||||
y -= self.row_height
|
||||
current_y -= line_height
|
||||
|
||||
def _padding_tuple(self) -> Tuple[float, float]:
|
||||
pad = self.padding
|
||||
if isinstance(pad, (list, tuple)):
|
||||
if len(pad) == 0:
|
||||
return (0.0, 0.0)
|
||||
if len(pad) == 1:
|
||||
value = float(pad[0])
|
||||
return (value, value)
|
||||
return (float(pad[0]), float(pad[1]))
|
||||
value = float(pad)
|
||||
return (value, value)
|
||||
|
||||
def _draw_background_rect(self, left, right, bottom, top):
|
||||
vertices = (
|
||||
(left, top),
|
||||
(left, bottom),
|
||||
(right, bottom),
|
||||
(right, top),
|
||||
)
|
||||
indices = ((0, 1, 2), (0, 2, 3))
|
||||
gpu.state.blend_set("ALPHA")
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", self._bg_color)
|
||||
batch = batch_for_shader(
|
||||
self.shader, "TRIS", {"pos": vertices}, indices=indices
|
||||
)
|
||||
batch.draw(self.shader)
|
||||
|
||||
@@ -2,7 +2,7 @@ schema_version = "1.0.0"
|
||||
|
||||
id = "blenderkit"
|
||||
type = "add-on"
|
||||
version = "3.18.0-251121" # X.Y.Z-YYYYMMDD, must have a dash instead of a dot
|
||||
version = "3.18.1-251219" # X.Y.Z-YYYYMMDD, must have a dash instead of a dot
|
||||
|
||||
name = "BlenderKit Online Asset Library"
|
||||
tagline = "Drag & drop of assets from the community driven library"
|
||||
|
||||
@@ -91,7 +91,7 @@ def ensure_minimal_data(data: Optional[dict] = None) -> dict:
|
||||
return data
|
||||
|
||||
|
||||
def ensure_minimal_data_class(data_class):
|
||||
def ensure_minimal_data_class(data_class: datas.SearchData) -> datas.SearchData:
|
||||
"""Ensure that the data send to the BlenderKit-Client contains:
|
||||
- app_id is the process ID of the Blender instance, so BlenderKit-client can return reports to the correct instance.
|
||||
- api_key is the authentication token for the BlenderKit server, so BlenderKit-Client can authenticate the user.
|
||||
|
||||
@@ -19,16 +19,35 @@
|
||||
Module colors defines color palette for BlenderKit UI.
|
||||
"""
|
||||
|
||||
# UI Colors
|
||||
|
||||
TOP_BAR_BLUE = (0.2, 0.25, 0.4, 1.0)
|
||||
"""TOP_BAR_BLUE Color for BlenderKit UI top bar."""
|
||||
|
||||
WHITE = (1, 1, 1, 0.9)
|
||||
|
||||
TEXT = (0.9, 0.9, 0.9, 0.6)
|
||||
GREEN = (0.9, 1, 0.9, 0.6)
|
||||
RED = (1, 0.5, 0.5, 0.8)
|
||||
BLUE = (0.8, 0.8, 1, 0.8)
|
||||
TEXT = (0.9, 0.9, 0.9, 0.9)
|
||||
"""TEXT Color for BlenderKit UI text."""
|
||||
|
||||
PURPLE = (0.8, 0.4, 1.0, 1.0) # Full Plan purple
|
||||
GREEN_FREE = (0.4, 0.8, 0.4, 1.0) # Green for free addons
|
||||
"""Color for validator reports."""
|
||||
TEXT_DIM = (0.8, 0.8, 0.8, 0.9)
|
||||
|
||||
GREEN = (0.9, 1, 0.9, 0.6)
|
||||
"""GREEN Color for validator reports."""
|
||||
|
||||
RED = (1, 0.5, 0.5, 0.8)
|
||||
"""RED Color for validator reports."""
|
||||
|
||||
BLUE = (0.8, 0.8, 1, 0.8)
|
||||
"""BLUE Color for validator reports."""
|
||||
|
||||
GREEN_PRICE = (0.42, 0.49, 0.19, 1.0)
|
||||
"""Emerald Green to be used on "discounted" add-ons."""
|
||||
|
||||
PURPLE_PRICE = (0.59, 0.05, 0.82, 1.0)
|
||||
"""Lavender Purple to be used on "for sale" add-ons."""
|
||||
|
||||
ORANGE_FULL = (0.702, 0.349, 0.208, 1.0)
|
||||
"""Burnt Orange associated with full plan assets and add-ons."""
|
||||
|
||||
GRAY = (0.7, 0.7, 0.7, 0.6)
|
||||
"""Default color for debug reports."""
|
||||
|
||||
@@ -109,8 +109,10 @@ def get_addon_installation_status(asset_data):
|
||||
if not is_enabled:
|
||||
extension_module_name = f"bl_ext.www_blenderkit_com.{extension_id}"
|
||||
is_enabled = extension_module_name in enabled_addons
|
||||
bk_logger.info(
|
||||
f"Checking extension format: {extension_module_name} -> enabled: {is_enabled}"
|
||||
bk_logger.debug(
|
||||
"Checking extension format: %s -> enabled: %s",
|
||||
extension_module_name,
|
||||
is_enabled,
|
||||
)
|
||||
|
||||
# Also try other possible repository name formats
|
||||
@@ -210,10 +212,15 @@ def get_addon_installation_status(asset_data):
|
||||
if "blenderkit" in addon.lower() or addon.endswith(extension_id)
|
||||
]
|
||||
if blenderkit_addons:
|
||||
bk_logger.info(f"Found BlenderKit-related enabled addons: {blenderkit_addons}")
|
||||
bk_logger.debug(
|
||||
"Found BlenderKit-related enabled addons: %s", blenderkit_addons
|
||||
)
|
||||
|
||||
bk_logger.info(
|
||||
f"Addon status check for '{extension_id}': installed={is_installed}, enabled={is_enabled}"
|
||||
bk_logger.debug(
|
||||
"Addon status check for '%s': installed=%s, enabled=%s",
|
||||
extension_id,
|
||||
is_installed,
|
||||
is_enabled,
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -877,8 +884,13 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
|
||||
if asset_blender_version < (4, 3, 0) and bpy.app.version >= (4, 3, 0):
|
||||
brush.asset_clear()
|
||||
brush.asset_mark()
|
||||
brush.icon_filepath = asset_thumb_path
|
||||
|
||||
if bpy.app.version <= (4, 5, 0):
|
||||
brush.icon_filepath = asset_thumb_path
|
||||
else:
|
||||
# load asset thumbnail into brush if it's not already present
|
||||
if brush.preview is None:
|
||||
with bpy.context.temp_override(id=brush):
|
||||
bpy.ops.ed.lib_id_load_custom_preview(filepath=asset_thumb_path)
|
||||
# set the brush active
|
||||
if bpy.context.view_layer.objects.active.mode == "SCULPT":
|
||||
if bpy.app.version < (4, 3, 0):
|
||||
@@ -897,7 +909,6 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
|
||||
relative_asset_identifier=f"Brush{os.sep}{brush.name}"
|
||||
)
|
||||
# TODO add grease pencil brushes!
|
||||
|
||||
# bpy.context.tool_settings.image_paint.brush = brush
|
||||
asset_main = brush
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
from logging import INFO, WARN
|
||||
from logging import DEBUG, INFO, WARN
|
||||
from os import environ
|
||||
from subprocess import Popen
|
||||
from typing import Any, Optional
|
||||
@@ -59,6 +59,11 @@ BKIT_AUTHORS: dict[int, datas.UserProfile] = {}
|
||||
"""All loaded profiles of other users. Current user is also present in stripped down version. Key is the UserProfile.id."""
|
||||
|
||||
LOGGING_LEVEL_BLENDERKIT = INFO
|
||||
|
||||
# read special DEBUG env var to set logging level to DEBUG
|
||||
if environ.get("BLENDERKIT_DEBUG", "0") == "1":
|
||||
LOGGING_LEVEL_BLENDERKIT = DEBUG
|
||||
|
||||
LOGGING_LEVEL_IMPORTED = WARN
|
||||
PREFS = {}
|
||||
|
||||
|
||||
@@ -27,8 +27,10 @@ import bpy
|
||||
icon_collections = {}
|
||||
|
||||
icons_read = {
|
||||
"fp.png": "free",
|
||||
"flp.png": "full",
|
||||
"free_plan.png": "free",
|
||||
"full_plan.png": "full",
|
||||
"promo_sale_symbol.png": "promo_sale_symbol",
|
||||
"sale_purple.png": "for_sale",
|
||||
"trophy.png": "trophy",
|
||||
"dumbbell.png": "dumbbell",
|
||||
"cc0.png": "cc0",
|
||||
|
||||
@@ -24,6 +24,7 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from functools import lru_cache
|
||||
|
||||
import bpy
|
||||
|
||||
@@ -39,6 +40,9 @@ BLENDERKIT_REPORT_URL = f"{global_vars.SERVER}/usage_report"
|
||||
BLENDERKIT_USER_ASSETS_URL = f"{global_vars.SERVER}/my-assets"
|
||||
BLENDERKIT_MANUAL_URL = "https://youtu.be/0P8ZjfbUjeA"
|
||||
BLENDERKIT_MODEL_UPLOAD_INSTRUCTIONS_URL = f"{global_vars.SERVER}/docs/upload/"
|
||||
BLENDERKIT_PRINTABLE_UPLOAD_INSTRUCTIONS_URL = (
|
||||
f"{global_vars.SERVER}/docs/upload-printables/"
|
||||
)
|
||||
BLENDERKIT_MATERIAL_UPLOAD_INSTRUCTIONS_URL = (
|
||||
f"{global_vars.SERVER}/docs/uploading-material/"
|
||||
)
|
||||
@@ -463,6 +467,8 @@ def get_addon_file(subpath=""):
|
||||
script_path = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
# cache this for minor performance boost
|
||||
@lru_cache(maxsize=128)
|
||||
def get_addon_thumbnail_path(name):
|
||||
global script_path
|
||||
# fpath = os.path.join(p, subpath)
|
||||
@@ -474,6 +480,13 @@ def get_addon_thumbnail_path(name):
|
||||
return os.path.join(script_path, subpath)
|
||||
|
||||
|
||||
# cache this for minor performance boost
|
||||
@lru_cache(maxsize=128)
|
||||
def icon_path_exists(path: str) -> bool:
|
||||
"""Cached version of os.path.exists"""
|
||||
return os.path.exists(path)
|
||||
|
||||
|
||||
def get_config_dir_path() -> str:
|
||||
"""Get the path to the config directory in global_dir."""
|
||||
global_dir = bpy.context.preferences.addons[__package__].preferences.global_dir # type: ignore
|
||||
|
||||
@@ -203,7 +203,7 @@ class SetBookmark(bpy.types.Operator):
|
||||
"""Add or remove bookmarking of the asset.\nShortcut: hover over asset in the asset bar and press 'B'."""
|
||||
|
||||
bl_idname = "wm.blenderkit_bookmark_asset"
|
||||
bl_label = "BlenderKit bookmark assest"
|
||||
bl_label = "BlenderKit bookmark assets"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
asset_id: StringProperty( # type: ignore[valid-type]
|
||||
@@ -233,28 +233,29 @@ class SetBookmark(bpy.types.Operator):
|
||||
ratings_utils.store_rating_local(
|
||||
self.asset_id, rating_type="bookmarks", value=bookmark_value
|
||||
)
|
||||
client_lib.send_rating(self.asset_id, "bookmarks", bookmark_value)
|
||||
client_lib.send_rating(self.asset_id, "bookmarks", str(bookmark_value))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def rating_menu_draw(self, context):
|
||||
layout = self.layout
|
||||
## NOT USED ANYMORE
|
||||
# def rating_menu_draw(self, context):
|
||||
# layout = self.layout
|
||||
|
||||
ui_props = context.window_manager.blenderkitUI
|
||||
sr = search.get_search_results()
|
||||
# ui_props = context.window_manager.blenderkitUI
|
||||
# sr = search.get_search_results()
|
||||
|
||||
asset_search_index = ui_props.active_index
|
||||
if asset_search_index > -1:
|
||||
asset_data = dict(sr["results"][asset_search_index])
|
||||
# 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"
|
||||
# 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"]
|
||||
# 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"]
|
||||
|
||||
|
||||
# Coordinates (each one is a triangle).
|
||||
|
||||
@@ -20,6 +20,7 @@ import copy
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from functools import lru_cache
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
@@ -43,6 +44,7 @@ from . import (
|
||||
image_utils,
|
||||
paths,
|
||||
reports,
|
||||
search_price,
|
||||
resolutions,
|
||||
tasks_queue,
|
||||
utils,
|
||||
@@ -53,6 +55,51 @@ bk_logger = logging.getLogger(__name__)
|
||||
search_tasks = {}
|
||||
|
||||
|
||||
def _inject_user_price_data(assets: list[dict]) -> None:
|
||||
"""Augment search results with per-user pricing info when available."""
|
||||
if not assets:
|
||||
bk_logger.debug("User price lookup skipped: empty assets list.")
|
||||
return
|
||||
|
||||
version_uuids: list[str] = [ass["id"] for ass in assets]
|
||||
if not version_uuids:
|
||||
bk_logger.debug("User price lookup skipped: empty version UUIDs list.")
|
||||
return
|
||||
|
||||
try:
|
||||
price_response = search_price.query_user_price(
|
||||
version_uuids=version_uuids,
|
||||
page_size=len(version_uuids),
|
||||
)
|
||||
except Exception as exc:
|
||||
bk_logger.warning("Failed to fetch user prices: %s", exc)
|
||||
return
|
||||
|
||||
if not price_response:
|
||||
bk_logger.debug(
|
||||
"User price lookup skipped: %s",
|
||||
price_response,
|
||||
)
|
||||
return
|
||||
|
||||
price_by_uuid: dict[str, dict] = {}
|
||||
for entry in price_response:
|
||||
version_uuid = entry.get("versionUuid") # maybe assetUuid ?
|
||||
if not version_uuid:
|
||||
continue
|
||||
price_by_uuid[version_uuid] = entry
|
||||
|
||||
if not price_by_uuid:
|
||||
return
|
||||
|
||||
for asset in assets:
|
||||
version_uuid = asset["id"]
|
||||
price_info = price_by_uuid.get(version_uuid)
|
||||
if not price_info:
|
||||
continue
|
||||
asset["userPrice"] = price_info["discountedPrice"]
|
||||
|
||||
|
||||
def update_ad(ad):
|
||||
if not ad.get("assetBaseId"):
|
||||
try:
|
||||
@@ -136,22 +183,23 @@ def check_clipboard():
|
||||
"""
|
||||
global last_clipboard
|
||||
try: # could be problematic on Linux
|
||||
current_clipboard = bpy.context.window_manager.clipboard
|
||||
current_clipboard = str(bpy.context.window_manager.clipboard)
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Failed to get clipboard: {e}")
|
||||
return
|
||||
|
||||
if current_clipboard == last_clipboard:
|
||||
return
|
||||
last_clipboard = current_clipboard
|
||||
|
||||
asset_type_index = last_clipboard.find("asset_type:")
|
||||
asset_type_index = current_clipboard.find("asset_type:")
|
||||
if asset_type_index == -1:
|
||||
return
|
||||
|
||||
if not last_clipboard.startswith("asset_base_id:"):
|
||||
if not current_clipboard.startswith("asset_base_id:"):
|
||||
return
|
||||
|
||||
last_clipboard = current_clipboard
|
||||
|
||||
asset_type_string = current_clipboard[asset_type_index:].lower()
|
||||
if asset_type_string.find("model") > -1:
|
||||
target_asset_type = "MODEL"
|
||||
@@ -169,6 +217,10 @@ def check_clipboard():
|
||||
target_asset_type = "NODEGROUP"
|
||||
elif asset_type_string.find("addon") > -1 or asset_type_string.find("add-on") > -1:
|
||||
target_asset_type = "ADDON"
|
||||
else:
|
||||
bk_logger.debug("Clipboard does not contain valid asset type.")
|
||||
return
|
||||
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if ui_props.asset_type != target_asset_type:
|
||||
ui_props.asset_type = target_asset_type # switch asset type before placing keywords, so it does not search under wrong asset type
|
||||
@@ -341,7 +393,7 @@ def handle_search_task(task: client_tasks.Task) -> bool:
|
||||
return True
|
||||
|
||||
# don't do anything while dragging - this could switch asset during drag, and make results list length different,
|
||||
# causing a lot of throuble.
|
||||
# causing a lot of trouble.
|
||||
if bpy.context.window_manager.blenderkitUI.dragging: # type: ignore[attr-defined]
|
||||
return False
|
||||
|
||||
@@ -403,6 +455,10 @@ def handle_search_task(task: client_tasks.Task) -> bool:
|
||||
asset for asset in result_field if asset.get("downloaded", 0) > 0
|
||||
]
|
||||
|
||||
# TODO: if ever needed, implement for other future types
|
||||
if result_field:
|
||||
_inject_user_price_data(result_field)
|
||||
|
||||
# Store results in history step
|
||||
history_step["search_results"] = result_field
|
||||
history_step["search_results_orig"] = task.result
|
||||
@@ -710,7 +766,7 @@ def query_to_url(
|
||||
scene_uuid: str = "",
|
||||
page_size: int = 15,
|
||||
) -> str:
|
||||
"""Build a new search request by parsing query dictionaty into appropriate URL.
|
||||
"""Build a new search request by parsing query dictionary into appropriate URL.
|
||||
Also modifies query and adds some stuff in there which is very misleading anti-pattern.
|
||||
TODO: just convert to URL here and move the sorting and adding of params to separate function.
|
||||
https://www.blenderkit.com/api/v1/search/
|
||||
@@ -1012,6 +1068,7 @@ def filter_addon_search_results(search_results, filter_installed_only=False):
|
||||
def add_search_process(
|
||||
query, get_next: bool, page_size: int, next_url: str, history_id: str
|
||||
):
|
||||
"""Initialize search task and add it to the task queue."""
|
||||
global search_tasks
|
||||
addon_version = utils.get_addon_version()
|
||||
blender_version = utils.get_blender_version()
|
||||
@@ -1232,7 +1289,7 @@ def search(get_next=False, query=None, author_id=""):
|
||||
|
||||
|
||||
def clean_filters():
|
||||
"""Cleanup filters in case search needs to be reset, typicaly when asset id is copy pasted."""
|
||||
"""Cleanup filters in case search needs to be reset, typically when asset id is copy pasted."""
|
||||
sprops = utils.get_search_props()
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
ui_props.property_unset("own_only")
|
||||
@@ -1551,6 +1608,13 @@ class SearchOperator(Operator):
|
||||
default="Runs search and displays the asset bar at the same time"
|
||||
)
|
||||
|
||||
force_clear: BoolProperty( # type: ignore[valid-type]
|
||||
name="Force clear keywords, before programmatic search",
|
||||
description="Force clear keywords before search",
|
||||
default=True,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
return properties.tooltip
|
||||
@@ -1564,16 +1628,25 @@ class SearchOperator(Operator):
|
||||
if self.esc:
|
||||
bpy.ops.view3d.close_popup_button("INVOKE_DEFAULT")
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
|
||||
search_keywords = str(ui_props.search_keywords)
|
||||
|
||||
if self.keywords != "":
|
||||
search_keywords = self.keywords
|
||||
|
||||
# remove all search keywords if force_clear is set
|
||||
if self.force_clear:
|
||||
# self.force_clear = False # reset the force clear
|
||||
search_keywords = ""
|
||||
|
||||
if self.author_id != "":
|
||||
bk_logger.info(f"Author ID: {self.author_id}")
|
||||
# if there is already an author id in the search keywords, remove it first, the author_id can be any so
|
||||
# use regex to find it
|
||||
ui_props.search_keywords = re.sub(
|
||||
r"\+author_id:\d+", "", ui_props.search_keywords
|
||||
)
|
||||
ui_props.search_keywords += f"+author_id:{self.author_id}"
|
||||
if self.keywords != "":
|
||||
ui_props.search_keywords = self.keywords
|
||||
search_keywords = re.sub(r"\+author_id:\d+", "", search_keywords)
|
||||
search_keywords += f"+author_id:{self.author_id}"
|
||||
|
||||
ui_props.search_keywords = search_keywords
|
||||
|
||||
search(get_next=self.get_next)
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
from typing import Iterable, List, Optional, Tuple
|
||||
|
||||
from . import client_lib, paths, utils
|
||||
|
||||
|
||||
def _normalize_version_uuid_list(values: Optional[Iterable[str]]) -> List[str]:
|
||||
if values is None:
|
||||
return []
|
||||
|
||||
normalized: List[str] = []
|
||||
for value in values:
|
||||
if not value:
|
||||
continue
|
||||
as_str = str(value)
|
||||
if as_str not in normalized:
|
||||
normalized.append(as_str)
|
||||
return normalized
|
||||
|
||||
|
||||
def query_user_price(
|
||||
version_uuids: list[str] = [],
|
||||
page_size: int = 15,
|
||||
timeout: Tuple[float, float] = (1, 30),
|
||||
) -> dict:
|
||||
"""Return results for price lookup of multiple asset versions.
|
||||
|
||||
The server endpoint now expects a POST body with `version_uuids`, so we keep
|
||||
the helper focused on returning the correct URL alongside the JSON payload
|
||||
that should be sent in the request.
|
||||
"""
|
||||
|
||||
if isinstance(version_uuids, str):
|
||||
version_uuids = [version_uuids]
|
||||
|
||||
version_uuid_list = _normalize_version_uuid_list(version_uuids)
|
||||
if page_size > 0:
|
||||
version_uuid_list = version_uuid_list[:page_size]
|
||||
|
||||
payload: dict = {"version_uuids": version_uuid_list}
|
||||
|
||||
url = f"{paths.BLENDERKIT_API}/cart/request-price-bulk/"
|
||||
|
||||
if not payload["version_uuids"]:
|
||||
raise ValueError("No version UUIDs provided for price lookup.")
|
||||
|
||||
headers = utils.get_simple_headers()
|
||||
headers.setdefault("Content-Type", "application/json")
|
||||
|
||||
response = client_lib.blocking_request(
|
||||
url,
|
||||
"POST",
|
||||
headers,
|
||||
json_data=payload,
|
||||
timeout=timeout,
|
||||
)
|
||||
search_results = response.json()
|
||||
return search_results
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -120,12 +120,14 @@ def handle_failed_reports(exception: Exception) -> float:
|
||||
|
||||
@bpy.app.handlers.persistent
|
||||
def client_communication_timer():
|
||||
"""Recieve all responses from Client and run according followup commands.
|
||||
"""Receive all responses from Client and run according followup commands.
|
||||
This function is the only one responsible for keeping the Client up and running.
|
||||
"""
|
||||
global pending_tasks
|
||||
bk_logger.debug("Getting tasks from Client")
|
||||
search.check_clipboard()
|
||||
bk_logger.log(5, "Getting tasks from Client")
|
||||
user_preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
if user_preferences.use_clipboard_scan:
|
||||
search.check_clipboard()
|
||||
results = list()
|
||||
try:
|
||||
results = client_lib.get_reports(os.getpid())
|
||||
@@ -141,7 +143,7 @@ def client_communication_timer():
|
||||
wm = bpy.context.window_manager
|
||||
wm.blenderkitUI.logo_status = "logo"
|
||||
|
||||
bk_logger.debug("Handling tasks")
|
||||
bk_logger.log(5, "Handling tasks")
|
||||
results_converted_tasks = []
|
||||
|
||||
# convert to task type
|
||||
@@ -166,8 +168,8 @@ def client_communication_timer():
|
||||
for task in results_converted_tasks:
|
||||
handle_task(task)
|
||||
|
||||
bk_logger.debug("Task handling finished")
|
||||
delay = bpy.context.preferences.addons[__package__].preferences.client_polling
|
||||
bk_logger.log(5, "Task handling finished")
|
||||
delay = user_preferences.client_polling
|
||||
if len(download.download_tasks) > 0:
|
||||
return min(0.2, delay)
|
||||
return delay
|
||||
|
||||
@@ -83,7 +83,7 @@ def draw_upload_common(layout, props, asset_type, context):
|
||||
url = "" # paths.BLENDERKIT_NODEGROUP_UPLOAD_INSTRUCTIONS_URL
|
||||
if asset_type == "PRINTABLE":
|
||||
url = (
|
||||
paths.BLENDERKIT_MODEL_UPLOAD_INSTRUCTIONS_URL
|
||||
paths.BLENDERKIT_PRINTABLE_UPLOAD_INSTRUCTIONS_URL
|
||||
) # Reuse model instructions since prints are similar
|
||||
if asset_type == "ADDON":
|
||||
asset_type_text = asset_type
|
||||
@@ -1721,6 +1721,10 @@ class VIEW3D_PT_blenderkit_import_settings(Panel):
|
||||
layout.prop(preferences, "resolution")
|
||||
# layout.prop(props, 'unpack_files')
|
||||
|
||||
# general settings
|
||||
# show toggle for clipboard scan
|
||||
layout.prop(preferences, "use_clipboard_scan")
|
||||
|
||||
|
||||
def deferred_set_name(props, expected_obj_name):
|
||||
"""Deferred timer to set empty name of uploaded asset to active Object's name.
|
||||
@@ -2725,15 +2729,26 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
is_free = self.asset_data.get("isFree")
|
||||
|
||||
# Get pricing info from extensions cache
|
||||
user_price = self.asset_data.get("userPrice")
|
||||
base_price = self.asset_data.get("basePrice")
|
||||
is_for_sale = self.asset_data.get("isForSale")
|
||||
|
||||
if self.asset_data["isPrivate"]:
|
||||
text = "Private"
|
||||
self.draw_property(box, "Access", text, icon="LOCKED")
|
||||
elif is_for_sale and not can_download and user_price and base_price:
|
||||
text = f"${user_price} (Not purchased)"
|
||||
icon = pcoll["for_sale"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Price",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip="This addon is for sale but you haven't purchased it yet.\nPrice shown is your price / base price",
|
||||
)
|
||||
elif is_for_sale and not can_download and base_price:
|
||||
text = f"${base_price} (Not purchased)"
|
||||
icon = pcoll["full"]
|
||||
icon = pcoll["for_sale"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Price",
|
||||
@@ -2742,8 +2757,8 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
tooltip="This addon is for sale but you haven't purchased it yet",
|
||||
)
|
||||
elif is_for_sale and can_download and base_price:
|
||||
text = f"${base_price} (Purchased)"
|
||||
icon = pcoll["full"]
|
||||
text = f"Purchased"
|
||||
icon = pcoll["for_sale"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Price",
|
||||
@@ -2752,7 +2767,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
tooltip="You have purchased this addon",
|
||||
)
|
||||
elif not is_free and not is_for_sale:
|
||||
text = "Full plan required"
|
||||
text = "Full plan"
|
||||
icon = pcoll["full"]
|
||||
self.draw_property(
|
||||
box,
|
||||
@@ -2991,7 +3006,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
)
|
||||
op.tooltip = "Search all assets by this author.\nShortcut: Hover over the asset in the asset bar and press 'A'." # type: ignore[attr-defined]
|
||||
op.esc = True # type: ignore[attr-defined]
|
||||
op.keywords = "" # type: ignore[attr-defined]
|
||||
op.keywords = "" # type: ignore[attr-defined] # must not be empty otherwise search will use previous keywords
|
||||
op.author_id = str(author_id) # type: ignore[attr-defined]
|
||||
|
||||
button_row = button_row.row(align=True)
|
||||
@@ -3222,7 +3237,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
# name_row.label(text='>')
|
||||
|
||||
name_row.label(text=aname)
|
||||
push_op_left(name_row, strength=3)
|
||||
push_op_left(name_row, strength=1)
|
||||
op = name_row.operator("view3d.close_popup_button", text="", icon="CANCEL")
|
||||
|
||||
def draw_comment_response(self, context, layout, comment_id):
|
||||
@@ -3568,6 +3583,18 @@ class SetCategoryOperatorLastInPopupCard(SetCategoryOperatorOrigin):
|
||||
bl_idname = "view3d.blenderkit_set_category_in_popup_card_last"
|
||||
|
||||
|
||||
class ToggleClipboardScan(bpy.types.Operator):
|
||||
"""Toggle whether asset links are set from clipboard when copied."""
|
||||
|
||||
bl_idname = "wm.blenderkit_toggle_clipboard_scan"
|
||||
bl_label = "Toggle Clipboard Scan"
|
||||
|
||||
def execute(self, context):
|
||||
user_preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
user_preferences.use_clipboard_scan = not user_preferences.use_clipboard_scan
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ClearSearchKeywords(bpy.types.Operator):
|
||||
"""Clear search keywords"""
|
||||
|
||||
|
||||
@@ -339,6 +339,25 @@ def get_active_asset_by_type(asset_type="model"):
|
||||
return None
|
||||
|
||||
|
||||
def get_equivalent_datablock(asset_type, name):
|
||||
"""Get the datablock that blocks us from renaming the asset, and rename it to something a bit else."""
|
||||
if asset_type == "MATERIAL":
|
||||
return bpy.data.materials.get(name)
|
||||
elif asset_type == "OBJECT":
|
||||
return bpy.data.objects.get(name)
|
||||
elif asset_type == "SCENE":
|
||||
return bpy.data.scenes.get(name)
|
||||
elif asset_type == "HDR":
|
||||
return bpy.data.images.get(name)
|
||||
elif asset_type == "BRUSH":
|
||||
return bpy.data.brushes.get(name)
|
||||
elif asset_type == "NODEGROUP":
|
||||
return bpy.data.node_groups.get(name)
|
||||
elif asset_type == "ADDON":
|
||||
return bpy.data.addons.get(name)
|
||||
return None
|
||||
|
||||
|
||||
def get_active_asset():
|
||||
scene = bpy.context.scene
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
@@ -970,12 +989,17 @@ def get_dimensions(obs):
|
||||
return dim, bbmin, bbmax
|
||||
|
||||
|
||||
def get_headers(api_key: str = "") -> dict[str, str]:
|
||||
def get_simple_headers() -> dict[str, str]:
|
||||
headers = {
|
||||
"accept": "application/json",
|
||||
"Platform-Version": platform.platform(),
|
||||
"addon-version": f"{global_vars.VERSION[0]}.{global_vars.VERSION[1]}.{global_vars.VERSION[2]}.{global_vars.VERSION[3]}",
|
||||
}
|
||||
return headers
|
||||
|
||||
|
||||
def get_headers(api_key: str = "") -> dict[str, str]:
|
||||
headers = get_simple_headers()
|
||||
if api_key != "":
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
@@ -1129,6 +1153,18 @@ def name_update(props, context=None):
|
||||
asset = get_active_asset()
|
||||
if asset.name != fname: # Here we actually rename assets datablocks
|
||||
asset.name = fname # change name of active object to upload Name
|
||||
# we need to set the name back for proper appending later
|
||||
if asset.name != fname and re.search(r"\.\d+$", asset.name) is not None:
|
||||
# - because assets end up with .001, .002, etc. names sometimes.
|
||||
# first, let's get the datablock that blocks us from renaming the asset, and rename it to something a bit else:
|
||||
# we need to ge the equivalent datablock ,
|
||||
# then we can swap those names around.
|
||||
datablock = get_equivalent_datablock(ui_props.asset_type, fname)
|
||||
if datablock is not None:
|
||||
datablock.name = fname + "_temprename"
|
||||
replace_name = asset.name
|
||||
asset.name = fname
|
||||
datablock.name = replace_name
|
||||
|
||||
|
||||
def fmt_dimensions(p):
|
||||
|
||||
Reference in New Issue
Block a user