2025-12-01
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user