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
@@ -9,10 +9,12 @@ The original method is then called from the new method, with the same arguments,
import json
import os
import time
import re
import logging
from . import icons
from . import icons, version_compare
import blf
import bpy
import bl_pkg.bl_extension_ui as exui
from bpy.props import IntProperty, StringProperty
@@ -23,6 +25,57 @@ EXTENSIONS_API_URL = "https://www.blenderkit.com/api/v1/extensions/"
bk_logger = logging.getLogger(__name__)
# Per-draw-cycle caching for ensure_repo_cache() to avoid repeated
# filesystem stat calls when drawing hundreds of extension items.
_repo_cache_last_check: float = 0.0
_repo_cache_last_result: bool = False
_REPO_CACHE_CHECK_INTERVAL: float = 3.0 # seconds between filesystem checks
# Cached repository reference to avoid iterating repos per item.
_cached_repository = None
_cached_repository_time: float = 0.0
_REPO_LOOKUP_INTERVAL: float = 5.0 # seconds between repo lookups
# Cache for price padding strings to avoid repeated blf.dimensions() calls.
_price_padding_cache: dict = {}
def get_perfect_price_padding(price_str: str, target_length: int = 70) -> str:
"""Generate a padding string to align price text nicely in the UI."""
cache_key = (price_str, target_length)
cached = _price_padding_cache.get(cache_key)
if cached is not None:
return cached
spaces = [
(19, "\u2003"), # em space = U+2003 > 19 units
(2, "\u200a"), # hair space = U+200A > 2 units
(3, "\u2009"), # 6 em space = U+2009 > 3 units
(5, "\u2005"), # 4 em space = U+2005 > 5 units
(6, "\u2004"), # 3 em space = U+2004 > 6 units
]
out = ""
size = blf.dimensions(0, price_str)
w_size = size[0]
final_size = target_length - w_size
if final_size <= 0:
_price_padding_cache[cache_key] = out
return out
while w_size < target_length:
for spc_len, spc_char in spaces:
if w_size + spc_len <= target_length:
out += spc_char
w_size += spc_len
break
else:
break # No suitable space found, exit loop
# double check if we are exporting only white spaces to prevent issues
if re.fullmatch(r"\s*", out) is None:
_price_padding_cache[cache_key] = ""
return ""
_price_padding_cache[cache_key] = out
return out
# --- New Modal Operator ---
class BK_OT_buy_extension_and_watch(Operator):
@@ -278,23 +331,47 @@ def extension_draw_item_blenderkit(
if pkg_block is not None:
row_right.label(text="Blocked ")
elif is_installed:
if is_outdated:
# double check if we are truly outdated
# local repo can have a newer version than remote if user installed a dev version
# so we compare versions here again
local_older = version_compare.compare_versions(
item_local.version, item_remote.version
)
if local_older < 0:
props = row_right.operator("extensions.package_install", text="Update")
props.repo_index = repo_index
props.pkg_id = pkg_id
props.enable_on_install = is_enabled
elif local_older == 0:
row_right.label(text="Up to date ")
else:
props = row_right.operator(
"extensions.package_install",
text=f"Downgrade {item_remote.version}",
)
props.repo_index = repo_index
props.pkg_id = pkg_id
props.enable_on_install = is_enabled
else:
### BlenderKit specific code
# blenderkit logo icon
pcoll = icons.icon_collections["main"]
icon_value = pcoll["logo"].icon_id
# row.label(text="", icon_value=icon_value)
# only enable install for those for whom it's available
if bk_cache_pkg is not None:
can_download_value = bk_cache_pkg.get("can_download")
is_for_sale_flag = bk_cache_pkg.get("is_for_sale") is True
is_free_flag = bk_cache_pkg.get("is_free") is True
# special case for blenderkit addon itself
if pkg_id == "blenderkit":
can_download_value = True
# Free , purchased and subscribed add-ons, probably also private add-ons
if bk_cache_pkg.get("can_download") is True:
if can_download_value is True:
# if the addon is also for sale, it means the user purchased it and we write "install purchased"
if bk_cache_pkg.get("is_for_sale") is True:
if is_for_sale_flag:
props = row_right.operator(
"extensions.package_install",
text="Install purchased",
@@ -309,14 +386,13 @@ def extension_draw_item_blenderkit(
props.repo_index = repo_index
props.pkg_id = pkg_id
# Full plan addons
elif not bk_cache_pkg.get("is_free") and not bk_cache_pkg.get(
"is_for_sale"
# Free addon but limited to full plan
elif (can_download_value == "Rejected in this plan") or (
not is_free_flag and not is_for_sale_flag
):
# open website to subscribe
props = row_right.operator(
"wm.url_open",
text="Subscribe to Full Plan",
text="Requires Full Plan",
icon_value=icon_value,
)
props.url = "https://www.blenderkit.com/plans/pricing/"
@@ -324,9 +400,17 @@ def extension_draw_item_blenderkit(
# Paid addons get a buy button and lead to their website link
else:
# Use the new modal operator
base_price_value = bk_cache_pkg.get("user_price")
if base_price_value in {None, "", "None"}:
base_price_value = bk_cache_pkg.get("base_price")
if base_price_value in {None, "", "None"}:
buy_label = "Buy online"
else:
pad_str = get_perfect_price_padding(base_price_value)
buy_label = f"Buy online {pad_str}${base_price_value}"
props = row_right.operator(
BK_OT_buy_extension_and_watch.bl_idname, # Use bl_idname
text=f"Buy online ${bk_cache_pkg.get('base_price')}",
text=buy_label,
icon_value=icon_value,
)
props.url = bk_cache_pkg.get("website", "") # Pass URL
@@ -528,12 +612,46 @@ def get_repository_by_url(url: str):
return None
def get_blenderkit_repository_cached():
"""Get BlenderKit repository with time-based caching to avoid iterating repos per item."""
global _cached_repository, _cached_repository_time
now = time.time()
if (
now - _cached_repository_time < _REPO_LOOKUP_INTERVAL
and _cached_repository is not None
):
# Verify the cached reference is still valid
try:
_ = _cached_repository.remote_url
return _cached_repository
except ReferenceError:
pass
_cached_repository = get_repository_by_url(EXTENSIONS_API_URL)
_cached_repository_time = now
return _cached_repository
def clear_repo_cache():
"""Clear the repository cache."""
global _repo_cache_last_check, _repo_cache_last_result
wm = bpy.context.window_manager
cache_key = "blenderkit_extensions_repo_cache"
if cache_key in wm:
del wm[cache_key]
# Reset throttle so next ensure_repo_cache() does a fresh check
_repo_cache_last_check = 0.0
_repo_cache_last_result = False
def _sanitize_pkg_for_cache(pkg):
"""Keep values stored as string to prevent C overflow."""
sanitized = {}
for k, v in pkg.items():
if isinstance(v, bool):
sanitized[k] = v
else:
sanitized[k] = str(v)
return sanitized
def ensure_repo_cache():
@@ -541,13 +659,28 @@ def ensure_repo_cache():
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.
Uses a time-based throttle so filesystem checks happen at most once per
_REPO_CACHE_CHECK_INTERVAL seconds, avoiding repeated stat calls when this
function is called per-item during extension list drawing.
"""
global _repo_cache_last_check, _repo_cache_last_result
now = time.time()
if now - _repo_cache_last_check < _REPO_CACHE_CHECK_INTERVAL:
# Consume the reload signal so only the first caller in the throttle window
# sees True — prevents the redraw timer from being registered on every draw tick.
result = _repo_cache_last_result
_repo_cache_last_result = False
return result
_repo_cache_last_check = now
reloaded_flag = False # Track if we actually reloaded
wm = bpy.context.window_manager
cache_key = "blenderkit_extensions_repo_cache"
mtime_key = "blenderkit_extensions_repo_cache_mtime"
blenderkit_repository = get_repository_by_url(EXTENSIONS_API_URL)
blenderkit_repository = get_blenderkit_repository_cached()
if blenderkit_repository is None:
# If repo doesn't exist, clear cache if it exists in window manager
if cache_key in wm:
@@ -566,7 +699,9 @@ def ensure_repo_cache():
current_mtime = None
try:
if os.path.exists(cache_file):
current_mtime = os.path.getmtime(cache_file)
# Use int to avoid float precision loss when stored in Blender IDProperty
# (IDProperties use single-precision floats, os.path.getmtime() returns double)
current_mtime = int(os.path.getmtime(cache_file))
except OSError as e: # Handle potential race condition or permission issue
bk_logger.exception("Could not get modification time for %s.", cache_file)
# Clear cache if we can't verify its freshness? Safer approach.
@@ -621,15 +756,13 @@ def ensure_repo_cache():
for pkg in data.get(
"data", []
): # Handle case where 'data' key might be missing
if (
isinstance(pkg, dict) and "id" in pkg
): # Ensure pkg is a dict and 'id' key exists
new_cache[pkg["id"][:32]] = pkg
else:
if not (isinstance(pkg, dict) and "id" in pkg):
bk_logger.info("Skipping invalid package entry in cache: %s.", pkg)
continue
new_cache[pkg["id"][:32]] = _sanitize_pkg_for_cache(pkg)
wm[cache_key] = new_cache
wm[mtime_key] = current_mtime # Update mtime only on successful load
wm[mtime_key] = current_mtime # Stored as int to survive IDProperty round-trip
reloaded_flag = True # Mark that we reloaded successfully
@@ -652,9 +785,46 @@ def ensure_repo_cache():
if mtime_key in wm:
del wm[mtime_key]
_repo_cache_last_result = reloaded_flag
return reloaded_flag # Return whether cache was actually reloaded
def update_cache_with_asset_prices(assets):
"""Copy addon pricing info from search assets into the extensions cache."""
if not assets:
return
wm = bpy.context.window_manager
cache_key = "blenderkit_extensions_repo_cache"
if cache_key not in wm:
wm[cache_key] = {}
cache = wm[cache_key]
for asset in assets:
if not isinstance(asset, dict):
continue
if asset.get("assetType") != "addon":
continue
dict_params = asset.get("dictParameters") or {}
extension_id = dict_params.get("extensionId") or asset.get("extensionId")
if not extension_id:
continue
cache_key_entry = extension_id[:32]
cache_entry = cache.get(cache_key_entry)
if cache_entry is None:
cache_entry = {}
cache[cache_key_entry] = cache_entry
base_price = asset.get("basePrice")
if base_price not in {None, "", "None"}:
cache_entry["base_price"] = str(base_price)
user_price = asset.get("userPrice")
if user_price not in {None, "", "None"}:
cache_entry["user_price"] = str(user_price)
def ensure_repo_order():
"""Ensure order of repositories in Blender's preferences."""
# get the blenderkit repository