2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
+540 -111
View File
@@ -16,16 +16,14 @@
#
# ##### END GPL LICENSE BLOCK #####
import ctypes
import logging
import os
import platform
import random
import time
from webbrowser import open_new_tab
import bpy
from bpy.props import IntProperty, StringProperty
from bpy.props import IntProperty, StringProperty, FloatVectorProperty, EnumProperty
from bpy.types import Context, Menu, Panel, UILayout
from . import (
@@ -53,7 +51,7 @@ ACCEPTABLE_ENGINES = ("CYCLES", "BLENDER_EEVEE", "BLENDER_EEVEE_NEXT")
bk_logger = logging.getLogger(__name__)
last_time_dropdown_active = 0.0
last_time_overlay_panel_active = 0.0
def draw_not_logged_in(source, message="Please Login/Signup to use this feature"):
@@ -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
@@ -256,6 +270,24 @@ def draw_panel_nodegroup_search(self, context):
utils.label_multiline(layout, text=props.report)
def draw_common_filters(layout, ui_props):
"""Draw common filter elements shared by multiple asset type panels.
Args:
layout: The UI layout to draw in
ui_props: The UI properties containing filter settings
"""
layout.separator()
row = layout.row()
row.prop(ui_props, "search_bookmarks", text="Bookmarks", icon="BOOKMARKS")
row.prop(ui_props, "own_only", icon="USER")
layout.prop(ui_props, "free_only")
layout.prop(ui_props, "quality_limit", slider=True)
layout.prop(ui_props, "search_license")
layout.prop(ui_props, "search_order_by")
def draw_thumbnail_upload_panel(layout, props):
tex = autothumb.get_texture_ui(props.thumbnail, ".upload_preview")
if not tex or not tex.image:
@@ -440,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,
@@ -479,9 +513,8 @@ class VIEW3D_PT_blenderkit_model_properties(Panel):
def poll(cls, context):
if bpy.context.view_layer.objects.active is None:
return False
# if bpy.context.view_layer.objects.get('asset_data') is None:
# return False
return True
preferences = bpy.context.preferences.addons[__package__].preferences
return not preferences.sidebar_panels
def draw(self, context):
draw_model_context_menu(self, context)
@@ -648,7 +681,8 @@ class VIEW3D_PT_blenderkit_profile(Panel):
@classmethod
def poll(cls, context):
return True
preferences = bpy.context.preferences.addons[__package__].preferences
return not preferences.sidebar_panels
def draw_header(self, context):
layout = self.layout
@@ -1378,6 +1412,9 @@ class VIEW3D_PT_blenderkit_advanced_model_search(Panel):
# NSFW filter
layout.prop(preferences, "nsfw_filter")
# ORDER
layout.prop(ui_props, "search_order_by")
def draw(self, context):
self.draw_layout(self.layout)
@@ -1437,6 +1474,9 @@ class VIEW3D_PT_blenderkit_advanced_material_search(Panel):
layout.prop(ui_props, "quality_limit", slider=True)
layout.prop(ui_props, "search_license")
# ORDER
layout.prop(ui_props, "search_order_by")
def draw(self, context):
self.draw_layout(self.layout)
@@ -1457,19 +1497,8 @@ class VIEW3D_PT_blenderkit_advanced_scene_search(Panel):
return ui_props.down_up == "SEARCH" and ui_props.asset_type == "SCENE"
def draw_layout(self, layout):
wm = bpy.context.window_manager
props = wm.blenderkit_scene
ui_props = wm.blenderkitUI
layout.separator()
row = layout.row()
row.prop(ui_props, "search_bookmarks", text="Bookmarks", icon="BOOKMARKS")
row.prop(ui_props, "own_only", icon="USER")
layout.prop(ui_props, "free_only")
layout.prop(ui_props, "quality_limit", slider=True)
layout.prop(ui_props, "search_license")
ui_props = bpy.context.window_manager.blenderkitUI
draw_common_filters(layout, ui_props)
def draw(self, context):
self.draw_layout(self.layout)
@@ -1497,20 +1526,14 @@ class VIEW3D_PT_blenderkit_advanced_HDR_search(Panel):
ui_props = wm.blenderkitUI
layout = self.layout
layout.separator()
draw_common_filters(layout, ui_props)
row = layout.row()
row.prop(ui_props, "search_bookmarks", text="Bookmarks", icon="BOOKMARKS")
row.prop(ui_props, "own_only", icon="USER")
layout.prop(ui_props, "free_only")
layout.prop(props, "true_hdr")
layout.prop(props, "search_texture_resolution", text="Texture Resolutions")
if props.search_texture_resolution:
row = layout.row(align=True)
row.prop(props, "search_texture_resolution_min", text="Min")
row.prop(props, "search_texture_resolution_max", text="Max")
layout.prop(ui_props, "quality_limit", slider=True)
layout.prop(ui_props, "search_license")
class VIEW3D_PT_blenderkit_advanced_brush_search(Panel):
@@ -1528,17 +1551,79 @@ class VIEW3D_PT_blenderkit_advanced_brush_search(Panel):
return ui_props.down_up == "SEARCH" and ui_props.asset_type == "BRUSH"
def draw_layout(self, layout):
wm = bpy.context.window_manager
ui_props = wm.blenderkitUI
ui_props = bpy.context.window_manager.blenderkitUI
draw_common_filters(layout, ui_props)
layout.separator()
def draw(self, context):
self.draw_layout(self.layout)
class VIEW3D_PT_blenderkit_advanced_nodegroup_search(Panel):
bl_category = "BlenderKit"
bl_idname = "VIEW3D_PT_blenderkit_advanced_nodegroup_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 == "NODEGROUP"
def draw(self, context):
ui_props = bpy.context.window_manager.blenderkitUI
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(ui_props, "search_bookmarks", text="Bookmarks", icon="BOOKMARKS")
row.prop(ui_props, "own_only", icon="USER")
layout.prop(ui_props, "free_only")
layout.prop(ui_props, "quality_limit", slider=True)
layout.prop(ui_props, "search_license")
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"
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 == "PRINTABLE"
def draw_layout(self, layout):
ui_props = bpy.context.window_manager.blenderkitUI
draw_common_filters(layout, ui_props)
def draw(self, context):
self.draw_layout(self.layout)
@@ -1563,8 +1648,8 @@ class VIEW3D_PT_blenderkit_categories(Panel):
def draw(self, context):
# measure time since last dropdown activation/ mouse hover e.t.c.
# this is then used in asset_bar_op.py to cancel asset drag drop if the time is too small and thus means double clicking.
global last_time_dropdown_active
last_time_dropdown_active = time.time()
global last_time_overlay_panel_active
last_time_overlay_panel_active = time.time()
draw_panel_categories(self.layout, context)
@@ -1637,6 +1722,25 @@ class VIEW3D_PT_blenderkit_import_settings(Panel):
# layout.prop(props, 'unpack_files')
def deferred_set_name(props, expected_obj_name):
"""Deferred timer to set empty name of uploaded asset to active Object's name.
We check if the names of active_now object and expected object are the same, because active object could have changed.
This is one-shot timer = return None.
"""
active_now = utils.get_active_asset()
if props.name != "":
return None
if not active_now:
return None
if active_now.name != expected_obj_name:
return None # active object is different from the one on which we have called the timer
props.name_old = (
expected_obj_name # prevents utils.name_update() from running twice
)
props.name = expected_obj_name # this ultimately triggers utils.name_update()
return None
class VIEW3D_PT_blenderkit_unified(Panel):
bl_category = "BlenderKit"
bl_idname = "VIEW3D_PT_blenderkit_unified"
@@ -1647,6 +1751,11 @@ class VIEW3D_PT_blenderkit_unified(Panel):
}
bl_label = ""
@classmethod
def poll(cls, context):
user_preferences = bpy.context.preferences.addons[__package__].preferences
return not user_preferences.sidebar_panels
def draw_header(self, context):
layout = self.layout
ui_props = bpy.context.window_manager.blenderkitUI
@@ -1752,7 +1861,17 @@ 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)
if props and not props.name:
bpy.app.timers.register(
lambda p=props, n=obj.name: deferred_set_name(p, n), first_interval=0.0
)
if ui_props.asset_type == "MODEL" or ui_props.asset_type == "PRINTABLE":
if bpy.context.view_layer.objects.active is not None:
return draw_panel_model_upload(self, context)
@@ -1787,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"""
@@ -2051,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
@@ -2364,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")
@@ -2438,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"
@@ -2540,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
@@ -2581,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]
@@ -2656,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")
@@ -3134,6 +3372,9 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
# box.label(text=str(comment['flags']))
def draw(self, context):
global last_time_overlay_panel_active
last_time_overlay_panel_active = time.time()
layout = self.layout
# top draggable bar with name of the asset
top_row = layout.row()
@@ -3143,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)
@@ -3335,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"}
@@ -3390,7 +3619,7 @@ class PopupDialog(bpy.types.Operator):
class UrlPopupDialog(bpy.types.Operator):
"""Generate Cycles thumbnail for model assets"""
"""Show a popup asking the user to subscribe or log in to access the locked asset"""
bl_idname = "wm.blenderkit_url_dialog"
bl_label = "BlenderKit message:"
@@ -3404,34 +3633,36 @@ class UrlPopupDialog(bpy.types.Operator):
message: bpy.props.StringProperty(name="Text", description="text", default="") # type: ignore[valid-type]
# @classmethod
# def poll(cls, context):
# return bpy.context.view_layer.objects.active is not None
width: bpy.props.IntProperty(name="width", description="width", default=300) # type: ignore[valid-type]
def draw(self, context):
layout = self.layout
row = layout.row()
row.label(text=self.message)
utils.label_multiline(layout, text=self.message, width=300)
row.operator("view3d.close_popup_button", text="", icon="CANCEL")
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? You need to login to access your Full Plan.",
text=f"Already {text}? Log in to access your account.",
width=300,
)
layout.operator_context = "EXEC_DEFAULT"
layout.operator("wm.blenderkit_login", text="Login", icon="URL").signup = (
False
)
layout.operator(
"wm.blenderkit_login", text="Welcome Home", icon="URL"
).signup = False
op.url = self.url
def execute(self, context):
wm = bpy.context.window_manager
return wm.invoke_popup(self, width=300)
return wm.invoke_popup(self, width=self.width)
class LoginPopupDialog(bpy.types.Operator):
@@ -3620,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"]
@@ -3633,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]
@@ -3723,7 +3956,7 @@ def header_search_draw(self, context):
icon_value=icon_id,
)
# FILTER ICON
# FILTER ICON: filters are default or modified
if props.use_filters:
icon_id = pcoll["filter_active"].icon_id
else:
@@ -3764,6 +3997,24 @@ def header_search_draw(self, context):
text="",
icon_value=icon_id,
)
elif ui_props.asset_type == "NODEGROUP":
layout.popover(
panel="VIEW3D_PT_blenderkit_advanced_nodegroup_search",
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",
text="",
icon_value=icon_id,
)
# NSFW filter shield badge - only for models right now
if preferences.nsfw_filter and ui_props.asset_type == "MODEL":
@@ -3802,6 +4053,180 @@ def ui_message(title, message):
bpy.context.window_manager.popup_menu(draw_message, title=title, icon="INFO")
class NodegroupDropDialog(bpy.types.Operator):
"""Dialog for choosing how to add a nodegroup when dropped on an object or in node editor"""
bl_idname = "wm.blenderkit_nodegroup_drop_dialog"
bl_label = "Add Nodegroup"
bl_options = {"REGISTER", "INTERNAL"}
# Store the parameters needed for the download
asset_search_index: bpy.props.IntProperty(default=-1) # type: ignore[valid-type]
target_object_name: bpy.props.StringProperty(default="") # type: ignore[valid-type]
snapped_location: bpy.props.FloatVectorProperty(size=3) # type: ignore[valid-type]
snapped_rotation: bpy.props.FloatVectorProperty(size=3) # type: ignore[valid-type]
# Node editor positioning (when dropped in node editor)
node_x: bpy.props.FloatProperty(default=0.0) # type: ignore[valid-type]
node_y: bpy.props.FloatProperty(default=0.0) # type: ignore[valid-type]
# Option for how to add the nodegroup
add_mode: bpy.props.EnumProperty( # type: ignore[valid-type]
name="Add Mode",
description="How to add the nodegroup",
items=[
(
"MODIFIER",
"As Modifier",
"Add the nodegroup as a new modifier on the object",
),
("NODE", "As Node", "Add the nodegroup as a node in an existing node tree"),
],
default="MODIFIER",
)
# Option for overwriting existing geometry nodes modifier
overwrite_modifier: bpy.props.BoolProperty( # type: ignore[valid-type]
name="Overwrite Last Geometry Nodes Modifier",
description="Replace the last geometry nodes modifier instead of creating a new one (recommended to avoid recursion)",
default=True,
)
def get_existing_geometry_modifiers(self, target_obj):
"""Get list of existing geometry nodes modifiers on target object"""
if not target_obj:
return []
return [mod for mod in target_obj.modifiers if mod.type == "NODES"]
def draw(self, context):
layout = self.layout
# Get asset data for display
sr = search.get_search_results()
if self.asset_search_index >= 0 and self.asset_search_index < len(sr):
asset_data = sr[self.asset_search_index]
col = layout.column(align=True)
col.label(text=f"Adding nodegroup: {asset_data['displayName']}")
# Get target object and check for existing geometry nodes modifiers
target_obj = None
existing_geo_modifiers = []
if self.target_object_name:
target_obj = bpy.data.objects.get(self.target_object_name)
existing_geo_modifiers = self.get_existing_geometry_modifiers(
target_obj
)
col.label(text=f"To object: {self.target_object_name}")
else:
col.label(text="A new target object will be created")
col.separator()
col.prop(self, "add_mode", expand=True)
# Show overwrite option only for MODIFIER mode when there are existing geometry nodes modifiers
if self.add_mode == "MODIFIER" and existing_geo_modifiers:
col.separator()
# Show info about existing modifiers
if len(existing_geo_modifiers) == 1:
col.label(text=f"Found 1 geometry nodes modifier:", icon="INFO")
else:
col.label(
text=f"Found {len(existing_geo_modifiers)} geometry nodes modifiers:",
icon="INFO",
)
# Show the last modifier name
last_modifier = existing_geo_modifiers[-1]
col.label(text=f"{last_modifier.name} (will be affected)")
col.separator()
col.prop(self, "overwrite_modifier")
col.separator()
# Add description based on selected mode
if self.add_mode == "MODIFIER":
if self.target_object_name:
if existing_geo_modifiers and self.overwrite_modifier:
col.label(text="The last geometry nodes modifier will be")
col.label(text="replaced with the new nodegroup.")
col.label(
text="(Recommended to avoid recursion)", icon="CHECKMARK"
)
else:
col.label(text="The nodegroup will be added as a new")
col.label(text="geometry nodes modifier on the object.")
if existing_geo_modifiers:
col.label(text="⚠ May cause recursion issues", icon="ERROR")
else:
col.label(text="A new cube will be created and the")
col.label(text="nodegroup added as a modifier.")
else:
if self.target_object_name:
col.label(text="The nodegroup will be added as a node")
col.label(text="in the geometry nodes editor.")
else:
col.label(text="A new cube will be created and the")
col.label(text="nodegroup added as a node.")
# Show node position if we have it
if self.node_x != 0.0 or self.node_y != 0.0:
col.label(
text=f"Position: ({self.node_x:.1f}, {self.node_y:.1f})",
icon="NODE",
)
def execute(self, context):
# Download the nodegroup with the specified mode
target_object = ""
if self.target_object_name:
target_object = self.target_object_name
# Handle modifier overwrite if requested
if self.add_mode == "MODIFIER" and self.overwrite_modifier and target_object:
target_obj = bpy.data.objects.get(target_object)
if target_obj:
existing_geo_modifiers = self.get_existing_geometry_modifiers(
target_obj
)
if existing_geo_modifiers:
# Remove the last geometry nodes modifier
last_modifier = existing_geo_modifiers[-1]
bk_logger.info(
f"Removed geometry nodes modifier '{last_modifier.name}' before adding new nodegroup"
)
target_obj.modifiers.remove(last_modifier)
# 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(
"EXEC_DEFAULT",
asset_index=self.asset_search_index,
node_x=self.node_x,
node_y=self.node_y,
target_object=target_object,
nodegroup_mode=self.add_mode,
model_location=self.snapped_location,
model_rotation=self.snapped_rotation,
)
else: # MODIFIER mode
bpy.ops.scene.blenderkit_download(
"EXEC_DEFAULT",
asset_index=self.asset_search_index,
model_location=self.snapped_location,
model_rotation=self.snapped_rotation,
target_object=target_object,
nodegroup_mode=self.add_mode,
)
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=400)
classes = (
SetCategoryOperatorOrigin,
SetCategoryOperator,
@@ -3818,6 +4243,9 @@ classes = (
VIEW3D_PT_blenderkit_advanced_scene_search,
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,
VIEW3D_PT_blenderkit_model_properties,
@@ -3850,6 +4278,7 @@ classes = (
NotificationOpenTarget,
MarkAllNotificationsRead,
LoginPopupDialog,
NodegroupDropDialog,
)