save startup blend for animation tab & whatnot
This commit is contained in:
2026-04-08 12:10:18 -06:00
parent 57a652524a
commit 692e200ffe
180 changed files with 12336 additions and 3431 deletions
@@ -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.
@@ -1,17 +1,17 @@
{
"last_check": "2026-01-12 10:24:10.400844",
"backup_date": "December-1-2025",
"last_check": "2026-04-02 12:41:20.491300",
"backup_date": "January-12-2026",
"update_ready": true,
"ignore": false,
"just_restored": false,
"just_updated": false,
"version_text": {
"link": "https://github.com/BlenderKit/BlenderKit/releases/download/v3.18.1.251219/blenderkit-v3.18.1.251219.zip",
"link": "https://github.com/BlenderKit/BlenderKit/releases/download/v3.19.1.260402/blenderkit-v3.19.1.260402.zip",
"version": [
3,
18,
19,
1,
251219
260402
]
}
}
@@ -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
@@ -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
@@ -1,50 +0,0 @@
.updater_install_popup
.updater_check_now
.updater_update_now
.updater_update_target
.updater_install_manually
.updater_update_successful
.updater_restore_backup
.updater_ignore
.end_background_check
view3d.asset_drag_drop
object.blenderkit_auto_tags
object.blenderkit_generate_thumbnail
object.blenderkit_regenerate_thumbnail
object.blenderkit_generate_material_thumbnail
object.blenderkit_regenerate_material_thumbnail
object.kill_bg_process
wm.blenderkit_login
wm.blenderkit_logout
wm.blenderkit_login_cancel
scene.blenderkit_addon_manager
scene.blenderkit_addon_choice
scene.blenderkit_download_kill
scene.blenderkit_download
wm.blenderkit_bookmark_asset
wm.blenderkit_mark_notification_read
wm.blenderkit_mark_notifications_read_all
wm.blenderkit_open_notification_target
wm.blenderkit_upvote_comment
wm.blenderkit_is_private_comment
wm.blenderkit_post_comment
wm.logo_status
wm.show_notifications
wm.blenderkit_join_discord
wm.blenderkit_welcome
wm.blenderkit_open_system_directory
wm.blenderkit_asset_popup
view3d.blenderkit_set_comment_reply_id
view3d.blenderkit_set_category_origin
view3d.blenderkit_clear_search_keywords
view3d.close_popup_button
wm.blenderkit_popup_dialog
wm.blenderkit_url_dialog
wm.blenderkit_login_dialog
wm.blenderkit_nodegroup_drop_dialog
object.blenderkit_particles_drop
object.blenderkit_data_trasnfer
wm.modal_timer_operator
view3d.run_assetbar_start_modal
view3d.run_assetbar_fix_context
wm.blenderkit_fast_metadata
@@ -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):
@@ -1,6 +1,6 @@
{
"last_check": "2026-03-16 11:19:55.639825",
"backup_date": "January-12-2026",
"last_check": "2026-04-02 12:41:20.491300",
"backup_date": "April-2-2026",
"update_ready": false,
"ignore": false,
"just_restored": false,