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
@@ -21,6 +21,7 @@ import logging
import math
import os
import random
from typing import Any, Optional, Set, Tuple, Union
import bpy
import mathutils
@@ -28,8 +29,6 @@ from bpy.props import IntProperty, StringProperty
from bpy_extras import view3d_utils
from mathutils import Vector
from typing import Any, Optional, Tuple, Set, Union
from . import (
bg_blender,
colors,
@@ -38,11 +37,12 @@ from . import (
image_utils,
paths,
reports,
search,
ui,
ui_bgl,
ui_panels,
utils,
search,
viewport_utils,
)
from .bl_ui_widgets.bl_ui_button import BL_UI_Button
from .bl_ui_widgets.bl_ui_drag_panel import BL_UI_Drag_Panel
@@ -56,11 +56,8 @@ handler_2d = None
handler_3d = None
DEAD_ZONE = 5 # pixels
"""Number of pixels mouse must move to start drag operation."""
DRAG_THRESHOLD = 10 # pixels
"""Number of pixels mouse must move to consider as a drag (vs click)."""
DEFAULT_DRAG_THRESHOLD = 30 # pixels
"""Pointer travel in pixels needed before we start rendering full drag hints."""
def is_draw_cb_available(self: bpy.types.Operator, context: bpy.types.Context) -> bool:
@@ -106,8 +103,8 @@ def draw_callback_dragging(
Returns:
None
"""
# Only draw 2D elements in the active region where the mouse is. Guard against destroyed operator.
# Only draw 2D elements in the active region where the mouse is. Guard against destroyed operator.
if not is_draw_cb_available(self, context):
return
@@ -356,6 +353,10 @@ def draw_callback_3d_dragging(
if not utils.guard_from_crash():
return
# ignore unless we are dragging
if not self.drag:
return
# Only draw 3D elements in VIEW_3D areas, not in outliner
if context.area.type != "VIEW_3D":
return
@@ -522,19 +523,6 @@ def draw_progress(
ui_bgl.draw_text(text, x, y + 8, 16, color)
def find_and_activate_instancers(
obj: bpy.types.Object,
) -> Optional[bpy.types.Object]:
for ob in bpy.context.visible_objects:
if (
ob.instance_type == "COLLECTION"
and ob.instance_collection
and obj.name in ob.instance_collection.objects
):
utils.activate(ob)
return ob
def mouse_raycast(
region: bpy.types.Region, rv3d: bpy.types.RegionView3D, mx: int, my: int
) -> Tuple[
@@ -741,11 +729,9 @@ def deep_ray_cast(ray_origin: Vector, vec: Vector) -> Tuple[
def object_in_particle_collection(o: bpy.types.Object) -> bool:
"""checks if an object is in a particle system as instance, to not snap to it and not to try to attach material."""
for p in bpy.data.particles:
if p.render_type == "COLLECTION":
if p.instance_collection:
for o1 in p.instance_collection.objects:
if o1 == o:
return True
if p.render_type == "COLLECTION" and p.instance_collection:
if o in p.instance_collection.objects:
return True
if p.render_type == "COLLECTION":
if p.instance_object == o:
return True
@@ -774,22 +760,6 @@ def get_node_tree(context: bpy.types.Context) -> bpy.types.NodeTree:
return context.scene.compositing_node_group
def assign_node_tree(
node_space: bpy.types.SpaceNodeEditor, node_tree: bpy.types.NodeTree
) -> None:
"""Blender version invariant way to assign a node tree to the current node editor."""
if bpy.app.version < (5, 0, 0):
node_space.node_tree = node_tree
return
# blender 5.0+
# recover the node_group from data and assign it
if hasattr(node_space, "node_group"):
node_space.node_group = bpy.data.node_groups[node_tree.name]
elif hasattr(node_space, "node_tree"):
node_space.node_tree = node_tree
class AssetDragOperator(bpy.types.Operator):
"""Drag & drop assets into scene. Operator being drawn when dragging asset."""
@@ -797,9 +767,9 @@ class AssetDragOperator(bpy.types.Operator):
bl_label = "BlenderKit asset drag drop"
asset_search_index: IntProperty(name="Active Index", default=0) # type: ignore
drag_length: IntProperty(name="Drag_length", default=0) # type: ignore
object_name = None
active_operator_id = None
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
@@ -813,32 +783,28 @@ class AssetDragOperator(bpy.types.Operator):
self.hovered_outliner_element: Union[bpy.types.Object, bpy.types.Collection] = (
None
)
self.active_window = None
self.active_area = None
self.active_region = None
self.orig_active_object = None
self.orig_selected_objects = None
self.orig_active_collection = None
self.downloader = None
# Mouse tracking variables
self.start_mouse_x = None
self.start_mouse_y = None
self.start_mouse_x = 0
self.start_mouse_y = 0
self.mouse_x = 0
self.mouse_y = 0
self.mouse_screen_x = 0
self.mouse_screen_y = 0
self.steps = 0
# Store the initial active region pointer
self.active_region_pointer = None
# Initialize outliner tracking variables
self.hovered_outliner_element = None
self.outliner_area = None
self.outliner_region = None
self.orig_selected_objects = None
self.orig_active_object = None
self.orig_active_collection = None
self.prev_area_type = None
# Initialize node editor tracking
@@ -858,6 +824,8 @@ class AssetDragOperator(bpy.types.Operator):
self.iname = ""
self.drag = False
self.steps = 0
self.closed_assetbar = False
def handlers_remove(self) -> None:
"""Remove all draw handlers."""
@@ -873,11 +841,8 @@ class AssetDragOperator(bpy.types.Operator):
self, nodegroup_type: str, editor_type: Optional[str] = None
) -> bool:
"""Check if a nodegroup of a specific type is compatible with the given editor type."""
# Direct matches
if nodegroup_type == editor_type:
return True
# Generic nodegroups can work in any editor
elif nodegroup_type is None:
# Direct matches, or invalid editor
if not nodegroup_type or nodegroup_type == editor_type:
return True
# Otherwise, not compatible
return False
@@ -958,7 +923,7 @@ class AssetDragOperator(bpy.types.Operator):
if obj.type == "MESH":
temp_mesh = object_eval.to_mesh()
mapping = create_material_mapping(obj, temp_mesh)
_mapping = create_material_mapping(obj, temp_mesh)
target_slot = temp_mesh.polygons[self.face_index].material_index
object_eval.to_mesh_clear()
else:
@@ -1073,7 +1038,7 @@ class AssetDragOperator(bpy.types.Operator):
target_collection = ""
# Check what type of element we're dropping on
element_type = type(self.hovered_outliner_element).__name__
_element_type = type(self.hovered_outliner_element).__name__
# If dropping on a collection, set target_collection parameter
if isinstance(self.hovered_outliner_element, bpy.types.Collection):
@@ -1480,10 +1445,10 @@ class AssetDragOperator(bpy.types.Operator):
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
window_width = window.width * self.resolution_factor
window_height = window.height * self.resolution_factor
window_x = window.x
window_y = window.y
window_width = window.width
window_height = window.height
if (
x < window_x
or x > window_x + window_width
@@ -1513,7 +1478,6 @@ class AssetDragOperator(bpy.types.Operator):
return None
context = bpy.context
scene = context.scene
view_layer = context.view_layer
selected_objects = context.selected_objects
active_object = context.active_object
@@ -1630,11 +1594,14 @@ class AssetDragOperator(bpy.types.Operator):
):
"""Get the active object under the mouse cursor during drag."""
region_data = None
for space in active_area.spaces:
if space.type == "VIEW_3D":
region_data = space.region_3d
# precise placement in ortho views, and quad view
region_data = viewport_utils.region_data_for_view(active_area, active_region)
if region_data is None:
for space in active_area.spaces:
if space.type == "VIEW_3D":
region_data = getattr(space, "region_3d", None)
if region_data is not None:
break
# Need to temporarily override context for raycasting
if bpy.app.version < (3, 2, 0): # B3.0, B3.1 - custom context override
@@ -1643,9 +1610,7 @@ class AssetDragOperator(bpy.types.Operator):
"screen": active_window.screen,
"area": active_area,
"region": active_region,
"region_data": active_area.spaces[
0
].region_3d, # Get region_data from space_data
"region_data": region_data,
"scene": context.scene,
"view_layer": context.view_layer,
}
@@ -1757,69 +1722,70 @@ class AssetDragOperator(bpy.types.Operator):
self.in_node_editor = False
self.node_editor_type = None
def _cleanup_drag(self, ui_props, cls, *, reopen_assetbar: bool = False) -> None:
"""Shared teardown: remove handlers, restore cursor, reset drag state."""
self.handlers_remove()
bpy.context.window.cursor_modal_restore()
ui_props.dragging = False
if self.closed_assetbar:
bpy.ops.view3d.run_assetbar_fix_context(keep_running=True, do_search=False)
if reopen_assetbar:
bpy.ops.view3d.blenderkit_asset_bar_widget(
"INVOKE_REGION_WIN", do_search=False
)
cls.active_operator_id = None
def modal(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
cls = type(self)
ui_props = bpy.context.window_manager.blenderkitUI
self.resolution_factor = (
bpy.context.preferences.system.pixel_size
/ bpy.context.preferences.view.ui_scale
)
self.mouse_screen_x = int(
context.window.x * self.resolution_factor + event.mouse_x
)
self.mouse_screen_y = int(
context.window.y * self.resolution_factor + event.mouse_y
)
self.mouse_screen_x = int(context.window.x + event.mouse_x)
self.mouse_screen_y = int(context.window.y + event.mouse_y)
# Find the active region under the mouse cursor using actual screen coordinates
self.active_window, self.active_area, self.active_region = (
self.find_active_region(self.mouse_screen_x, self.mouse_screen_y)
found_window, found_area, found_region = self.find_active_region(
self.mouse_screen_x, self.mouse_screen_y
)
if (
found_region is not None
and found_area is not None
and found_window is not None
):
self.active_window, self.active_area, self.active_region = (
found_window,
found_area,
found_region,
)
# --- CURSOR VISIBILITY FIX ---
if self.active_region is None or self.active_area is None:
bpy.context.window.cursor_modal_set("STOP")
# bpy.context.window.cursor_modal_set("STOP")
bpy.context.window.cursor_modal_restore()
return {"PASS_THROUGH"}
elif self.drag:
if self.drag:
bpy.context.window.cursor_modal_set("NONE")
# Convert screen coords (bottom-left) to region-local coords
# window.x/y and region.x/y are also in bottom-left coordinate system
self.mouse_x = int(
self.mouse_screen_x
- self.active_window.x * self.resolution_factor
- self.active_region.x
self.mouse_screen_x - self.active_window.x - self.active_region.x
)
self.mouse_y = int(
self.mouse_screen_y
- self.active_window.y * self.resolution_factor
- self.active_region.y
self.mouse_screen_y - self.active_window.y - self.active_region.y
)
if self.start_mouse_x is None or self.start_mouse_y is None:
self.start_mouse_x = self.mouse_x
self.start_mouse_y = self.mouse_y
# --- REDRAW ALL WINDOWS/AREAS FOR MULTI-WINDOW DRAG ---
# redraw all windows to update cursor and other elements
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
area.tag_redraw()
current_area_type = self.active_area.type if self.active_area else None
current_area_type = self.active_area.type
# Check if we're transitioning out of the outliner
if (
self.prev_area_type
and self.prev_area_type == "OUTLINER"
and current_area_type != "OUTLINER"
):
# If we're leaving the outliner, restore the original selection
if self.prev_area_type == "OUTLINER" and current_area_type != "OUTLINER":
self.restore_original_selection()
# shift pressed
if event.shift:
self.shift_pressed = True
else:
self.shift_pressed = False
self.shift_pressed = event.shift
# Track if we're in a node editor
self._handle_node_editor_type(current_area_type, self.active_area)
@@ -1832,12 +1798,8 @@ class AssetDragOperator(bpy.types.Operator):
# Store the active region pointer for drawing 2D elements only in this region
self.active_region_pointer = self.active_region.as_pointer()
# Make sure all 3D views get redrawn
for area in context.screen.areas:
area.tag_redraw()
# Handle outliner interaction
if self.active_area.type == "OUTLINER":
if current_area_type == "OUTLINER":
self.hovered_outliner_element = self.find_outliner_element_under_mouse()
self.outliner_window = self.active_window
self.outliner_area = self.active_area
@@ -1853,48 +1815,46 @@ class AssetDragOperator(bpy.types.Operator):
self.active_region_pointer = context.region.as_pointer()
# are we dragging already?
delta_x = abs(self.start_mouse_x - self.mouse_screen_x)
delta_y = abs(self.start_mouse_y - self.mouse_screen_y)
if not self.drag and (
abs(self.start_mouse_x - self.mouse_x) > DRAG_THRESHOLD
or abs(self.start_mouse_y - self.mouse_y) > DRAG_THRESHOLD
delta_x > DEFAULT_DRAG_THRESHOLD or delta_y > DEFAULT_DRAG_THRESHOLD
):
self.drag = True
if self.drag and ui_props.assetbar_on:
# turn off asset bar here, shout start again after finishing drag drop.
if self.drag and ui_props.assetbar_on and not self.closed_assetbar:
# turn off asset bar here; reopen after placement when we actually dragged
ui_props.turn_off = True
if (
event.type == "ESC"
or not ui.mouse_in_region(context.region, self.mouse_x, self.mouse_y)
) and (not self.drag or self.steps < DEAD_ZONE):
# this case is for canceling from inside popup card when there's an escape attempt to close the window
return {"PASS_THROUGH"}
self.closed_assetbar = True
if event.type in {"RIGHTMOUSE", "ESC"}:
# Restore original selection if we changed it
self.restore_original_selection()
self.handlers_remove()
bpy.context.window.cursor_modal_restore()
ui_props.dragging = False
bpy.ops.view3d.blenderkit_asset_bar_widget(
"INVOKE_REGION_WIN", do_search=False
)
self._cleanup_drag(ui_props, cls, reopen_assetbar=True)
return {"CANCELLED"}
self.steps += 1
if (
event.type == "ESC"
or not ui.mouse_in_region(context.region, self.mouse_x, self.mouse_y)
) and (not self.drag or self.steps < 5):
# this case is for canceling from inside popup card when there's an escape attempt to close the window
return {"PASS_THROUGH"}
sprops = bpy.context.window_manager.blenderkit_models
if event.type == "WHEELUPMOUSE":
sprops.offset_rotation_amount += sprops.offset_rotation_step
elif event.type == "WHEELDOWNMOUSE":
sprops.offset_rotation_amount -= sprops.offset_rotation_step
if (
event.type == "MOUSEMOVE"
or event.type == "WHEELUPMOUSE"
or event.type == "WHEELDOWNMOUSE"
):
if event.type in {
"MOUSEMOVE",
"INBETWEEN_MOUSEMOVE",
"WHEELUPMOUSE",
"WHEELDOWNMOUSE",
}:
# sometimes active area or region can be None, so we need to check for that
if self.active_area is None or self.active_region is None:
return {"RUNNING_MODAL"}
@@ -1904,11 +1864,7 @@ class AssetDragOperator(bpy.types.Operator):
self.has_hit = False
# Only perform raycasting in 3D view areas
if (
self.active_region
and self.active_area
and self.active_area.type == "VIEW_3D"
):
if current_area_type == "VIEW_3D":
# prefetch the drag active object info
self.drag_raycast_3d_view(
context,
@@ -1921,21 +1877,15 @@ class AssetDragOperator(bpy.types.Operator):
if self.asset_data["assetType"] in ["model", "printable"]:
self.snapped_bbox_min = Vector(self.asset_data["bbox_min"])
self.snapped_bbox_max = Vector(self.asset_data["bbox_max"])
elif self.active_area.type != "VIEW_3D":
elif current_area_type != "VIEW_3D":
# In outliner, don't do raycasting, but keep has_hit to avoid errors
self.has_hit = False
if event.type == "LEFTMOUSE" and event.value == "RELEASE":
self.mouse_release(context) # Pass context here
self.handlers_remove()
bpy.context.window.cursor_modal_restore()
bpy.ops.view3d.run_assetbar_fix_context(keep_running=True, do_search=False)
ui_props.dragging = False
self.mouse_release(context)
self._cleanup_drag(ui_props, cls)
return {"FINISHED"}
self.steps += 1
# pass event to assetbar so it can close itself
if ui_props.assetbar_on and ui_props.turn_off:
return {"PASS_THROUGH"}
@@ -1946,9 +1896,29 @@ class AssetDragOperator(bpy.types.Operator):
# Before registering callbacks, check for canceling situations: login and localdir popups, sculpt popup/switch
sr = search.get_search_results()
ui_props = bpy.context.window_manager.blenderkitUI
if ui_props.dragging:
return {"CANCELLED"}
cls = type(self)
if cls.active_operator_id is not None and cls.active_operator_id != id(self):
return {"CANCELLED"}
# Acquire drag lock immediately so concurrent invoke paths cannot race while this invoke initializes.
ui_props.dragging = True
cls.active_operator_id = id(self)
self.closed_assetbar = False
# Use the asset_search_index parameter passed to the operator, not the global ui_props.active_index
# This is critical for multi-window support where active_index is shared across windows
self.asset_data = dict(sr[self.asset_search_index])
# Initialize drag-start coordinates immediately in invoke. If mouse-move
# events are sparse (or arrive late), we still compute threshold against
# the true click/press origin instead of first modal tick.
self.mouse_screen_x = int(context.window.x + event.mouse_x)
self.mouse_screen_y = int(context.window.y + event.mouse_y)
self.start_mouse_x = self.mouse_screen_x
self.start_mouse_y = self.mouse_screen_y
# Author assets should not be dragged, cancel immediately
if self.asset_data.get("assetType") == "author":
return {"CANCELLED"}
# add-ons
if self.asset_data.get("assetType") == "addon" and not self.asset_data.get(
"canDownload"
@@ -1959,6 +1929,8 @@ class AssetDragOperator(bpy.types.Operator):
bpy.ops.wm.blenderkit_url_dialog(
"INVOKE_REGION_WIN", url=url, message=message, link_text=link_text
)
ui_props.dragging = False
cls.active_operator_id = None
return {"CANCELLED"}
if not self.asset_data.get("canDownload"):
@@ -1969,6 +1941,8 @@ class AssetDragOperator(bpy.types.Operator):
bpy.ops.wm.blenderkit_url_dialog(
"INVOKE_REGION_WIN", url=url, message=message, link_text=link_text
)
ui_props.dragging = False
cls.active_operator_id = None
return {"CANCELLED"}
prefs = bpy.context.preferences.addons[__package__].preferences
@@ -1982,26 +1956,31 @@ class AssetDragOperator(bpy.types.Operator):
bpy.ops.wm.blenderkit_url_dialog(
"INVOKE_REGION_WIN", url=url, message=message, link_text=link_text
)
ui_props.dragging = False
cls.active_operator_id = None
return {"CANCELLED"}
if self.asset_data.get("assetType") == "brush":
if not (context.sculpt_object or context.image_paint_object):
# either switch to sculpt mode and layout automatically or show a popup message
if context.active_object and context.active_object.type == "MESH":
bpy.ops.object.mode_set(mode="SCULPT")
self.mouse_release(context) # does the main job with assets
if self.asset_data.get("assetType") == "brush" and not (
context.sculpt_object or context.image_paint_object
):
# either switch to sculpt mode and layout automatically or show a popup message
if context.active_object and context.active_object.type == "MESH":
bpy.ops.object.mode_set(mode="SCULPT")
self.mouse_release(context) # does the main job with assets
if bpy.data.workspaces.get("Sculpting") is not None:
bpy.context.window.workspace = bpy.data.workspaces["Sculpting"]
reports.add_report(
"Automatically switched to sculpt mode to use brushes."
)
else:
message = "Select a mesh and switch to sculpt or image paint modes to use the brushes."
bpy.ops.wm.blenderkit_popup_dialog(
"INVOKE_REGION_WIN", message=message, width=500
)
return {"CANCELLED"}
if bpy.data.workspaces.get("Sculpting") is not None:
bpy.context.window.workspace = bpy.data.workspaces["Sculpting"]
reports.add_report(
"Automatically switched to sculpt mode to use brushes."
)
else:
message = "Select a mesh and switch to sculpt or image paint modes to use the brushes."
bpy.ops.wm.blenderkit_popup_dialog(
"INVOKE_REGION_WIN", message=message, width=500
)
ui_props.dragging = False
cls.active_operator_id = None
return {"CANCELLED"}
# the arguments we pass the the callback
args = (self, context)
@@ -2044,48 +2023,25 @@ class AssetDragOperator(bpy.types.Operator):
# but if RNA Struct fails we are not longer able to remove it, so we log an error and store None
self._handlers_universal[space_type] = handler
except (AttributeError, TypeError) as e:
bk_logger.error(f"Could not register handler for {space_type}: {e}")
bk_logger.error("Could not register handler for %s: %s", space_type, e)
self._handlers_universal[space_type] = None
self.mouse_x = 0
self.mouse_y = 0
self.mouse_screen_x = 0
self.mouse_screen_y = 0
self.steps = 0
self.mouse_screen_x = self.start_mouse_x
self.mouse_screen_y = self.start_mouse_y
# Store the initial active region pointer
self.active_region_pointer = context.region.as_pointer()
# Initialize outliner tracking variables
self.hovered_outliner_element = None
self.outliner_area = None
self.outliner_region = None
self.orig_selected_objects = None
self.orig_active_object = None
self.orig_active_collection = None
self.prev_area_type = context.area.type # Track previous area type
# Initialize node editor tracking
self.in_node_editor = False
self.node_editor_type = None
self.shift_pressed = False
# Initialize has_hit to False, and set other 3D properties
# We'll only use these in 3D views, not in outliner
self.has_hit = False
self.snapped_location = (0, 0, 0)
self.snapped_normal = (0, 0, 1)
self.snapped_rotation = (0, 0, 0)
self.face_index = 0
self.matrix = None
self.iname = f".{self.asset_data['thumbnail_small']}"
self.iname = (self.iname[:63]) if len(self.iname) > 63 else self.iname
bpy.context.window.cursor_modal_set("NONE")
bpy.context.window.cursor_modal_restore()
ui_props = bpy.context.window_manager.blenderkitUI
ui_props.dragging = True
self.drag = False
self.steps = 0
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
@@ -2245,7 +2201,7 @@ class DownloadGizmoOperator(BL_UI_OT_draw_operator):
self.button_close.set_image_size((button_size, button_size))
self.button_close.set_image_position((0, 0))
directory = paths.get_temp_dir("%s_search" % self.asset_data["assetType"])
directory = paths.get_temp_dir(f"{self.asset_data['assetType']}_search")
thumbnail_path = os.path.join(directory, self.asset_data["thumbnail_small"])
self.image.set_image(thumbnail_path)
@@ -2296,7 +2252,10 @@ class DownloadGizmoOperator(BL_UI_OT_draw_operator):
bk_logger.debug("unregistering class %s", cls)
instances_copy = cls.instances.copy()
for instance in instances_copy:
bk_logger.debug("- class instance %s", instance)
try:
bk_logger.debug("- class instance %s", instance)
except ReferenceError:
bk_logger.debug("- class instance <deleted>")
try:
instance.unregister_handlers(instance.context)
except Exception as e:
@@ -2407,37 +2366,6 @@ def create_material_mapping(obj, temp_mesh):
return mapping
def add_set_material_node(tree):
"""Add a Set Material node at the end of the node tree"""
# Find output node
output_node = None
for node in tree.nodes:
if node.type == "GROUP_OUTPUT":
output_node = node
break
if output_node:
# Create Set Material node
set_mat_node = tree.nodes.new("GeometryNodeSetMaterial")
# Position it before output
set_mat_node.location = (output_node.location.x - 200, output_node.location.y)
# Connect nodes
last_geometry_socket = None
for source in output_node.inputs:
if source.type == "GEOMETRY":
if source.is_linked:
last_geometry_socket = source.links[0].from_socket
break
if last_geometry_socket:
tree.links.new(last_geometry_socket, set_mat_node.inputs["Geometry"])
tree.links.new(set_mat_node.outputs["Geometry"], output_node.inputs[0])
return set_mat_node
return None
classes = (
AssetDragOperator,
DownloadGizmoOperator,