update addons (for 5.2)
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class, unregister_class
|
||||
import importlib
|
||||
|
||||
_VPM_AGENT_IMMEDIATE_REGISTER_DONE = locals().get("_VPM_AGENT_IMMEDIATE_REGISTER_DONE", False)
|
||||
|
||||
@@ -15,16 +16,19 @@ module_names = (
|
||||
"prefs",
|
||||
"sidebar",
|
||||
"tweak_builtin_pies",
|
||||
|
||||
"pie_animation",
|
||||
"pie_playback",
|
||||
"pie_keyframes",
|
||||
"pie_copy_attributes",
|
||||
"pie_apply_transform",
|
||||
"pie_camera",
|
||||
"pie_curve_delete",
|
||||
"pie_preferences",
|
||||
"pie_editor_split_merge",
|
||||
"pie_editor_switch",
|
||||
"pie_file",
|
||||
"pie_manipulator",
|
||||
"pie_mesh_delete",
|
||||
"pie_mesh_edge",
|
||||
"pie_mesh_flatten",
|
||||
"pie_mesh_merge",
|
||||
"pie_object_add",
|
||||
@@ -33,16 +37,15 @@ module_names = (
|
||||
"pie_proportional_editing",
|
||||
"pie_relationship_delete",
|
||||
"pie_sculpt_brush_select",
|
||||
"pie_select_similar",
|
||||
"pie_selection",
|
||||
"pie_set_origin",
|
||||
"pie_view_3d",
|
||||
"pie_window",
|
||||
)
|
||||
|
||||
modules = [
|
||||
__import__(__package__ + "." + submod, {}, {}, submod)
|
||||
for submod in module_names
|
||||
]
|
||||
modules = [__import__(__package__ + "." + submod, {}, {}, submod) for submod in module_names]
|
||||
|
||||
|
||||
def register_unregister_modules(modules: list, register: bool):
|
||||
"""Recursively register or unregister modules by looking for either
|
||||
@@ -71,12 +74,14 @@ def register_unregister_modules(modules: list, register: bool):
|
||||
elif hasattr(m, 'unregister'):
|
||||
m.unregister()
|
||||
|
||||
|
||||
def delayed_register(_scene=None):
|
||||
# Register whole add-on with a slight delay,
|
||||
# to make sure Keymap data we need already exists on Blender launch.
|
||||
# Otherwise, keyconfigs.user.keymaps is an empty list, we can't find fallback ops.
|
||||
register_unregister_modules(modules, True)
|
||||
|
||||
|
||||
def register():
|
||||
"""
|
||||
We prefer an *immediate* register during startup, because other add-ons may touch
|
||||
@@ -96,9 +101,10 @@ def register():
|
||||
# Keep behavior unchanged (fallback to timer), but avoid raising during registration.
|
||||
pass
|
||||
|
||||
# NOTE: persistent=True must be set, otherwise this doesn't work when opening
|
||||
# NOTE: persistent=True must be set, otherwise this doesn't work when opening
|
||||
# a .blend file directly from a file browser.
|
||||
bpy.app.timers.register(delayed_register, first_interval=0.0, persistent=True)
|
||||
|
||||
|
||||
def unregister():
|
||||
register_unregister_modules(reversed(modules), False)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
id = "viewport_pie_menus"
|
||||
name = "3D Viewport Pie Menus"
|
||||
version = "1.7.3"
|
||||
version = "1.7.4"
|
||||
tagline = "Various pie menus to speed up your workflow"
|
||||
maintainer = "Community"
|
||||
type = "add-on"
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
# This file requires (and is made possible by) Blender 5.0 due to the find_match() API call.
|
||||
|
||||
import hashlib
|
||||
from typing import Callable, Any
|
||||
import json
|
||||
from typing import Any, Callable
|
||||
|
||||
import bpy
|
||||
import json
|
||||
from bpy.app.translations import pgettext_n as n_
|
||||
from bpy.app.translations import pgettext_rpt as rpt_
|
||||
from bpy.types import KeyMap, KeyMapItem, UILayout
|
||||
|
||||
# Preserve across Reload Scripts (module reload) when possible, but also keep
|
||||
@@ -27,11 +29,14 @@ KEYMAP_ICONS = {
|
||||
'Armature': 'ARMATURE_DATA',
|
||||
'Pose': 'POSE_HLT',
|
||||
'Weight Paint': 'WPAINT_HLT',
|
||||
'Graph Editor': 'GRAPH',
|
||||
'Dopesheet': 'ACTION',
|
||||
'Curve': 'CURVE_DATA',
|
||||
}
|
||||
|
||||
KEYMAP_UI_NAMES = {
|
||||
'Armature': "Armature Edit",
|
||||
'Object Non-modal': "Object Mode",
|
||||
'Armature': n_("Armature Edit"),
|
||||
'Object Non-modal': n_("Object Mode"),
|
||||
}
|
||||
|
||||
KMI_DEFAULTS = {
|
||||
@@ -75,9 +80,9 @@ def register_hotkey(
|
||||
existing_kmi = find_kmi_in_km_by_data(addon_km, hotkey_kwargs, bl_idname, op_kwargs)
|
||||
if existing_kmi:
|
||||
# NOTE: It is extremely important to not register duplicate add-on keymaps, AND
|
||||
# to NOT remove them on add-on unregister, because once an add-on keymap is registered,
|
||||
# to NOT remove them on add-on unregister, because once an add-on keymap is registered,
|
||||
# it is SUPPOSED TO stick around for ever.
|
||||
# This allows Blender to store the associated user keymap, meaning the user's modifications
|
||||
# This allows Blender to store the associated user keymap, meaning the user's modifications
|
||||
# will be stored and restored as expected, whenever the add-on is enabled again.
|
||||
if (addon_km, existing_kmi) not in ADDON_KEYMAPS:
|
||||
ADDON_KEYMAPS.append((addon_km, existing_kmi))
|
||||
@@ -213,7 +218,7 @@ def get_kmi_ui_info(km, kmi) -> tuple[str, str, str]:
|
||||
return km_icon, km_name, kmi_name
|
||||
|
||||
def find_kmi_in_km_by_data(km: KeyMap, hotkey_kwargs: dict, op_idname: str, op_kwargs: dict) -> KeyMapItem | None:
|
||||
"""Loop over KeyMapItems of the provided KeyMap, and return the first entry,
|
||||
"""Loop over KeyMapItems of the provided KeyMap, and return the first entry,
|
||||
if any, which matches the passed key combo, operator, and operator properties.
|
||||
"""
|
||||
|
||||
@@ -369,7 +374,7 @@ class WINDOW_OT_restore_deleted_hotkeys(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
num_restored = restore_deleted_keymap_items_global(context)
|
||||
self.report({'INFO'}, f"Restored {num_restored} deleted keymaps.")
|
||||
self.report({'INFO'}, rpt_("Restored {num_restored} deleted keymaps.").format(num_restored=num_restored))
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ def x_mirror_value(value):
|
||||
if isinstance(value, str):
|
||||
return flip_name(value)
|
||||
elif isinstance(value, Object):
|
||||
get_opposite_obj(value)
|
||||
return get_opposite_obj(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
import os
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty, BoolProperty
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, StringProperty
|
||||
from bpy.types import Context, Event, Operator
|
||||
|
||||
from .bs_utils.hotkeys import register_hotkey
|
||||
|
||||
|
||||
def idname_to_op_class(idname: str):
|
||||
parts = idname.split(".")
|
||||
op = bpy.ops
|
||||
for part in parts:
|
||||
op = getattr(op, part)
|
||||
return op
|
||||
|
||||
|
||||
class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
"""Summon a pie menu only on mouse drag, otherwise initiate an operator"""
|
||||
# This class is for cases where we want to overwrite a built-in shortcut which triggers on Press,
|
||||
# with a pie menu that only appears on mouse drag.
|
||||
|
||||
# This class is for cases where we want to overwrite a built-in shortcut which triggers on Press,
|
||||
# with a pie menu that only appears on mouse drag.
|
||||
# If the user does not mouse drag, invoke the provided fallback operator.
|
||||
# In register_drag_hotkey(), the fallback operator is automagically extracted from the keymap,
|
||||
# at the moment the hotkey is registered.
|
||||
|
||||
bl_idname = "wm.call_menu_pie_drag_only"
|
||||
bl_label = "Pie Menu on Drag"
|
||||
bl_options = {'REGISTER', 'INTERNAL'}
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def update_kmi(self, context):
|
||||
def update_kmi(self, context: Context) -> None:
|
||||
if not hasattr(context, 'keymapitem'):
|
||||
return
|
||||
kmi = context.keymapitem # Set via UILayout.context_pointer_set().
|
||||
kmi = context.keymapitem # Set via UILayout.context_pointer_set().
|
||||
kmi.type = kmi.type
|
||||
|
||||
name: StringProperty(options={'SKIP_SAVE'})
|
||||
@@ -47,7 +43,7 @@ class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
fallback_operator: StringProperty(options={'SKIP_SAVE'})
|
||||
fallback_op_kwargs: StringProperty(default="{}", options={'SKIP_SAVE'})
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
if not self.on_drag:
|
||||
return self.execute(context)
|
||||
self.init_mouse_x = event.mouse_x
|
||||
@@ -55,33 +51,30 @@ class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def invoke_fallback_operator(self):
|
||||
def invoke_fallback_operator(self) -> None:
|
||||
op_cls = idname_to_op_class(self.fallback_operator)
|
||||
if not op_cls:
|
||||
return
|
||||
|
||||
fallback_op_kwargs = json.loads(self.fallback_op_kwargs)
|
||||
if type(fallback_op_kwargs) == str:
|
||||
# Not sure why json.loads seems to sometimes return a string, but it does
|
||||
# when eg. setting Shift+C to drag, and using it when first launching Blender.
|
||||
# After Reload Scripts, it works fine without this workaround. Weird af.
|
||||
if isinstance(fallback_op_kwargs, str):
|
||||
# Not sure why json.loads sometimes returns a string, but it does
|
||||
# when eg. setting Shift+C to drag and using it on first launch.
|
||||
# After Reload Scripts it works fine without this. Weird.
|
||||
fallback_op_kwargs = json.loads(fallback_op_kwargs)
|
||||
|
||||
if op_cls.poll():
|
||||
try:
|
||||
# 1. Execute the original operator and capture the result
|
||||
result = op_cls('INVOKE_DEFAULT', **fallback_op_kwargs)
|
||||
|
||||
# 2. [Added] Check if it's a Save operation and report the message manually
|
||||
if 'FINISHED' in result and self.fallback_operator == 'wm.save_mainfile':
|
||||
# Get current filename, or default to untitled
|
||||
filename = os.path.basename(bpy.data.filepath) if bpy.data.filepath else "untitled.blend"
|
||||
# HACK: Fix save notification not appearing when tapping Ctrl+S.
|
||||
filename = os.path.basename(bpy.data.filepath)
|
||||
self.report({'INFO'}, f'Saved "{filename}"')
|
||||
except TypeError:
|
||||
# This can apparently happen sometimes, see issue #86.
|
||||
print(f"Pie Menu Fallback Operator failed: {self.fallback_operator}, {self.fallback_op_kwargs}")
|
||||
|
||||
def modal(self, context, event):
|
||||
def modal(self, context: Context, event: Event) -> set[str]:
|
||||
if event.value == 'RELEASE':
|
||||
if self.fallback_operator:
|
||||
self.invoke_fallback_operator()
|
||||
@@ -91,10 +84,9 @@ class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
delta_y = abs(event.mouse_y - self.init_mouse_y)
|
||||
if delta_x > threshold or delta_y > threshold:
|
||||
return self.execute(context)
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context) -> set[str]:
|
||||
bpy.ops.wm.call_menu_pie(name=self.name)
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -104,16 +96,16 @@ class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
*,
|
||||
keymap_name: str,
|
||||
pie_name: str,
|
||||
hotkey_kwargs=None,
|
||||
default_fallback_op="",
|
||||
default_fallback_kwargs=None,
|
||||
on_drag=True,
|
||||
):
|
||||
hotkey_kwargs: dict[str, Any] | None = None,
|
||||
default_fallback_op: str = "",
|
||||
default_fallback_kwargs: dict[str, Any] | None = None,
|
||||
on_drag: bool = True,
|
||||
) -> None:
|
||||
if hotkey_kwargs is None:
|
||||
hotkey_kwargs = {'type': "SPACE", 'value': "PRESS"}
|
||||
context = bpy.context
|
||||
fallback_operator = default_fallback_op
|
||||
fallback_op_kwargs = default_fallback_kwargs if default_fallback_kwargs is not None else {}
|
||||
fallback_op_kwargs: dict[str, Any] = default_fallback_kwargs if default_fallback_kwargs is not None else {}
|
||||
|
||||
# IMPORTANT:
|
||||
# Do NOT derive fallback operator/kwargs from the USER keyconfig.
|
||||
@@ -137,21 +129,24 @@ class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
kmi.oskey == hotkey_kwargs.get('oskey', False),
|
||||
kmi.any == hotkey_kwargs.get('any', False),
|
||||
kmi.key_modifier == hotkey_kwargs.get('key_modifier', 'NONE'),
|
||||
kmi.active
|
||||
kmi.active,
|
||||
]:
|
||||
if not condition:
|
||||
break
|
||||
else:
|
||||
fallback_operator = kmi.idname
|
||||
if kmi.properties:
|
||||
fallback_op_kwargs = {k:getattr(kmi.properties, k) if hasattr(kmi.properties, k) else v for k, v in kmi.properties.items()}
|
||||
fallback_op_kwargs = {
|
||||
k: getattr(kmi.properties, k) if hasattr(kmi.properties, k) else v
|
||||
for k, v in kmi.properties.items()
|
||||
}
|
||||
break
|
||||
|
||||
register_hotkey(
|
||||
cls.bl_idname,
|
||||
op_kwargs={
|
||||
'name': pie_name,
|
||||
'fallback_operator': fallback_operator,
|
||||
'name': pie_name,
|
||||
'fallback_operator': fallback_operator,
|
||||
# Deterministic JSON (sort_keys=True) helps keep KMI identity stable across sessions.
|
||||
'fallback_op_kwargs': json.dumps(fallback_op_kwargs, sort_keys=True),
|
||||
'on_drag': on_drag,
|
||||
@@ -160,10 +155,20 @@ class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
keymap_name=keymap_name,
|
||||
)
|
||||
|
||||
def register():
|
||||
|
||||
def idname_to_op_class(idname: str) -> Any:
|
||||
parts = idname.split(".")
|
||||
op = bpy.ops
|
||||
for part in parts:
|
||||
op = getattr(op, part)
|
||||
return op
|
||||
|
||||
|
||||
def register() -> None:
|
||||
bpy.utils.register_class(WM_OT_call_menu_pie_drag_only)
|
||||
|
||||
def unregister():
|
||||
|
||||
def unregister() -> None:
|
||||
# HACK: As a workaround to https://projects.blender.org/blender/blender/issues/150229, we do not unregister
|
||||
# this operator when the add-on is uninstalled, which is pretty bad.
|
||||
# bpy.utils.unregister_class(WM_OT_call_menu_pie_drag_only)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.types import Context, Event, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
@@ -11,34 +12,26 @@ class PIE_MT_apply_transforms(Menu):
|
||||
bl_idname = "PIE_MT_apply_transforms"
|
||||
bl_label = "Apply Transforms"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
props = pie.operator("object.transform_apply", text="Rot/Scale", icon='CON_SIZELIKE')
|
||||
props.location, props.rotation, props.scale = (False, True, True)
|
||||
# 6 - RIGHT
|
||||
props = pie.operator(
|
||||
"object.transform_apply", text="Loc/Rot/Scale", icon='ORIENTATION_LOCAL'
|
||||
)
|
||||
props = pie.operator("object.transform_apply", text="Loc/Rot/Scale", icon='ORIENTATION_LOCAL')
|
||||
props.location, props.rotation, props.scale = (True, True, True)
|
||||
# 2 - BOTTOM
|
||||
pie.operator("object.apply_transforms_of_constraints", text="Soft-Apply Constraints", icon='CONSTRAINT')
|
||||
|
||||
# 8 - TOP
|
||||
props = pie.operator(
|
||||
"object.transform_apply", text="Rotation", icon='CON_ROTLIKE'
|
||||
)
|
||||
props = pie.operator("object.transform_apply", text="Rotation", icon='CON_ROTLIKE')
|
||||
props.location, props.rotation, props.scale = (False, True, False)
|
||||
# 7 - TOP - LEFT
|
||||
props = pie.operator(
|
||||
"object.transform_apply", text="Location", icon='CON_LOCLIKE'
|
||||
)
|
||||
props = pie.operator("object.transform_apply", text="Location", icon='CON_LOCLIKE')
|
||||
props.location, props.rotation, props.scale = (True, False, False)
|
||||
# 9 - TOP - RIGHT
|
||||
props = pie.operator(
|
||||
"object.transform_apply", text="Scale", icon='CON_SIZELIKE'
|
||||
)
|
||||
props = pie.operator("object.transform_apply", text="Scale", icon='CON_SIZELIKE')
|
||||
props.location, props.rotation, props.scale = (False, False, True)
|
||||
# 1 - BOTTOM - LEFT
|
||||
if (
|
||||
@@ -49,11 +42,10 @@ class PIE_MT_apply_transforms(Menu):
|
||||
):
|
||||
pie.operator('object.instancer_empty_to_collection', icon='LINKED')
|
||||
else:
|
||||
pie.operator(
|
||||
"object.make_single_user", text="Make Single-User", icon='DUPLICATE'
|
||||
).obdata = True
|
||||
pie.operator("object.apply_modifiers_shapekeys", icon='MODIFIER_DATA')
|
||||
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.menu("PIE_MT_clear_menu", text="Clear Transforms", icon='THREE_DOTS')
|
||||
pie.menu("PIE_MT_apply_transforms_extras", text="More...", icon='THREE_DOTS')
|
||||
|
||||
|
||||
class OBJECT_OT_apply_transforms_of_constraints(Operator):
|
||||
@@ -64,18 +56,13 @@ class OBJECT_OT_apply_transforms_of_constraints(Operator):
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not any(
|
||||
[
|
||||
any([c.enabled and c.influence > 0 for c in obj.constraints])
|
||||
for obj in context.selected_objects
|
||||
]
|
||||
):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not any(any(c.enabled and c.influence > 0 for c in obj.constraints) for obj in context.selected_objects):
|
||||
cls.poll_message_set("No selected objects with enabled constraints.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
# This is just a wrapper operator, to add a better tooltip and poll message.
|
||||
return bpy.ops.object.visual_transform_apply()
|
||||
|
||||
@@ -88,15 +75,13 @@ class OBJECT_OT_make_meshes_single_user(Operator):
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not any(
|
||||
[obj.data and obj.data.users > 1 for obj in context.selected_objects]
|
||||
):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not any(obj.data and obj.data.users > 1 for obj in context.selected_objects):
|
||||
cls.poll_message_set("No selected objects with multi-user meshes.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
# This is just a wrapper operator, to add a better tooltip and poll message.
|
||||
return bpy.ops.object.duplicates_make_real()
|
||||
|
||||
@@ -107,25 +92,63 @@ class OBJECT_OT_clear_all_transforms(Operator):
|
||||
bl_description = "Clear All Transforms"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
bpy.ops.object.location_clear()
|
||||
bpy.ops.object.rotation_clear()
|
||||
bpy.ops.object.scale_clear()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_apply_modifiers_shapekeys(Operator):
|
||||
"""Apply the object's modifier stack and shape keys. Same as calling "Convert To -> Mesh".
|
||||
|
||||
Shift: Keep the original object as a duplicate."""
|
||||
|
||||
bl_idname = "object.apply_modifiers_shapekeys"
|
||||
bl_label = "Apply Modifiers & Shape Keys"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return bool(context.active_object)
|
||||
|
||||
def invoke(self, _context: Context, event: Event):
|
||||
bpy.ops.object.convert(target='MESH', keep_original=event.shift)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PIE_MT_clear_transforms(Menu):
|
||||
bl_idname = "PIE_MT_clear_menu"
|
||||
bl_label = "Clear Transforms"
|
||||
bl_idname = "PIE_MT_apply_transforms_extras"
|
||||
bl_label = "Apply Transforms Extras"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
layout.operator("clear.all", text="Clear All", icon='NONE')
|
||||
layout.operator("object.location_clear", text="Clear Location", icon='NONE')
|
||||
layout.operator("object.rotation_clear", text="Clear Rotation", icon='NONE')
|
||||
layout.operator("object.scale_clear", text="Clear Scale", icon='NONE')
|
||||
layout.operator("object.origin_clear", text="Clear Origin", icon='NONE')
|
||||
layout.label(text="Clear Transforms")
|
||||
layout.operator("clear.all", text="Clear All", icon='X')
|
||||
layout.operator("object.location_clear", text="Clear Location", icon='CON_LOCLIKE')
|
||||
layout.operator("object.rotation_clear", text="Clear Rotation", icon='CON_ROTLIKE')
|
||||
layout.operator("object.scale_clear", text="Clear Scale", icon='CON_SIZELIKE')
|
||||
layout.operator("object.origin_clear", text="Clear Origin", icon='PIVOT_CURSOR')
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="To Deltas")
|
||||
props = layout.operator("object.transforms_to_deltas", text="Location to Deltas", icon='CON_LOCLIKE')
|
||||
props.mode = 'LOC'
|
||||
props = layout.operator("object.transforms_to_deltas", text="Rotation to Deltas", icon='CON_ROTLIKE')
|
||||
props.mode = 'ROT'
|
||||
props = layout.operator("object.transforms_to_deltas", text="Scale to Deltas", icon='CON_SIZELIKE')
|
||||
props.mode = 'SCALE'
|
||||
props = layout.operator(
|
||||
"object.transforms_to_deltas", text="All Transforms to Deltas", icon='ORIENTATION_LOCAL'
|
||||
)
|
||||
props.mode = 'ALL'
|
||||
layout.operator("object.anim_transforms_to_deltas", text="Animated Transforms to Deltas", icon='KEYINGSET')
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Mesh")
|
||||
make_single = layout.operator("object.make_single_user", text="Make Single-User", icon='DUPLICATE')
|
||||
make_single.obdata = True
|
||||
layout.operator("object.duplicates_make_real", text="Make Instances Real", icon='DUPLICATE')
|
||||
|
||||
|
||||
registry = (
|
||||
@@ -133,6 +156,7 @@ registry = (
|
||||
OBJECT_OT_apply_transforms_of_constraints,
|
||||
OBJECT_OT_make_meshes_single_user,
|
||||
OBJECT_OT_clear_all_transforms,
|
||||
OBJECT_OT_apply_modifiers_shapekeys,
|
||||
PIE_MT_clear_transforms,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.types import Context, Event, Menu, Object, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
# Some magic numbers that are either hard-coded into Blender,
|
||||
@@ -16,7 +17,7 @@ class PIE_MT_camera(Menu):
|
||||
bl_idname = "PIE_MT_camera"
|
||||
bl_label = "Camera"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
scene = context.scene
|
||||
space = context.area.spaces.active
|
||||
@@ -35,9 +36,7 @@ class PIE_MT_camera(Menu):
|
||||
text = "View Active Camera"
|
||||
if context.space_data.region_3d.view_perspective == 'CAMERA':
|
||||
text = "Return to Viewport"
|
||||
pie.operator(
|
||||
'view3d.view_camera_with_poll', text=text, icon='VIEW_CAMERA_UNSELECTED'
|
||||
)
|
||||
pie.operator('view3d.view_camera_with_poll', text=text, icon='VIEW_CAMERA_UNSELECTED')
|
||||
# 2 - BOTTOM
|
||||
box = pie.box().column(align=True)
|
||||
row = box.row()
|
||||
@@ -48,9 +47,20 @@ class PIE_MT_camera(Menu):
|
||||
if camera:
|
||||
box.prop(camera.data, 'lens')
|
||||
box.prop(camera.data, 'sensor_width')
|
||||
|
||||
row = box.row(align=True)
|
||||
row.prop(camera.data.dof, 'use_dof', text="")
|
||||
fstop_row = row.row()
|
||||
fstop_row.enabled = camera.data.dof.use_dof
|
||||
fstop_row.prop(camera.data.dof, 'aperture_fstop', text="Depth of Field F-Stop")
|
||||
|
||||
box.separator()
|
||||
row = box.row(align=True)
|
||||
row.prop(camera.data, 'show_passepartout', text="")
|
||||
row.prop(camera.data, 'passepartout_alpha')
|
||||
row_alpha = row.row()
|
||||
row_alpha.enabled = camera.data.show_passepartout
|
||||
row_alpha.prop(camera.data, 'passepartout_alpha')
|
||||
|
||||
row = box.row(align=True)
|
||||
row.prop(camera.data, 'show_composition_thirds')
|
||||
row.prop(camera.data, 'show_composition_center')
|
||||
@@ -88,13 +98,13 @@ class VIEW3D_OT_lock_active_camera_transforms(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not get_current_camera(context):
|
||||
cls.poll_message_set("No active camera in this viewport.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
camera = get_current_camera(context)
|
||||
|
||||
lock = False
|
||||
@@ -119,16 +129,14 @@ class VIEW3D_OT_camera_fit_view(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not context.space_data or not hasattr(context.space_data, 'region_3d'):
|
||||
return False
|
||||
if context.space_data.region_3d.view_perspective == 'CAMERA':
|
||||
cls.poll_message_set("Already in a camera view.")
|
||||
return False
|
||||
camera = get_current_camera(context)
|
||||
if not camera and not (
|
||||
context.active_object and context.active_object.type == 'CAMERA'
|
||||
):
|
||||
if not camera and not (context.active_object and context.active_object.type == 'CAMERA'):
|
||||
cls.poll_message_set("No active camera.")
|
||||
return False
|
||||
if camera and (any(camera.lock_location) or any(camera.lock_rotation)):
|
||||
@@ -136,14 +144,12 @@ class VIEW3D_OT_camera_fit_view(Operator):
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context, _event):
|
||||
if not context.scene.camera and not (
|
||||
context.active_object and context.active_object.type == 'CAMERA'
|
||||
):
|
||||
def invoke(self, context: Context, _event: Event):
|
||||
if not context.scene.camera and not (context.active_object and context.active_object.type == 'CAMERA'):
|
||||
self.create_camera = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
cam = get_current_camera(context)
|
||||
|
||||
space = context.space_data
|
||||
@@ -173,7 +179,7 @@ class VIEW3D_OT_camera_from_view(Operator):
|
||||
bl_label = "New Camera from View"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
if context.mode != 'OBJECT':
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.camera_add()
|
||||
@@ -193,7 +199,7 @@ class VIEW3D_OT_set_active_camera(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not context.active_object:
|
||||
cls.poll_message_set("No active object.")
|
||||
return False
|
||||
@@ -205,7 +211,7 @@ class VIEW3D_OT_set_active_camera(Operator):
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
context.scene.camera = context.active_object
|
||||
self.report({'INFO'}, f"Set active camera: {context.active_object.name}")
|
||||
return {'FINISHED'}
|
||||
@@ -219,17 +225,17 @@ class VIEW3D_OT_view_camera_with_poll(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not get_current_camera(context):
|
||||
cls.poll_message_set("No active camera.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
return bpy.ops.view3d.view_camera()
|
||||
|
||||
|
||||
def get_current_camera(context):
|
||||
def get_current_camera(context: Context) -> Object | None:
|
||||
space = context.area.spaces.active
|
||||
if space.type == 'VIEW_3D' and space.use_local_camera:
|
||||
return space.camera
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context, Menu, Object, Operator, Spline
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
class PIE_MT_curve_delete(Menu):
|
||||
bl_idname = "PIE_MT_curve_delete"
|
||||
bl_label = "Curve Delete"
|
||||
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
pie.operator(CURVE_OT_dissolve_verts.bl_idname, text="Dissolve Points")
|
||||
# 6 - RIGHT
|
||||
pie.operator(CURVE_OT_delete_verts.bl_idname, text="Delete Points")
|
||||
# 2 - BOTTOM
|
||||
pie.operator(CURVE_OT_delete_segment.bl_idname, text="Delete Segments")
|
||||
# 8 - TOP
|
||||
# 7 - TOP - LEFT
|
||||
# 9 - TOP - RIGHT
|
||||
# 1 - BOTTOM - LEFT
|
||||
# 3 - BOTTOM - RIGHT
|
||||
|
||||
|
||||
class CURVE_OT_delete_verts(Operator):
|
||||
"""Delete selected curve points"""
|
||||
|
||||
bl_idname = "curve.delete_verts"
|
||||
bl_label = "Delete Points"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
curve_objects = _curve_objects_in_edit_mode(context)
|
||||
if not curve_objects:
|
||||
cls.poll_message_set("Must be editing a curve.")
|
||||
return False
|
||||
if any(spline_has_any_selected(spline) for obj in curve_objects for spline in obj.data.splines):
|
||||
return True
|
||||
cls.poll_message_set("No points are selected.")
|
||||
return False
|
||||
|
||||
def execute(self, _context: Context):
|
||||
return bpy.ops.curve.delete(type='VERT')
|
||||
|
||||
|
||||
class CURVE_OT_dissolve_verts(Operator):
|
||||
"""Dissolve selected Bezier points, preserving the curve shape"""
|
||||
|
||||
bl_idname = "curve.dissolve_verts_py"
|
||||
bl_label = "Dissolve Points"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
curve_objects = _curve_objects_in_edit_mode(context)
|
||||
if not curve_objects:
|
||||
cls.poll_message_set("Must be editing a curve.")
|
||||
return False
|
||||
if any(
|
||||
spline_has_dissolvable_bezier_point(spline) #
|
||||
for obj in curve_objects
|
||||
for spline in obj.data.splines
|
||||
):
|
||||
return True
|
||||
cls.poll_message_set("No selected Bezier points with neighbours on both sides.")
|
||||
return False
|
||||
|
||||
def execute(self, _context: Context):
|
||||
return bpy.ops.curve.dissolve_verts()
|
||||
|
||||
|
||||
class CURVE_OT_delete_segment(Operator):
|
||||
"""Delete segments between selected consecutive curve points"""
|
||||
|
||||
bl_idname = "curve.delete_segment"
|
||||
bl_label = "Delete Segments"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
curve_objects = _curve_objects_in_edit_mode(context)
|
||||
if not curve_objects:
|
||||
cls.poll_message_set("Must be editing a curve.")
|
||||
return False
|
||||
if any(
|
||||
spline_has_consecutive_selected(spline) #
|
||||
for obj in curve_objects
|
||||
for spline in obj.data.splines
|
||||
):
|
||||
return True
|
||||
cls.poll_message_set("No two consecutive points are selected.")
|
||||
return False
|
||||
|
||||
def execute(self, _context: Context):
|
||||
return bpy.ops.curve.delete(type='SEGMENT')
|
||||
|
||||
|
||||
def _curve_objects_in_edit_mode(context: Context) -> list[Object]:
|
||||
return [obj for obj in (context.objects_in_mode or []) if obj.type == 'CURVE' and obj.data]
|
||||
|
||||
|
||||
def spline_has_dissolvable_bezier_point(spline: Spline) -> bool:
|
||||
if spline.type != 'BEZIER':
|
||||
return False
|
||||
points = spline.bezier_points
|
||||
point_count = len(points)
|
||||
if spline.use_cyclic_u:
|
||||
return point_count > 2 and spline_has_any_selected(spline)
|
||||
return point_count >= 3 and any(point.select_control_point for point in points[1 : point_count - 1])
|
||||
|
||||
|
||||
def spline_has_any_selected(spline: Spline) -> bool:
|
||||
if spline.type == 'BEZIER':
|
||||
return any(point.select_control_point for point in spline.bezier_points)
|
||||
return any(point.select for point in spline.points)
|
||||
|
||||
|
||||
def spline_has_consecutive_selected(spline: Spline) -> bool:
|
||||
if spline.type == 'BEZIER':
|
||||
sel = [point.select_control_point for point in spline.bezier_points]
|
||||
else:
|
||||
sel = [point.select for point in spline.points]
|
||||
for prev, curr in zip(sel, sel[1:]):
|
||||
if prev and curr:
|
||||
return True
|
||||
return spline.use_cyclic_u and len(sel) >= 2 and sel[-1] and sel[0]
|
||||
|
||||
|
||||
registry = [
|
||||
CURVE_OT_delete_verts,
|
||||
CURVE_OT_dissolve_verts,
|
||||
CURVE_OT_delete_segment,
|
||||
PIE_MT_curve_delete,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name="Curve",
|
||||
pie_name=PIE_MT_curve_delete.bl_idname,
|
||||
hotkey_kwargs={'type': "X", 'value': "PRESS"},
|
||||
on_drag=True,
|
||||
)
|
||||
@@ -1,109 +1,79 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy, sys, json
|
||||
from bpy.types import Menu, Operator
|
||||
import json
|
||||
|
||||
import bpy
|
||||
from bpy.props import StringProperty
|
||||
from bpy.types import Context, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
AVAILABLE_PROPERTIES_EDITORS = []
|
||||
|
||||
|
||||
class PIE_MT_editor_switch(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch"
|
||||
bl_label = "Editor Switch"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
pie.operator(
|
||||
'wm.call_menu_pie', text="Video Editing...", icon="SEQUENCE"
|
||||
).name = "PIE_MT_editor_switch_video"
|
||||
pie.operator('wm.call_menu_pie', text="Video Editing...", icon="SEQUENCE").name = "PIE_MT_editor_switch_video"
|
||||
# 6 - RIGHT
|
||||
pie.operator('wm.call_menu_pie', text="Nodes...", icon="NODETREE").name = (
|
||||
"PIE_MT_editor_switch_nodes"
|
||||
)
|
||||
pie.operator('wm.call_menu_pie', text="Nodes...", icon="NODETREE").name = "PIE_MT_editor_switch_nodes"
|
||||
# 2 - BOTTOM
|
||||
pie.operator('wm.call_menu_pie', text="Data...", icon="PRESET").name = (
|
||||
"PIE_MT_editor_switch_data"
|
||||
)
|
||||
pie.operator('wm.call_menu_pie', text="Data...", icon="PRESET").name = "PIE_MT_editor_switch_data"
|
||||
# 8 - TOP
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="3D View", icon="VIEW3D"
|
||||
).ui_type = "VIEW_3D"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="3D View", icon="VIEW3D").ui_type = "VIEW_3D"
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(
|
||||
'wm.call_menu_pie', text="2D Editors...", icon="FILE_IMAGE"
|
||||
).name = "PIE_MT_editor_switch_image"
|
||||
pie.operator('wm.call_menu_pie', text="2D Editors...", icon="FILE_IMAGE").name = "PIE_MT_editor_switch_image"
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator('wm.call_menu_pie', text="Script...", icon="SCRIPT").name = (
|
||||
"PIE_MT_editor_switch_script"
|
||||
)
|
||||
pie.operator('wm.call_menu_pie', text="Script...", icon="SCRIPT").name = "PIE_MT_editor_switch_script"
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.separator()
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator(
|
||||
'wm.call_menu_pie', text="Animation...", icon="ARMATURE_DATA"
|
||||
).name = "PIE_MT_editor_switch_anim"
|
||||
pie.operator('wm.call_menu_pie', text="Animation...", icon="ARMATURE_DATA").name = "PIE_MT_editor_switch_anim"
|
||||
|
||||
|
||||
class PIE_MT_editor_switch_video(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_video"
|
||||
bl_label = "Editor Switch: Video"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname,
|
||||
text="Sequencer & Preview",
|
||||
icon="SEQ_SPLITVIEW",
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Sequencer & Preview", icon="SEQ_SPLITVIEW")
|
||||
op.ui_type = "SEQUENCE_EDITOR"
|
||||
op.view_type = 'SEQUENCER_PREVIEW'
|
||||
# 6 - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Movie Clip Editor", icon="SEQUENCE"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Movie Clip Editor", icon="SEQUENCE")
|
||||
op.ui_type = "CLIP_EDITOR"
|
||||
op.mode = 'TRACKING'
|
||||
op.view = 'CLIP'
|
||||
# 2 - BOTTOM
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Clip Mask Editor", icon="MOD_MASK"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Clip Mask Editor", icon="MOD_MASK")
|
||||
op.ui_type = "CLIP_EDITOR"
|
||||
op.mode = 'MASK'
|
||||
# 8 - TOP
|
||||
pie.separator()
|
||||
# 7 - TOP - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Preview", icon="SEQ_PREVIEW"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Preview", icon="SEQ_PREVIEW")
|
||||
op.ui_type = "SEQUENCE_EDITOR"
|
||||
op.view_type = 'PREVIEW'
|
||||
# 9 - TOP - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Tracking: Dopesheet", icon="ACTION"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Tracking: Dopesheet", icon="ACTION")
|
||||
op.ui_type = "CLIP_EDITOR"
|
||||
op.mode = 'TRACKING'
|
||||
op.view = 'DOPESHEET'
|
||||
# 1 - BOTTOM - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Sequencer", icon="SEQ_SEQUENCER"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Sequencer", icon="SEQ_SEQUENCER")
|
||||
op.ui_type = "SEQUENCE_EDITOR"
|
||||
op.view_type = 'SEQUENCER'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Tracking: Graph", icon="GRAPH"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Tracking: Graph", icon="GRAPH")
|
||||
op.ui_type = "CLIP_EDITOR"
|
||||
op.mode = 'TRACKING'
|
||||
op.view = 'GRAPH'
|
||||
@@ -113,42 +83,30 @@ class PIE_MT_editor_switch_nodes(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_nodes"
|
||||
bl_label = "Editor Switch: Nodes"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Compositing", icon="NODE_COMPOSITING"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Compositing", icon="NODE_COMPOSITING")
|
||||
op.ui_type = 'CompositorNodeTree'
|
||||
# 6 - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Shader", icon="NODE_MATERIAL"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Shader", icon="NODE_MATERIAL")
|
||||
op.ui_type = 'ShaderNodeTree'
|
||||
op.shader_type = 'OBJECT'
|
||||
# 2 - BOTTOM
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Geometry", icon="GEOMETRY_NODES"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Geometry", icon="GEOMETRY_NODES")
|
||||
op.ui_type = 'GeometryNodeTree'
|
||||
# 8 - TOP
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Texture", icon="TEXTURE")
|
||||
op.ui_type = 'TextureNodeTree'
|
||||
# 7 - TOP - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Line Style", icon="LINE_DATA"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Line Style", icon="LINE_DATA")
|
||||
op.ui_type = 'ShaderNodeTree'
|
||||
op.shader_type = 'LINESTYLE'
|
||||
# 9 - TOP - RIGHT
|
||||
if get_custom_nodetree_types(context, layout):
|
||||
pie.menu(
|
||||
'PIE_MT_editor_switch_nodes_custom',
|
||||
text="Custom Nodes",
|
||||
icon='NODETREE',
|
||||
)
|
||||
if get_custom_nodetree_types(context):
|
||||
pie.menu('PIE_MT_editor_switch_nodes_custom', text="Custom Nodes", icon='NODETREE')
|
||||
else:
|
||||
pie.separator()
|
||||
# 1 - BOTTOM - LEFT
|
||||
@@ -163,253 +121,117 @@ class PIE_MT_editor_switch_nodes_custom(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_nodes_custom"
|
||||
bl_label = "Editor Switch: Custom Nodes"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
|
||||
for ui_type, label, icon in get_custom_nodetree_types(context, layout):
|
||||
layout.operator(
|
||||
WM_OT_set_area_type.bl_idname, text=label, icon=icon
|
||||
).ui_type = ui_type
|
||||
|
||||
|
||||
def get_custom_nodetree_types(context, layout):
|
||||
"""There are no rules In love, war, and Python scripting.
|
||||
The only way to get custom node tree types is this:
|
||||
- Try to set the UI type to some nonsense
|
||||
- The resulting error tells you the possible UI types
|
||||
- Catch and crunch the error string into a set
|
||||
- Subtract from this the set of built-in UI types
|
||||
- The result is a set of the custom node tree type names
|
||||
- We can get their class with bl_rna_get_subclass_py
|
||||
|
||||
"""
|
||||
builtin_types = set(
|
||||
[
|
||||
'VIEW_3D',
|
||||
'IMAGE_EDITOR',
|
||||
'UV',
|
||||
'CompositorNodeTree',
|
||||
'TextureNodeTree',
|
||||
'GeometryNodeTree',
|
||||
'ShaderNodeTree',
|
||||
'SEQUENCE_EDITOR',
|
||||
'CLIP_EDITOR',
|
||||
'DOPESHEET',
|
||||
'TIMELINE',
|
||||
'FCURVES',
|
||||
'DRIVERS',
|
||||
'NLA_EDITOR',
|
||||
'TEXT_EDITOR',
|
||||
'CONSOLE',
|
||||
'INFO',
|
||||
'OUTLINER',
|
||||
'PROPERTIES',
|
||||
'FILES',
|
||||
'ASSETS',
|
||||
'SPREADSHEET',
|
||||
'PREFERENCES',
|
||||
]
|
||||
)
|
||||
ret = []
|
||||
try:
|
||||
context.area.ui_type = "Nonsense."
|
||||
except Exception as exc:
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
if not exc_value:
|
||||
return []
|
||||
type_list_str = (
|
||||
str(exc_value)
|
||||
.split("not found in ")[-1]
|
||||
.replace("(", "[")
|
||||
.replace(")", "]")
|
||||
.replace("'", '"')
|
||||
)
|
||||
type_list = set(json.loads(type_list_str))
|
||||
custom_types = type_list - builtin_types
|
||||
|
||||
for type_name in custom_types:
|
||||
type_class = bpy.types.NodeTree.bl_rna_get_subclass_py(type_name)
|
||||
if not type_class:
|
||||
continue
|
||||
ret.append((type_name, type_class.bl_label, type_class.bl_icon))
|
||||
|
||||
return ret
|
||||
for ui_type, label, icon in get_custom_nodetree_types(context):
|
||||
layout.operator(WM_OT_set_area_type.bl_idname, text=label, icon=icon).ui_type = ui_type
|
||||
|
||||
|
||||
class PIE_MT_editor_switch_data(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_data"
|
||||
bl_label = "Editor Switch: Data"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="File Browser", icon="FILEBROWSER"
|
||||
).ui_type = "FILES"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="File Browser", icon="FILEBROWSER").ui_type = "FILES"
|
||||
# 6 - RIGHT
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Preferences", icon="PREFERENCES"
|
||||
).ui_type = "PREFERENCES"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Preferences", icon="PREFERENCES").ui_type = "PREFERENCES"
|
||||
# 2 - BOTTOM
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Outliner", icon="OUTLINER"
|
||||
).ui_type = "OUTLINER"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Outliner", icon="OUTLINER").ui_type = "OUTLINER"
|
||||
# 8 - TOP
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Properties", icon="PROPERTIES"
|
||||
).ui_type = "PROPERTIES"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Properties", icon="PROPERTIES").ui_type = "PROPERTIES"
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Asset Browser", icon="ASSET_MANAGER"
|
||||
).ui_type = "ASSETS"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Asset Browser", icon="ASSET_MANAGER").ui_type = "ASSETS"
|
||||
# 9 - TOP - RIGHT
|
||||
pie.menu(
|
||||
"PIE_MT_editor_switch_properties",
|
||||
text="Properties Types...",
|
||||
icon="PROPERTIES",
|
||||
)
|
||||
pie.menu("PIE_MT_editor_switch_properties", text="Properties Types...", icon="PROPERTIES")
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Spreadsheet", icon="SPREADSHEET"
|
||||
).ui_type = "SPREADSHEET"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Spreadsheet", icon="SPREADSHEET").ui_type = "SPREADSHEET"
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.menu(
|
||||
"PIE_MT_editor_switch_outliner", text="Outliner Types...", icon="OUTLINER"
|
||||
)
|
||||
pie.menu("PIE_MT_editor_switch_outliner", text="Outliner Types...", icon="OUTLINER")
|
||||
|
||||
|
||||
class PIE_MT_editor_switch_outliner(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_outliner"
|
||||
bl_label = "Editor Switch: Outliner"
|
||||
|
||||
def draw(self, context):
|
||||
pie = self.layout#.menu_pie()
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
|
||||
# 4 - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Scenes", icon="SCENE_DATA"
|
||||
)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="Scenes", icon="SCENE_DATA")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'SCENES'
|
||||
# 6 - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="View Layer", icon="RENDERLAYERS"
|
||||
)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="View Layer", icon="RENDERLAYERS")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'VIEW_LAYER'
|
||||
# 2 - BOTTOM
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Video Sequencer", icon="SEQUENCE"
|
||||
)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="Video Sequencer", icon="SEQUENCE")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'SEQUENCE'
|
||||
# 8 - TOP
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Blender File", icon="FILE_BLEND"
|
||||
)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="Blender File", icon="FILE_BLEND")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'LIBRARIES'
|
||||
# 7 - TOP - LEFT
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Data API", icon="RNA")
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="Data API", icon="RNA")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'DATA_API'
|
||||
# 9 - TOP - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname,
|
||||
text="Library Overrides",
|
||||
icon="LIBRARY_DATA_OVERRIDE",
|
||||
)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="Library Overrides", icon="LIBRARY_DATA_OVERRIDE")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'LIBRARY_OVERRIDES'
|
||||
# 1 - BOTTOM - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Orphan Data", icon="ORPHAN_DATA"
|
||||
)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="Orphan Data", icon="ORPHAN_DATA")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'ORPHAN_DATA'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
|
||||
|
||||
def get_available_properties_editors(context):
|
||||
"""Similar hack as get_custom_nodetree_types.
|
||||
Ideally we need an existing Properties editor, otherwise
|
||||
the list of available types is not up to date.
|
||||
Although even in that case, we still don't get an error,
|
||||
so this is fine.
|
||||
"""
|
||||
org_ui_type = context.area.ui_type
|
||||
try:
|
||||
space = None
|
||||
for area in context.screen.areas:
|
||||
if area.ui_type == 'PROPERTIES':
|
||||
space = area.spaces.active
|
||||
break
|
||||
if not space:
|
||||
context.area.ui_type = 'PROPERTIES'
|
||||
space = context.space_data
|
||||
space.context = "Nonsense."
|
||||
except Exception as exc:
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
if not exc_value:
|
||||
return []
|
||||
type_list_str = (
|
||||
str(exc_value)
|
||||
.split("not found in ")[-1]
|
||||
.replace("(", "[")
|
||||
.replace(")", "]")
|
||||
.replace("'", '"')
|
||||
)
|
||||
available_types = set(json.loads(type_list_str))
|
||||
context.area.ui_type = org_ui_type
|
||||
return available_types
|
||||
|
||||
|
||||
class PIE_MT_editor_switch_properties(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_properties"
|
||||
bl_label = "Editor Switch: Properties"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
|
||||
available_editors = get_available_properties_editors(context)
|
||||
|
||||
def safe_draw(pie, text, icon, context_name):
|
||||
def draw_entry(text, icon, context_name):
|
||||
if context_name in available_editors:
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text=text, icon=icon)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text=text, icon=icon)
|
||||
op.ui_type = "PROPERTIES"
|
||||
op.context_name = context_name
|
||||
else:
|
||||
pie.separator()
|
||||
layout.separator()
|
||||
|
||||
safe_draw(layout, "Render", 'SCENE', 'RENDER')
|
||||
safe_draw(layout, "Output", 'OUTPUT', 'OUTPUT')
|
||||
safe_draw(layout, "View Layer", 'RENDERLAYERS', 'VIEW_LAYER')
|
||||
safe_draw(layout, "Scene", 'SCENE_DATA', 'SCENE')
|
||||
safe_draw(layout, "World", 'WORLD', 'WORLD')
|
||||
draw_entry("Render", 'SCENE', 'RENDER')
|
||||
draw_entry("Output", 'OUTPUT', 'OUTPUT')
|
||||
draw_entry("View Layer", 'RENDERLAYERS', 'VIEW_LAYER')
|
||||
draw_entry("Scene", 'SCENE_DATA', 'SCENE')
|
||||
draw_entry("World", 'WORLD', 'WORLD')
|
||||
layout.separator()
|
||||
safe_draw(layout, "Object", 'OBJECT_DATAMODE', 'OBJECT')
|
||||
safe_draw(layout, "Modifiers", 'MODIFIER', 'MODIFIER')
|
||||
safe_draw(layout, "Particles", 'PARTICLES', 'PARTICLES')
|
||||
safe_draw(layout, "Physics", 'PHYSICS', 'PHYSICS')
|
||||
safe_draw(layout, "Constraints", 'CONSTRAINT', 'CONSTRAINT')
|
||||
safe_draw(layout, "Object Data", 'OUTLINER_DATA_MESH', 'DATA')
|
||||
safe_draw(layout, "Material", 'MATERIAL', 'MATERIAL')
|
||||
draw_entry("Object", 'OBJECT_DATAMODE', 'OBJECT')
|
||||
draw_entry("Modifiers", 'MODIFIER', 'MODIFIER')
|
||||
draw_entry("Particles", 'PARTICLES', 'PARTICLES')
|
||||
draw_entry("Physics", 'PHYSICS', 'PHYSICS')
|
||||
draw_entry("Constraints", 'CONSTRAINT', 'CONSTRAINT')
|
||||
draw_entry("Object Data", 'OUTLINER_DATA_MESH', 'DATA')
|
||||
draw_entry("Material", 'MATERIAL', 'MATERIAL')
|
||||
layout.separator()
|
||||
safe_draw(layout, "Texture", 'TEXTURE', 'TEXTURE')
|
||||
draw_entry("Texture", 'TEXTURE', 'TEXTURE')
|
||||
|
||||
|
||||
class PIE_MT_editor_switch_image(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_image"
|
||||
bl_label = "Editor Switch: 2D Editors"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Image Viewer", icon="SEQ_PREVIEW"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Image Viewer", icon="SEQ_PREVIEW")
|
||||
op.ui_type = "IMAGE_EDITOR"
|
||||
op.ui_mode = "VIEW"
|
||||
# 6 - RIGHT
|
||||
@@ -420,17 +242,13 @@ class PIE_MT_editor_switch_image(Menu):
|
||||
# 8 - TOP
|
||||
pie.separator()
|
||||
# 7 - TOP - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Image Paint", icon="TPAINT_HLT"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Image Paint", icon="TPAINT_HLT")
|
||||
op.ui_type = "IMAGE_EDITOR"
|
||||
op.ui_mode = "PAINT"
|
||||
# 9 - TOP - RIGHT
|
||||
pie.separator()
|
||||
# 1 - BOTTOM - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Image Masking", icon="MOD_MASK"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Image Masking", icon="MOD_MASK")
|
||||
op.ui_type = "IMAGE_EDITOR"
|
||||
op.ui_mode = "PAINT"
|
||||
# 3 - BOTTOM - RIGHT
|
||||
@@ -440,23 +258,17 @@ class PIE_MT_editor_switch_script(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_script"
|
||||
bl_label = "Editor Switch: Script"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.separator()
|
||||
# 6 - RIGHT
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Text Editor", icon="TEXT"
|
||||
).ui_type = "TEXT_EDITOR"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Text Editor", icon="TEXT").ui_type = "TEXT_EDITOR"
|
||||
# 2 - BOTTOM
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Python Console", icon="CONSOLE"
|
||||
).ui_type = "CONSOLE"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Python Console", icon="CONSOLE").ui_type = "CONSOLE"
|
||||
# 8 - TOP
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Info", icon="INFO"
|
||||
).ui_type = "INFO"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Info", icon="INFO").ui_type = "INFO"
|
||||
# 7 - TOP - LEFT
|
||||
# 9 - TOP - RIGHT
|
||||
# 1 - BOTTOM - LEFT
|
||||
@@ -467,26 +279,20 @@ class PIE_MT_editor_switch_anim(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_anim"
|
||||
bl_label = "Editor Switch: Animation"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Grease Pencil", icon="GREASEPENCIL"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Grease Pencil", icon="GREASEPENCIL")
|
||||
op.ui_type = 'DOPESHEET'
|
||||
op.ui_mode = 'GPENCIL'
|
||||
# 6 - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Dopesheet", icon="ACTION"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Dopesheet", icon="ACTION")
|
||||
op.ui_type = 'DOPESHEET'
|
||||
op.ui_mode = 'DOPESHEET'
|
||||
# 2 - BOTTOM
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Driver Editor", icon="DRIVER"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Driver Editor", icon="DRIVER")
|
||||
op.ui_type = 'DRIVERS'
|
||||
# 8 - TOP
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Timeline", icon="TIME")
|
||||
@@ -494,17 +300,13 @@ class PIE_MT_editor_switch_anim(Menu):
|
||||
# 7 - TOP - LEFT
|
||||
pie.separator()
|
||||
# 9 - TOP - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Graph Editor", icon="GRAPH"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Graph Editor", icon="GRAPH")
|
||||
op.ui_type = 'FCURVES'
|
||||
# 1 - BOTTOM - LEFT
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="NLA Editor", icon="NLA")
|
||||
op.ui_type = 'NLA_EDITOR'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Action Editor", icon="OBJECT_DATAMODE"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Action Editor", icon="OBJECT_DATAMODE")
|
||||
op.ui_type = 'DOPESHEET'
|
||||
op.ui_mode = 'ACTION'
|
||||
|
||||
@@ -525,7 +327,7 @@ class WM_OT_set_area_type(Operator):
|
||||
mode: StringProperty(default="", options={'SKIP_SAVE'})
|
||||
view: StringProperty(default="", options={'SKIP_SAVE'})
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
context.area.ui_type = self.ui_type
|
||||
if self.view_type:
|
||||
context.space_data.view_type = self.view_type
|
||||
@@ -541,10 +343,66 @@ class WM_OT_set_area_type(Operator):
|
||||
context.space_data.mode = self.mode
|
||||
if self.view:
|
||||
context.space_data.view = self.view
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_custom_nodetree_types(context: Context) -> list[tuple[str, str, str]]:
|
||||
"""There are no rules in love, war, and Python scripting.
|
||||
The only way to get custom node tree types is this:
|
||||
- Try to set the UI type to some nonsense
|
||||
- The resulting error tells you the possible UI types
|
||||
- Catch and crunch the error string into a set
|
||||
- Subtract from this the set of built-in UI types
|
||||
- The result is a set of the custom node tree type names
|
||||
- We can get their class with bl_rna_get_subclass_py
|
||||
"""
|
||||
builtin_types = {
|
||||
'VIEW_3D', 'IMAGE_EDITOR', 'UV',
|
||||
'CompositorNodeTree', 'TextureNodeTree', 'GeometryNodeTree', 'ShaderNodeTree',
|
||||
'SEQUENCE_EDITOR', 'CLIP_EDITOR',
|
||||
'DOPESHEET', 'TIMELINE', 'FCURVES', 'DRIVERS', 'NLA_EDITOR',
|
||||
'TEXT_EDITOR', 'CONSOLE', 'INFO',
|
||||
'OUTLINER', 'PROPERTIES', 'FILES', 'ASSETS', 'SPREADSHEET', 'PREFERENCES',
|
||||
}
|
||||
ret = []
|
||||
try:
|
||||
context.area.ui_type = "Nonsense."
|
||||
except Exception as exc:
|
||||
type_list_str = str(exc).split("not found in ")[-1].replace("(", "[").replace(")", "]").replace("'", '"')
|
||||
custom_types = set(json.loads(type_list_str)) - builtin_types
|
||||
for type_name in custom_types:
|
||||
type_class = bpy.types.NodeTree.bl_rna_get_subclass_py(type_name)
|
||||
if not type_class:
|
||||
continue
|
||||
ret.append((type_name, type_class.bl_label, type_class.bl_icon))
|
||||
return ret
|
||||
|
||||
|
||||
def get_available_properties_editors(context: Context) -> set[str]:
|
||||
"""Similar hack as get_custom_nodetree_types.
|
||||
Ideally we need an existing Properties editor, otherwise
|
||||
the list of available types is not up to date.
|
||||
Although even in that case, we still don't get an error,
|
||||
so this is fine.
|
||||
"""
|
||||
org_ui_type = context.area.ui_type
|
||||
try:
|
||||
space = None
|
||||
for area in context.screen.areas:
|
||||
if area.ui_type == 'PROPERTIES':
|
||||
space = area.spaces.active
|
||||
break
|
||||
if not space:
|
||||
context.area.ui_type = 'PROPERTIES'
|
||||
space = context.space_data
|
||||
space.context = "Nonsense."
|
||||
except Exception as exc:
|
||||
type_list_str = str(exc).split("not found in ")[-1].replace("(", "[").replace(")", "]").replace("'", '"')
|
||||
context.area.ui_type = org_ui_type
|
||||
return set(json.loads(type_list_str))
|
||||
return set()
|
||||
|
||||
|
||||
registry = [
|
||||
PIE_MT_editor_switch,
|
||||
PIE_MT_editor_switch_video,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.props import BoolProperty
|
||||
from bpy.types import Context, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class PIE_MT_file(Menu):
|
||||
bl_idname = "PIE_MT_file"
|
||||
bl_label = "File"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
@@ -25,13 +25,9 @@ class PIE_MT_file(Menu):
|
||||
# 8 - TOP
|
||||
pie.menu("TOPBAR_MT_file_import", text="Import...", icon='IMPORT')
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator('wm.call_menu_pie', text="Library...", icon='LINK_BLEND').name = (
|
||||
"PIE_MT_library"
|
||||
)
|
||||
pie.operator('wm.call_menu_pie', text="Library...", icon='LINK_BLEND').name = "PIE_MT_library"
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(
|
||||
"wm.save_mainfile", text="Save Incremental", icon='FILE_TICK'
|
||||
).incremental = True
|
||||
pie.operator("wm.save_mainfile", text="Save Incremental", icon='FILE_TICK').incremental = True
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.menu("TOPBAR_MT_file_open_recent", icon='FILE_FOLDER')
|
||||
# 3 - BOTTOM - RIGHT
|
||||
@@ -42,7 +38,7 @@ class PIE_MT_library(Menu):
|
||||
bl_idname = "PIE_MT_library"
|
||||
bl_label = "Library"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
@@ -50,29 +46,17 @@ class PIE_MT_library(Menu):
|
||||
# 6 - RIGHT
|
||||
pie.operator("wm.append", text="Append", icon='APPEND_BLEND')
|
||||
# 2 - BOTTOM
|
||||
pie.operator(
|
||||
"object.make_local", text="Make All Local", icon='FILE_BLEND'
|
||||
).type = 'ALL'
|
||||
pie.operator("object.make_local", text="Make All Local", icon='FILE_BLEND').type = 'ALL'
|
||||
# 8 - TOP
|
||||
pie.prop(bpy.data, 'use_autopack', text="Auto-pack")
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(
|
||||
"file.pack_res_and_lib", text="Pack All Local", icon='PACKAGE'
|
||||
).pack = True
|
||||
pie.operator("file.pack_res_and_lib", text="Pack All Local", icon='PACKAGE').pack = True
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(
|
||||
"file.pack_res_and_lib", text="Unpack All Local", icon='UGLYPACKAGE'
|
||||
).pack = False
|
||||
pie.operator("file.pack_res_and_lib", text="Unpack All Local", icon='UGLYPACKAGE').pack = False
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(
|
||||
"file.make_paths_relative", icon='FILE', text="Make Paths Relative"
|
||||
)
|
||||
pie.operator("file.make_paths_relative", icon='FILE', text="Make Paths Relative")
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator(
|
||||
"file.make_paths_absolute",
|
||||
icon='LIBRARY_DATA_BROKEN',
|
||||
text="Make Paths Absolute",
|
||||
)
|
||||
pie.operator("file.make_paths_absolute", icon='LIBRARY_DATA_BROKEN', text="Make Paths Absolute")
|
||||
|
||||
|
||||
class WM_OT_pack_res_and_lib(Operator):
|
||||
@@ -84,17 +68,21 @@ class WM_OT_pack_res_and_lib(Operator):
|
||||
|
||||
pack: BoolProperty()
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
if self.pack:
|
||||
bpy.ops.file.pack_libraries()
|
||||
bpy.ops.file.pack_all()
|
||||
else:
|
||||
bpy.ops.file.unpack_libraries()
|
||||
bpy.ops.file.unpack_all()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def draw_revert(self, _context: Context):
|
||||
self.layout.operator('wm.revert_mainfile', icon='LOOP_BACK')
|
||||
self.layout.separator()
|
||||
|
||||
|
||||
registry = [
|
||||
PIE_MT_file,
|
||||
PIE_MT_library,
|
||||
@@ -102,11 +90,6 @@ registry = [
|
||||
]
|
||||
|
||||
|
||||
def draw_revert(self, context):
|
||||
self.layout.operator('wm.revert_mainfile', icon='LOOP_BACK')
|
||||
self.layout.separator()
|
||||
|
||||
|
||||
def register():
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name="Window",
|
||||
@@ -115,7 +98,6 @@ def register():
|
||||
default_fallback_op='wm.save_mainfile',
|
||||
on_drag=True,
|
||||
)
|
||||
|
||||
bpy.types.TOPBAR_MT_file_recover.prepend(draw_revert)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Context, Menu
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
class PIE_MT_keyframe_type(Menu):
|
||||
bl_idname = "PIE_MT_keyframe_type"
|
||||
bl_label = "Keyframe Type"
|
||||
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator('action.keyframe_type', text="Keyframe", icon='KEYTYPE_KEYFRAME_VEC').type = 'KEYFRAME'
|
||||
# 6 - RIGHT
|
||||
pie.operator('action.keyframe_type', text="Extreme", icon='KEYTYPE_EXTREME_VEC').type = 'EXTREME'
|
||||
# 2 - BOTTOM
|
||||
pie.operator('action.keyframe_type', text="Breakdown", icon='KEYTYPE_BREAKDOWN_VEC').type = 'BREAKDOWN'
|
||||
# 8 - TOP
|
||||
pie.operator('action.keyframe_type', text="Moving Hold", icon='KEYTYPE_MOVING_HOLD_VEC').type = 'MOVING_HOLD'
|
||||
# 7 - TOP - LEFT
|
||||
pie.separator()
|
||||
# 9 - TOP - RIGHT
|
||||
pie.separator()
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator('action.keyframe_type', text="Generated", icon='KEYTYPE_GENERATED_VEC').type = 'GENERATED'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator('action.keyframe_type', text="Jitter", icon='KEYTYPE_JITTER_VEC').type = 'JITTER'
|
||||
|
||||
|
||||
class PIE_MT_handle_type(Menu):
|
||||
bl_idname = "PIE_MT_handle_type"
|
||||
bl_label = "Handle Type"
|
||||
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
base = 'graph' if context.area.type == 'GRAPH_EDITOR' else 'action'
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator(f'{base}.handle_type', text="Free", icon='HANDLE_FREE').type = 'FREE'
|
||||
# 6 - RIGHT
|
||||
pie.operator(f'{base}.handle_type', text="Aligned", icon='HANDLE_ALIGNED').type = 'ALIGNED'
|
||||
# 2 - BOTTOM
|
||||
pie.operator(f'{base}.handle_type', text="Vector", icon='HANDLE_VECTOR').type = 'VECTOR'
|
||||
# 8 - TOP
|
||||
pie.operator(f'{base}.handle_type', text="Automatic", icon='HANDLE_AUTO').type = 'AUTO'
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(f'{base}.handle_type', text="Auto Clamped", icon='HANDLE_AUTOCLAMPED').type = 'AUTO_CLAMPED'
|
||||
|
||||
|
||||
class PIE_MT_interpolation_mode(Menu):
|
||||
bl_idname = "PIE_MT_interpolation_mode"
|
||||
bl_label = "Interpolation Mode"
|
||||
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
base = 'graph' if context.area.type == 'GRAPH_EDITOR' else 'action'
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator(f'{base}.interpolation_type', text="Constant", icon='IPO_CONSTANT').type = 'CONSTANT'
|
||||
# 6 - RIGHT
|
||||
pie.operator(f'{base}.interpolation_type', text="Bezier", icon='IPO_BEZIER').type = 'BEZIER'
|
||||
# 2 - BOTTOM
|
||||
pie.separator()
|
||||
# 8 - TOP
|
||||
pie.operator(f'{base}.interpolation_type', text="Linear", icon='IPO_LINEAR').type = 'LINEAR'
|
||||
# 7 - TOP - LEFT
|
||||
pie.separator()
|
||||
# 9 - TOP - RIGHT
|
||||
pie.menu("PIE_MT_interpolation_dynamic", text="Dynamic...", icon='IPO_BACK')
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.separator()
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.menu("PIE_MT_interpolation_easing", text="Easing...", icon='IPO_SINE')
|
||||
|
||||
|
||||
class PIE_MT_interpolation_easing(Menu):
|
||||
bl_idname = "PIE_MT_interpolation_easing"
|
||||
bl_label = "Easing Interpolation"
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
base = 'graph' if context.area.type == 'GRAPH_EDITOR' else 'action'
|
||||
layout.operator(f'{base}.interpolation_type', text="Sinusoidal", icon='IPO_SINE').type = 'SINE'
|
||||
layout.operator(f'{base}.interpolation_type', text="Quadratic", icon='IPO_QUAD').type = 'QUAD'
|
||||
layout.operator(f'{base}.interpolation_type', text="Cubic", icon='IPO_CUBIC').type = 'CUBIC'
|
||||
layout.operator(f'{base}.interpolation_type', text="Quartic", icon='IPO_QUART').type = 'QUART'
|
||||
layout.operator(f'{base}.interpolation_type', text="Quintic", icon='IPO_QUINT').type = 'QUINT'
|
||||
layout.operator(f'{base}.interpolation_type', text="Exponential", icon='IPO_EXPO').type = 'EXPO'
|
||||
layout.operator(f'{base}.interpolation_type', text="Circular", icon='IPO_CIRC').type = 'CIRC'
|
||||
|
||||
|
||||
class PIE_MT_interpolation_dynamic(Menu):
|
||||
bl_idname = "PIE_MT_interpolation_dynamic"
|
||||
bl_label = "Dynamic Effects"
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
|
||||
base = 'graph' if context.area.type == 'GRAPH_EDITOR' else 'action'
|
||||
|
||||
layout.operator(f'{base}.interpolation_type', text="Back", icon='IPO_BACK').type = 'BACK'
|
||||
layout.operator(f'{base}.interpolation_type', text="Bounce", icon='IPO_BOUNCE').type = 'BOUNCE'
|
||||
layout.operator(f'{base}.interpolation_type', text="Elastic", icon='IPO_ELASTIC').type = 'ELASTIC'
|
||||
|
||||
|
||||
class PIE_MT_easing_mode(Menu):
|
||||
bl_idname = "PIE_MT_easing_mode"
|
||||
bl_label = "Easing Mode"
|
||||
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
base = 'graph' if context.area.type == 'GRAPH_EDITOR' else 'action'
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator(f'{base}.easing_type', text="Ease In", icon='IPO_EASE_IN').type = 'EASE_IN'
|
||||
# 6 - RIGHT
|
||||
pie.operator(f'{base}.easing_type', text="Ease Out", icon='IPO_EASE_OUT').type = 'EASE_OUT'
|
||||
# 2 - BOTTOM
|
||||
pie.operator(f'{base}.easing_type', text="Ease In and Out", icon='IPO_EASE_IN_OUT').type = 'EASE_IN_OUT'
|
||||
# 8 - TOP
|
||||
pie.operator(f'{base}.easing_type', text="Automatic", icon='AUTO').type = 'AUTO'
|
||||
|
||||
|
||||
class PIE_MT_extrapolation_mode(Menu):
|
||||
bl_idname = "PIE_MT_extrapolation_mode"
|
||||
bl_label = "Extrapolation Mode"
|
||||
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator('graph.extrapolation_type', text="Constant", icon='IPO_CONSTANT').type = 'CONSTANT'
|
||||
# 6 - RIGHT
|
||||
pie.operator('graph.extrapolation_type', text="Linear", icon='IPO_LINEAR').type = 'LINEAR'
|
||||
# 2 - BOTTOM
|
||||
pie.operator('graph.extrapolation_type', text="Clear Cyclic", icon='X').type = 'CLEAR_CYCLIC'
|
||||
# 8 - TOP
|
||||
pie.operator('graph.extrapolation_type', text="Make Cyclic", icon='FILE_REFRESH').type = 'MAKE_CYCLIC'
|
||||
|
||||
|
||||
registry = [
|
||||
PIE_MT_keyframe_type,
|
||||
PIE_MT_handle_type,
|
||||
PIE_MT_interpolation_mode,
|
||||
PIE_MT_interpolation_easing,
|
||||
PIE_MT_interpolation_dynamic,
|
||||
PIE_MT_easing_mode,
|
||||
PIE_MT_extrapolation_mode,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name='Dopesheet',
|
||||
pie_name=PIE_MT_keyframe_type.bl_idname,
|
||||
hotkey_kwargs={'type': "R", 'value': "PRESS"},
|
||||
on_drag=False,
|
||||
)
|
||||
for keymap in ('Dopesheet', 'Graph Editor'):
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name=keymap,
|
||||
pie_name=PIE_MT_handle_type.bl_idname,
|
||||
hotkey_kwargs={'type': "V", 'value': "PRESS"},
|
||||
on_drag=False,
|
||||
)
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name=keymap,
|
||||
pie_name=PIE_MT_interpolation_mode.bl_idname,
|
||||
hotkey_kwargs={'type': "T", 'value': "PRESS"},
|
||||
on_drag=False,
|
||||
)
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name=keymap,
|
||||
pie_name=PIE_MT_easing_mode.bl_idname,
|
||||
hotkey_kwargs={'type': "E", 'value': "PRESS", 'ctrl': True},
|
||||
on_drag=False,
|
||||
)
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name=keymap,
|
||||
pie_name=PIE_MT_extrapolation_mode.bl_idname,
|
||||
hotkey_kwargs={'type': "E", 'value': "PRESS", 'shift': True},
|
||||
on_drag=False,
|
||||
)
|
||||
@@ -1,9 +1,9 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.props import EnumProperty, BoolProperty
|
||||
from bpy.props import BoolProperty, EnumProperty
|
||||
from bpy.types import Context, Event, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
@@ -12,10 +12,9 @@ class PIE_MT_manipulator(Menu):
|
||||
bl_idname = "PIE_MT_manipulator"
|
||||
bl_label = "Manipulator"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
|
||||
space = context.space_data
|
||||
|
||||
# 4 - LEFT
|
||||
@@ -27,25 +26,31 @@ class PIE_MT_manipulator(Menu):
|
||||
'view3d.set_manipulator', text="Scale", icon='CON_SIZELIKE', depress=space.show_gizmo_object_scale
|
||||
).manipulator = 'SCALE'
|
||||
# 2 - BOTTOM
|
||||
pie.operator(
|
||||
'view3d.set_manipulator', text="None", icon='X'
|
||||
).manipulator='NONE'
|
||||
pie.operator('view3d.set_manipulator', text="None", icon='X').manipulator = 'NONE'
|
||||
# 8 - TOP
|
||||
pie.operator(
|
||||
'view3d.set_manipulator', text="Location", icon='CON_LOCLIKE', depress=space.show_gizmo_object_translate
|
||||
).manipulator = 'LOC'
|
||||
|
||||
# 7 - TOP-LEFT
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(
|
||||
'view3d.set_manipulator', text="Loc/Rot", icon='CON_LOCLIKE', depress=space.show_gizmo_object_translate and space.show_gizmo_object_rotate
|
||||
).manipulator='LOCROT'
|
||||
# 9 - TOP-RIGHT
|
||||
'view3d.set_manipulator',
|
||||
text="Loc/Rot",
|
||||
icon='CON_LOCLIKE',
|
||||
depress=space.show_gizmo_object_translate and space.show_gizmo_object_rotate,
|
||||
).manipulator = 'LOCROT'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(
|
||||
'view3d.set_manipulator', text="Loc/Scale", icon='CON_SIZELIKE', depress=space.show_gizmo_object_translate and space.show_gizmo_object_scale
|
||||
).manipulator='LOCSCALE'
|
||||
# 1 - BOT-LEFT
|
||||
'view3d.set_manipulator',
|
||||
text="Loc/Scale",
|
||||
icon='CON_SIZELIKE',
|
||||
depress=space.show_gizmo_object_translate and space.show_gizmo_object_scale,
|
||||
).manipulator = 'LOCSCALE'
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(
|
||||
'view3d.set_manipulator', text="Loc/Rot/Scale", icon='CON_LOCLIKE', depress=space.show_gizmo_object_translate and space.show_gizmo_object_rotate and space.show_gizmo_object_scale
|
||||
'view3d.set_manipulator',
|
||||
text="Loc/Rot/Scale",
|
||||
icon='CON_LOCLIKE',
|
||||
depress=space.show_gizmo_object_translate and space.show_gizmo_object_rotate and space.show_gizmo_object_scale,
|
||||
).manipulator = 'LOCROTSCALE'
|
||||
|
||||
|
||||
@@ -75,19 +80,19 @@ class VIEW3D_OT_set_manipulator(Operator):
|
||||
name="Toggle",
|
||||
description="Hold Shift to toggle the selected manipulator rather than setting it as the only active one",
|
||||
default=False,
|
||||
options={'SKIP_SAVE'}
|
||||
options={'SKIP_SAVE'},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
space = context.space_data
|
||||
return space and space.type == 'VIEW_3D'
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: Context, event: Event):
|
||||
self.toggle = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
space = context.space_data
|
||||
|
||||
if self.manipulator != 'NONE':
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Menu
|
||||
from bpy.types import Context, Menu
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
@@ -11,39 +11,29 @@ class PIE_MT_mesh_delete(Menu):
|
||||
bl_idname = "PIE_MT_mesh_delete"
|
||||
bl_label = "Mesh Delete"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
box = pie.split().column()
|
||||
box.operator(
|
||||
"mesh.dissolve_limited", text="Limited Dissolve", icon='STICKY_UVS_LOC'
|
||||
)
|
||||
box.operator("mesh.dissolve_limited", text="Limited Dissolve", icon='STICKY_UVS_LOC')
|
||||
box.operator("mesh.delete_edgeloop", text="Delete Edge Loops", icon='NONE')
|
||||
box.operator("mesh.edge_collapse", text="Edge Collapse", icon='UV_EDGESEL')
|
||||
# 6 - RIGHT
|
||||
box = pie.split().column()
|
||||
box.operator("mesh.remove_doubles", text="Merge By Distance", icon='NONE')
|
||||
box.operator("mesh.delete", text="Only Edge & Faces", icon='NONE').type = (
|
||||
'EDGE_FACE'
|
||||
)
|
||||
box.operator("mesh.delete", text="Only Faces", icon='UV_FACESEL').type = (
|
||||
'ONLY_FACE'
|
||||
)
|
||||
box.operator("mesh.delete", text="Only Edge & Faces", icon='NONE').type = 'EDGE_FACE'
|
||||
box.operator("mesh.delete", text="Only Faces", icon='UV_FACESEL').type = 'ONLY_FACE'
|
||||
# 2 - BOTTOM
|
||||
pie.operator("mesh.dissolve_edges", text="Dissolve Edges", icon='SNAP_EDGE')
|
||||
# 8 - TOP
|
||||
pie.operator("mesh.delete", text="Delete Edges", icon='EDGESEL').type = 'EDGE'
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator("mesh.delete", text="Delete Vertices", icon='VERTEXSEL').type = (
|
||||
'VERT'
|
||||
)
|
||||
pie.operator("mesh.delete", text="Delete Vertices", icon='VERTEXSEL').type = 'VERT'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator("mesh.delete", text="Delete Faces", icon='FACESEL').type = 'FACE'
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(
|
||||
"mesh.dissolve_verts", text="Dissolve Vertices", icon='SNAP_VERTEX'
|
||||
)
|
||||
pie.operator("mesh.dissolve_verts", text="Dissolve Vertices", icon='SNAP_VERTEX')
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator("mesh.dissolve_faces", text="Dissolve Faces", icon='SNAP_FACE')
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
from bmesh.types import BMEdge
|
||||
from bpy.types import Context, Menu, Object, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
# Above this edge count the state check is skipped to avoid poll lag.
|
||||
_EDGE_POLL_LIMIT = 100_000
|
||||
|
||||
|
||||
class PIE_MT_mesh_edge(Menu):
|
||||
bl_idname = "PIE_MT_mesh_edge"
|
||||
bl_label = "Mesh Edge"
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
pie.operator(MESH_OT_pie_clear_seam.bl_idname, text="Clear Seam", icon='EDGE_SEAM')
|
||||
# 6 - RIGHT
|
||||
pie.operator(MESH_OT_pie_mark_seam.bl_idname, text="Mark Seam", icon='EDGE_SEAM')
|
||||
# 2 - BOTTOM
|
||||
pie.operator(MESH_OT_pie_clear_sharp.bl_idname, text="Clear Sharp", icon='EDGE_SHARP')
|
||||
# 8 - TOP
|
||||
pie.operator(MESH_OT_pie_mark_sharp.bl_idname, text="Mark Sharp", icon='EDGE_SHARP')
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(MESH_OT_pie_clear_freestyle_edge.bl_idname, text="Clear Freestyle Edge", icon='EDGESEL')
|
||||
# 9 - TOP - RIGHT
|
||||
if context.tool_settings.mesh_select_mode[0]:
|
||||
pie.operator("transform.vert_crease", text="Vertex Crease", icon='VERTEX_CREASE')
|
||||
else:
|
||||
pie.operator("transform.edge_crease", text="Edge Crease", icon='EDGE_CREASE')
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(MESH_OT_pie_mark_freestyle_edge.bl_idname, text="Mark Freestyle Edge", icon='EDGESEL')
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator("transform.edge_bevelweight", text="Edge Bevel Weight", icon='EDGE_BEVEL')
|
||||
|
||||
|
||||
class _EdgeMarkBase(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
edge_data_type: str
|
||||
do_clear: bool
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
obj = context.active_object
|
||||
if not (obj and obj.type == 'MESH' and obj.mode == 'EDIT'):
|
||||
cls.poll_message_set("Must be in Mesh Edit mode")
|
||||
return False
|
||||
if len(obj.data.edges) > _EDGE_POLL_LIMIT:
|
||||
return True
|
||||
any_have, all_have = selected_edge_data_state(obj, cls.edge_data_type)
|
||||
if not any_have and all_have:
|
||||
cls.poll_message_set("No edges selected")
|
||||
return False
|
||||
if cls.do_clear:
|
||||
if not any_have:
|
||||
cls.poll_message_set("No selected edges are marked")
|
||||
return False
|
||||
else:
|
||||
if all_have:
|
||||
cls.poll_message_set("All selected edges are already marked")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, _context: Context):
|
||||
if self.edge_data_type == 'SEAM':
|
||||
bpy.ops.mesh.mark_seam(clear=self.do_clear)
|
||||
elif self.edge_data_type == 'SHARP':
|
||||
bpy.ops.mesh.mark_sharp(clear=self.do_clear)
|
||||
elif self.edge_data_type == 'FREESTYLE':
|
||||
bpy.ops.mesh.mark_freestyle_edge(clear=self.do_clear)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class MESH_OT_pie_mark_seam(_EdgeMarkBase):
|
||||
bl_idname = "mesh.pie_mark_seam"
|
||||
bl_label = "Mark Seam"
|
||||
bl_description = "Mark selected edges as seams for UV unwrapping"
|
||||
edge_data_type = 'SEAM'
|
||||
do_clear = False
|
||||
|
||||
|
||||
class MESH_OT_pie_clear_seam(_EdgeMarkBase):
|
||||
bl_idname = "mesh.pie_clear_seam"
|
||||
bl_label = "Clear Seam"
|
||||
bl_description = "Remove seam marks from selected edges"
|
||||
edge_data_type = 'SEAM'
|
||||
do_clear = True
|
||||
|
||||
|
||||
class MESH_OT_pie_mark_sharp(_EdgeMarkBase):
|
||||
bl_idname = "mesh.pie_mark_sharp"
|
||||
bl_label = "Mark Sharp"
|
||||
bl_description = "Mark selected edges as sharp"
|
||||
edge_data_type = 'SHARP'
|
||||
do_clear = False
|
||||
|
||||
|
||||
class MESH_OT_pie_clear_sharp(_EdgeMarkBase):
|
||||
bl_idname = "mesh.pie_clear_sharp"
|
||||
bl_label = "Clear Sharp"
|
||||
bl_description = "Remove sharp marks from selected edges"
|
||||
edge_data_type = 'SHARP'
|
||||
do_clear = True
|
||||
|
||||
|
||||
class MESH_OT_pie_mark_freestyle_edge(_EdgeMarkBase):
|
||||
bl_idname = "mesh.pie_mark_freestyle_edge"
|
||||
bl_label = "Mark Freestyle Edge"
|
||||
bl_description = "Mark selected edges as Freestyle edges"
|
||||
edge_data_type = 'FREESTYLE'
|
||||
do_clear = False
|
||||
|
||||
|
||||
class MESH_OT_pie_clear_freestyle_edge(_EdgeMarkBase):
|
||||
bl_idname = "mesh.pie_clear_freestyle_edge"
|
||||
bl_label = "Clear Freestyle Edge"
|
||||
bl_description = "Remove Freestyle marks from selected edges"
|
||||
edge_data_type = 'FREESTYLE'
|
||||
do_clear = True
|
||||
|
||||
|
||||
def _edge_has_data(edge: BMEdge, edge_data_type: str, freestyle_layer) -> bool:
|
||||
if edge_data_type == 'SEAM':
|
||||
return edge.seam
|
||||
if edge_data_type == 'SHARP':
|
||||
return not edge.smooth
|
||||
if edge_data_type == 'FREESTYLE':
|
||||
return edge[freestyle_layer] if freestyle_layer else False
|
||||
return False
|
||||
|
||||
|
||||
def selected_edge_data_state(obj: Object, edge_data_type: str) -> tuple[bool, bool]:
|
||||
"""Return (any_have, all_have) for selected edges of the given data type."""
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
freestyle_layer = bm.edges.layers.bool.get("freestyle_edge") if edge_data_type == 'FREESTYLE' else None
|
||||
|
||||
any_have = False
|
||||
all_have = True
|
||||
found = False
|
||||
for edge in bm.edges:
|
||||
if not edge.select:
|
||||
continue
|
||||
found = True
|
||||
has = _edge_has_data(edge, edge_data_type, freestyle_layer)
|
||||
any_have |= has
|
||||
all_have &= has
|
||||
if any_have and not all_have:
|
||||
break # Both states seen, result can't change.
|
||||
|
||||
if not found:
|
||||
return False, False
|
||||
return any_have, all_have
|
||||
|
||||
|
||||
registry = [
|
||||
PIE_MT_mesh_edge,
|
||||
MESH_OT_pie_mark_seam,
|
||||
MESH_OT_pie_clear_seam,
|
||||
MESH_OT_pie_mark_sharp,
|
||||
MESH_OT_pie_clear_sharp,
|
||||
MESH_OT_pie_mark_freestyle_edge,
|
||||
MESH_OT_pie_clear_freestyle_edge,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name="Mesh",
|
||||
pie_name=PIE_MT_mesh_edge.bl_idname,
|
||||
hotkey_kwargs={'type': "E", 'value': "PRESS", 'shift': True},
|
||||
on_drag=True,
|
||||
)
|
||||
@@ -1,10 +1,10 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.props import EnumProperty
|
||||
from bpy.types import Context, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
@@ -13,7 +13,7 @@ class PIE_MT_mesh_flatten(Menu):
|
||||
bl_idname = "PIE_MT_mesh_flatten"
|
||||
bl_label = "Mesh Flatten"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
@@ -65,11 +65,11 @@ class TRANSFORM_OT_flatten_to_center(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
values = {
|
||||
'X': [(0, 1, 1), (True, False, False)],
|
||||
'Y': [(1, 0, 1), (False, True, False)],
|
||||
@@ -106,11 +106,11 @@ class TRANSFORM_OT_flatten_to_object_origin(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
index = "XYZ".find(self.axis)
|
||||
for vert in bpy.context.object.data.vertices:
|
||||
@@ -149,11 +149,11 @@ class TRANSFORM_OT_flatten_to_selection_bounding_box(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
count = 0
|
||||
axis_idx = "XYZ".find(self.axis)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Menu
|
||||
from bpy.types import Context, Menu
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
@@ -11,16 +11,14 @@ class PIE_MT_mesh_merge(Menu):
|
||||
bl_idname = "PIE_MT_mesh_merge"
|
||||
bl_label = "Mesh Merge"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator('mesh.remove_doubles', text="By Distance", icon='PROP_ON')
|
||||
|
||||
# 6 - RIGHT
|
||||
pie.operator('mesh.merge', text="At Center", icon='SNAP_FACE_CENTER').type = (
|
||||
'CENTER'
|
||||
)
|
||||
pie.operator('mesh.merge', text="At Center", icon='SNAP_FACE_CENTER').type = 'CENTER'
|
||||
|
||||
# 2 - BOTTOM
|
||||
op = pie.operator('mesh.merge', text="Collapse", icon='FULLSCREEN_EXIT')
|
||||
@@ -37,13 +35,9 @@ class PIE_MT_mesh_merge(Menu):
|
||||
# This will raise an error if the option isn't available.
|
||||
op.type = 'FIRST'
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(
|
||||
'mesh.merge', text="At First", icon='TRACKING_REFINE_BACKWARDS'
|
||||
).type = 'FIRST'
|
||||
pie.operator('mesh.merge', text="At First", icon='TRACKING_REFINE_BACKWARDS').type = 'FIRST'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(
|
||||
'mesh.merge', text="At Last", icon='TRACKING_REFINE_FORWARDS'
|
||||
).type = 'LAST'
|
||||
pie.operator('mesh.merge', text="At Last", icon='TRACKING_REFINE_FORWARDS').type = 'LAST'
|
||||
except:
|
||||
op.type = 'COLLAPSE'
|
||||
pie.separator()
|
||||
@@ -52,9 +46,7 @@ class PIE_MT_mesh_merge(Menu):
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.separator()
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator('mesh.merge', text="At 3D Cursor", icon='PIVOT_CURSOR').type = (
|
||||
'CURSOR'
|
||||
)
|
||||
pie.operator('mesh.merge', text="At 3D Cursor", icon='PIVOT_CURSOR').type = 'CURSOR'
|
||||
|
||||
|
||||
registry = [PIE_MT_mesh_merge]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# Requested by users here: https://projects.blender.org/extensions/space_view3d_pie_menus/issues/67
|
||||
|
||||
from bpy.types import Menu
|
||||
from bpy.types import Context, Menu
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
@@ -12,7 +13,7 @@ class PIE_MT_object_add(Menu):
|
||||
bl_idname = "PIE_MT_object_add"
|
||||
bl_label = "Add Object"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.types import Context, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
@@ -12,10 +13,10 @@ class PIE_MT_object_display(Menu):
|
||||
bl_label = "Object Display"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
return context.active_object
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
obj = context.active_object
|
||||
|
||||
@@ -37,7 +38,7 @@ class PIE_MT_object_display(Menu):
|
||||
box.label(text="Color")
|
||||
row = box.row(align=True)
|
||||
row.prop(obj, 'color', text="")
|
||||
row.operator('view3d.copy_property_to_selected', text="", icon='LOOP_FORWARDS').rna_path='color'
|
||||
row.operator('view3d.copy_property_to_selected', text="", icon='LOOP_FORWARDS').rna_path = 'color'
|
||||
else:
|
||||
pie.separator()
|
||||
|
||||
@@ -95,7 +96,7 @@ class OBJECT_OT_add_weighted_normals(Operator):
|
||||
bl_label = "Weighted Normals"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
@@ -113,7 +114,7 @@ class OBJECT_OT_reset_normals(Operator):
|
||||
bl_label = "Reset Normals"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.props import BoolProperty, EnumProperty
|
||||
from bpy.types import Context, Menu, Operator, OperatorProperties
|
||||
from mathutils import Matrix
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
@@ -15,10 +15,10 @@ class OBJECT_MT_parenting_pie(Menu):
|
||||
bl_idname = 'OBJECT_MT_parenting_pie'
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
return context.mode == 'OBJECT'
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
|
||||
@@ -64,7 +64,7 @@ class OBJECT_MT_parenting_pie(Menu):
|
||||
pie.separator()
|
||||
|
||||
|
||||
def selected_objs_with_parents(context):
|
||||
def selected_objs_with_parents(context: Context):
|
||||
return [ob for ob in context.selected_objects if ob.parent]
|
||||
|
||||
|
||||
@@ -81,21 +81,21 @@ class OBJECT_OT_clear_parent(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
def description(cls, _context: Context, properties: OperatorProperties):
|
||||
if properties.keep_transform:
|
||||
return "Clear the parent of selected objects, while preserving their position in space"
|
||||
else:
|
||||
return "Clear the parent of selected objects, without affecting their Loc/Rot/Scale values. This may cause the now parentless children to change position"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
if not selected_objs_with_parents(context):
|
||||
cls.poll_message_set("No selected objects have parents.")
|
||||
return False
|
||||
|
||||
return set_parent_poll_check_linked(cls, context)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
objs = selected_objs_with_parents(context)
|
||||
|
||||
op_type = 'CLEAR_KEEP_TRANSFORM' if self.keep_transform else 'CLEAR'
|
||||
@@ -115,22 +115,20 @@ class OBJECT_OT_clear_parent_inverse_matrix(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
objs_with_parents = selected_objs_with_parents(context)
|
||||
if not objs_with_parents:
|
||||
cls.poll_message_set("No selected objects have parents.")
|
||||
return False
|
||||
|
||||
identity_matrix = Matrix.Identity(4)
|
||||
if not any(
|
||||
[obj.matrix_parent_inverse != identity_matrix for obj in objs_with_parents]
|
||||
):
|
||||
if not any([obj.matrix_parent_inverse != identity_matrix for obj in objs_with_parents]):
|
||||
cls.poll_message_set("No selected objects have a parenting offset set.")
|
||||
return False
|
||||
|
||||
return set_parent_poll_check_linked(cls, context)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
objs = selected_objs_with_parents(context)
|
||||
bpy.ops.object.parent_clear(type='CLEAR_INVERSE')
|
||||
|
||||
@@ -148,16 +146,14 @@ class OBJECT_OT_parent_set_simple(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
if not len(context.selected_objects) > 1 and context.active_object:
|
||||
cls.poll_message_set(
|
||||
"Only one object is selected. You can't parent an object to itself."
|
||||
)
|
||||
cls.poll_message_set("Only one object is selected. You can't parent an object to itself.")
|
||||
return False
|
||||
|
||||
return set_parent_poll_check_linked(cls, context)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
parent_ob = context.active_object
|
||||
objs_to_parent = [obj for obj in context.selected_objects if obj != parent_ob]
|
||||
|
||||
@@ -168,11 +164,7 @@ class OBJECT_OT_parent_set_simple(Operator):
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Report what was done.
|
||||
objs_str = (
|
||||
objs_to_parent[0].name
|
||||
if len(objs_to_parent) == 1
|
||||
else f"{len(objs_to_parent)} objects"
|
||||
)
|
||||
objs_str = objs_to_parent[0].name if len(objs_to_parent) == 1 else f"{len(objs_to_parent)} objects"
|
||||
self.report({'INFO'}, f"Parented {objs_str} to {parent_ob.name}")
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -185,16 +177,14 @@ class OBJECT_OT_parent_set_advanced(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
if not len(context.selected_objects) > 1 and context.active_object:
|
||||
cls.poll_message_set(
|
||||
"Only one object is selected. You can't parent an object to itself."
|
||||
)
|
||||
cls.poll_message_set("Only one object is selected. You can't parent an object to itself.")
|
||||
return False
|
||||
|
||||
return set_parent_poll_check_linked(cls, context)
|
||||
|
||||
def get_parent_method_items(self, context):
|
||||
def get_parent_method_items(self, context: Context):
|
||||
parent_ob = context.active_object
|
||||
items = [
|
||||
(
|
||||
@@ -273,9 +263,7 @@ class OBJECT_OT_parent_set_advanced(Operator):
|
||||
)
|
||||
)
|
||||
|
||||
return [
|
||||
(item[0], item[1], item[2], item[3], idx) for idx, item in enumerate(items)
|
||||
]
|
||||
return [(item[0], item[1], item[2], item[3], idx) for idx, item in enumerate(items)]
|
||||
|
||||
parent_method: EnumProperty(
|
||||
name="Parent Method",
|
||||
@@ -428,9 +416,7 @@ class OBJECT_OT_parent_set_advanced(Operator):
|
||||
keep_transform = self.transform_correction == 'MATRIX_INTERNAL'
|
||||
|
||||
objs_to_parent = [obj for obj in context.selected_objects if obj != parent_ob]
|
||||
matrix_backups = [
|
||||
(obj.matrix_world.copy(), obj.matrix_local.copy()) for obj in objs_to_parent
|
||||
]
|
||||
matrix_backups = [(obj.matrix_world.copy(), obj.matrix_local.copy()) for obj in objs_to_parent]
|
||||
|
||||
op_parent_method = self.parent_method
|
||||
|
||||
@@ -459,9 +445,7 @@ class OBJECT_OT_parent_set_advanced(Operator):
|
||||
return self.parent_with_copy_transforms_con(context)
|
||||
|
||||
try:
|
||||
bpy.ops.object.parent_set(
|
||||
type=op_parent_method, keep_transform=keep_transform
|
||||
)
|
||||
bpy.ops.object.parent_set(type=op_parent_method, keep_transform=keep_transform)
|
||||
except Exception as exc:
|
||||
self.report({'ERROR'}, str(exc))
|
||||
return {'CANCELLED'}
|
||||
@@ -477,11 +461,7 @@ class OBJECT_OT_parent_set_advanced(Operator):
|
||||
obj.matrix_world = matrices[0]
|
||||
|
||||
# Report what was done.
|
||||
objs_str = (
|
||||
objs_to_parent[0].name
|
||||
if len(objs_to_parent) == 1
|
||||
else f"{len(objs_to_parent)} objects"
|
||||
)
|
||||
objs_str = objs_to_parent[0].name if len(objs_to_parent) == 1 else f"{len(objs_to_parent)} objects"
|
||||
self.report({'INFO'}, f"Parented {objs_str} to {parent_ob.name}")
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -515,9 +495,7 @@ class OBJECT_OT_parent_set_advanced(Operator):
|
||||
target.subtarget = active_bone.name
|
||||
|
||||
# Draw a nice info message.
|
||||
objs_str = (
|
||||
obj.name if len(objs_to_parent) == 1 else f"{len(objs_to_parent)} objects"
|
||||
)
|
||||
objs_str = obj.name if len(objs_to_parent) == 1 else f"{len(objs_to_parent)} objects"
|
||||
self.report({'INFO'}, f"Constrained {objs_str} to {active_bone.name}")
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -543,21 +521,13 @@ class OBJECT_OT_parent_set_advanced(Operator):
|
||||
childof.target = parent_ob
|
||||
if context.active_pose_bone:
|
||||
childof.subtarget = context.active_pose_bone.name
|
||||
childof.inverse_matrix = (
|
||||
parent_ob.matrix_world @ context.active_pose_bone.matrix
|
||||
).inverted()
|
||||
childof.inverse_matrix = (parent_ob.matrix_world @ context.active_pose_bone.matrix).inverted()
|
||||
else:
|
||||
childof.inverse_matrix = parent_ob.matrix_world.inverted()
|
||||
|
||||
# Draw a nice info message.
|
||||
objs_str = (
|
||||
obj.name if len(objs_to_parent) == 1 else f"{len(objs_to_parent)} objects"
|
||||
)
|
||||
parent_str = (
|
||||
context.active_pose_bone.name
|
||||
if context.active_pose_bone
|
||||
else parent_ob.name
|
||||
)
|
||||
objs_str = obj.name if len(objs_to_parent) == 1 else f"{len(objs_to_parent)} objects"
|
||||
parent_str = context.active_pose_bone.name if context.active_pose_bone else parent_ob.name
|
||||
self.report({'INFO'}, f"Constrained {objs_str} to {parent_str}")
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -573,23 +543,15 @@ class OBJECT_OT_parent_set_advanced(Operator):
|
||||
copytrans.subtarget = context.active_pose_bone.name
|
||||
|
||||
# Draw a nice info message.
|
||||
objs_str = (
|
||||
obj.name if len(objs_to_parent) == 1 else f"{len(objs_to_parent)} objects"
|
||||
)
|
||||
parent_str = (
|
||||
context.active_pose_bone.name
|
||||
if context.active_pose_bone
|
||||
else parent_ob.name
|
||||
)
|
||||
objs_str = obj.name if len(objs_to_parent) == 1 else f"{len(objs_to_parent)} objects"
|
||||
parent_str = context.active_pose_bone.name if context.active_pose_bone else parent_ob.name
|
||||
self.report({'INFO'}, f"Constrained {objs_str} to {parent_str}")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def set_parent_poll_check_linked(cls, context):
|
||||
def set_parent_poll_check_linked(cls, context: Context):
|
||||
if any([ob.library for ob in context.selected_objects]):
|
||||
cls.poll_message_set(
|
||||
"An object is linked. You need to override it to change the parenting."
|
||||
)
|
||||
cls.poll_message_set("An object is linked. You need to override it to change the parenting.")
|
||||
return False
|
||||
if any([ob.override_library and ob.override_library.is_system_override for ob in context.selected_objects]):
|
||||
cls.poll_message_set(
|
||||
@@ -598,8 +560,9 @@ def set_parent_poll_check_linked(cls, context):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
### Header Menu UI
|
||||
def draw_new_header_menu(self, context):
|
||||
def draw_new_header_menu(self, _context: Context):
|
||||
layout = self.layout
|
||||
|
||||
# Set Parent
|
||||
|
||||
+12
-17
@@ -1,16 +1,17 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Menu
|
||||
from bpy.types import Context, Menu
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
class PIE_MT_animation(Menu):
|
||||
bl_idname = "PIE_MT_animation"
|
||||
bl_label = "Animation"
|
||||
class PIE_MT_playback(Menu):
|
||||
bl_idname = "PIE_MT_playback"
|
||||
bl_label = "Playback"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
@@ -18,22 +19,16 @@ class PIE_MT_animation(Menu):
|
||||
# 6 - RIGHT
|
||||
pie.operator("screen.frame_jump", text="Jump to End", icon='FF').end = True
|
||||
# 2 - BOTTOM
|
||||
pie.operator(
|
||||
"screen.animation_play", text="Play Reverse", icon='PLAY_REVERSE'
|
||||
).reverse = True
|
||||
pie.operator("screen.animation_play", text="Play Reverse", icon='PLAY_REVERSE').reverse = True
|
||||
# 8 - TOP
|
||||
if not context.screen.is_animation_playing: # Play / Pause
|
||||
pie.operator("screen.animation_play", text="Play", icon='PLAY')
|
||||
else:
|
||||
pie.operator("screen.animation_play", text="Stop", icon='PAUSE')
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(
|
||||
"screen.keyframe_jump", text="Previous Keyframe", icon='PREV_KEYFRAME'
|
||||
).next = False
|
||||
pie.operator("screen.keyframe_jump", text="Previous Keyframe", icon='PREV_KEYFRAME').next = False
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(
|
||||
"screen.keyframe_jump", text="Next Keyframe", icon='NEXT_KEYFRAME'
|
||||
).next = True
|
||||
pie.operator("screen.keyframe_jump", text="Next Keyframe", icon='NEXT_KEYFRAME').next = True
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.prop(context.tool_settings, "use_keyframe_insert_auto", text="Auto Keying")
|
||||
# 3 - BOTTOM - RIGHT
|
||||
@@ -41,14 +36,14 @@ class PIE_MT_animation(Menu):
|
||||
|
||||
|
||||
registry = [
|
||||
PIE_MT_animation,
|
||||
PIE_MT_playback,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name="Object Non-modal",
|
||||
pie_name=PIE_MT_animation.bl_idname,
|
||||
pie_name=PIE_MT_playback.bl_idname,
|
||||
hotkey_kwargs={'type': "SPACE", 'value': "PRESS", 'shift': True},
|
||||
default_fallback_op='screen.animation_play',
|
||||
on_drag=True,
|
||||
@@ -3,6 +3,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Menu
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.props import StringProperty
|
||||
from bpy.types import Context, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
@@ -11,38 +12,28 @@ class PIE_MT_proportional_edit(Menu):
|
||||
bl_idname = "PIE_MT_proportional_edit"
|
||||
bl_label = "Proportional Edit"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
ts = context.tool_settings
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator(
|
||||
"object.set_proportional_falloff", text="Smooth", icon='SMOOTHCURVE'
|
||||
).falloff_shape = 'SMOOTH'
|
||||
pie.operator("object.set_proportional_falloff", text="Smooth", icon='SMOOTHCURVE').falloff_shape = 'SMOOTH'
|
||||
|
||||
# 6 - RIGHT
|
||||
if context.mode == 'OBJECT':
|
||||
pie.menu(
|
||||
"PIE_MT_proportional_edit_obj_more", text="More...", icon='THREE_DOTS'
|
||||
)
|
||||
pie.menu("PIE_MT_proportional_edit_obj_more", text="More...", icon='THREE_DOTS')
|
||||
else:
|
||||
pie.menu(
|
||||
"PIE_MT_proportional_edit_mesh_more", text="More...", icon='THREE_DOTS'
|
||||
)
|
||||
pie.menu("PIE_MT_proportional_edit_mesh_more", text="More...", icon='THREE_DOTS')
|
||||
|
||||
# 2 - BOTTOM
|
||||
pie.operator(
|
||||
"object.set_proportional_falloff", text="Sharp", icon='SHARPCURVE'
|
||||
).falloff_shape = 'SHARP'
|
||||
pie.operator("object.set_proportional_falloff", text="Sharp", icon='SHARPCURVE').falloff_shape = 'SHARP'
|
||||
|
||||
if context.mode == 'OBJECT':
|
||||
# 8 - TOP
|
||||
pie.prop(ts, "use_proportional_edit_objects", text="Toggle Proportional")
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(
|
||||
"object.set_proportional_falloff", text="Root", icon='ROOTCURVE'
|
||||
).falloff_shape = 'ROOT'
|
||||
pie.operator("object.set_proportional_falloff", text="Root", icon='ROOTCURVE').falloff_shape = 'ROOT'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(
|
||||
"object.set_proportional_falloff",
|
||||
@@ -51,9 +42,7 @@ class PIE_MT_proportional_edit(Menu):
|
||||
).falloff_shape = 'INVERSE_SQUARE'
|
||||
else:
|
||||
# 8 - TOP
|
||||
pie.prop(
|
||||
ts, "use_proportional_edit", text="Toggle Proportional", icon='PROP_ON'
|
||||
)
|
||||
pie.prop(ts, "use_proportional_edit", text="Toggle Proportional", icon='PROP_ON')
|
||||
# 7 - TOP - LEFT
|
||||
pie.prop(
|
||||
ts,
|
||||
@@ -70,50 +59,36 @@ class PIE_MT_proportional_edit(Menu):
|
||||
)
|
||||
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(
|
||||
"object.set_proportional_falloff", text="Linear", icon='LINCURVE'
|
||||
).falloff_shape = 'LINEAR'
|
||||
pie.operator("object.set_proportional_falloff", text="Linear", icon='LINCURVE').falloff_shape = 'LINEAR'
|
||||
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator(
|
||||
"object.set_proportional_falloff", text="Sphere", icon='SPHERECURVE'
|
||||
).falloff_shape = 'SPHERE'
|
||||
pie.operator("object.set_proportional_falloff", text="Sphere", icon='SPHERECURVE').falloff_shape = 'SPHERE'
|
||||
|
||||
|
||||
class PIE_MT_proportional_edit_mesh_more(Menu):
|
||||
bl_idname = "PIE_MT_proportional_edit_mesh_more"
|
||||
bl_label = "More Falloff Shapes"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
layout.operator(
|
||||
"object.set_proportional_falloff", text="Root", icon='ROOTCURVE'
|
||||
).falloff_shape = 'ROOT'
|
||||
layout.operator("object.set_proportional_falloff", text="Root", icon='ROOTCURVE').falloff_shape = 'ROOT'
|
||||
layout.operator(
|
||||
"object.set_proportional_falloff",
|
||||
text="Inverse Square",
|
||||
icon='INVERSESQUARECURVE',
|
||||
).falloff_shape = 'INVERSE_SQUARE'
|
||||
layout.operator(
|
||||
"object.set_proportional_falloff", text="Constant", icon='NOCURVE'
|
||||
).falloff_shape = 'CONSTANT'
|
||||
layout.operator(
|
||||
"object.set_proportional_falloff", text="Random", icon='RNDCURVE'
|
||||
).falloff_shape = 'RANDOM'
|
||||
layout.operator("object.set_proportional_falloff", text="Constant", icon='NOCURVE').falloff_shape = 'CONSTANT'
|
||||
layout.operator("object.set_proportional_falloff", text="Random", icon='RNDCURVE').falloff_shape = 'RANDOM'
|
||||
|
||||
|
||||
class PIE_MT_proportional_edit_obj_more(Menu):
|
||||
bl_idname = "PIE_MT_proportional_edit_obj_more"
|
||||
bl_label = "More Falloff Shapes"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
layout.operator(
|
||||
"object.set_proportional_falloff", text="Constant", icon='NOCURVE'
|
||||
).falloff_shape = 'CONSTANT'
|
||||
layout.operator(
|
||||
"object.set_proportional_falloff", text="Random", icon='RNDCURVE'
|
||||
).falloff_shape = 'RANDOM'
|
||||
layout.operator("object.set_proportional_falloff", text="Constant", icon='NOCURVE').falloff_shape = 'CONSTANT'
|
||||
layout.operator("object.set_proportional_falloff", text="Random", icon='RNDCURVE').falloff_shape = 'RANDOM'
|
||||
|
||||
|
||||
class OBJECT_OT_set_proportional_falloff(Operator):
|
||||
@@ -125,7 +100,7 @@ class OBJECT_OT_set_proportional_falloff(Operator):
|
||||
|
||||
falloff_shape: StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
ts = context.tool_settings
|
||||
if context.mode == 'OBJECT':
|
||||
ts.use_proportional_edit_objects = True
|
||||
@@ -145,7 +120,10 @@ registry = (
|
||||
|
||||
|
||||
def register():
|
||||
for km_name, proportional_name in [('Object Mode', 'use_proportional_edit_objects'), ('Mesh', 'use_proportional_edit')]:
|
||||
for km_name, proportional_name in [
|
||||
('Object Mode', 'use_proportional_edit_objects'),
|
||||
('Mesh', 'use_proportional_edit'),
|
||||
]:
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name=km_name,
|
||||
pie_name=PIE_MT_proportional_edit.bl_idname,
|
||||
|
||||
@@ -3,11 +3,23 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import bpy
|
||||
from bpy.types import Object, Collection, Operator, Menu, ID
|
||||
from bpy.props import StringProperty, CollectionProperty
|
||||
from bpy.props import BoolProperty, CollectionProperty, StringProperty
|
||||
from bpy.types import (
|
||||
ID,
|
||||
Collection,
|
||||
Context,
|
||||
Event,
|
||||
Menu,
|
||||
NodeTree,
|
||||
Object,
|
||||
Operator,
|
||||
OperatorProperties,
|
||||
PropertyGroup,
|
||||
Scene,
|
||||
bpy_prop_collection,
|
||||
)
|
||||
from bpy_extras import id_map_utils
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
@@ -18,88 +30,95 @@ class OUTLINER_MT_relationship_pie(Menu):
|
||||
bl_idname = 'OUTLINER_MT_relationship_pie'
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if context.area.type == 'VIEW_3D':
|
||||
return True
|
||||
if context.area.type == 'OUTLINER':
|
||||
return bool(context.id)
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
|
||||
active_id = get_active_id(context)
|
||||
active_override = OUTLINER_OT_delete_override_library_hierarchy.poll(context)
|
||||
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
if context.area.type == 'VIEW_3D':
|
||||
if active_override:
|
||||
pie.operator(
|
||||
OUTLINER_OT_delete_override_library_hierarchy.bl_idname,
|
||||
icon='TRASH',
|
||||
text="Unlink Override Hierarchy",
|
||||
).remove_from_file = False
|
||||
else:
|
||||
pie.operator(
|
||||
OBJECT_OT_unlink_from_scene.bl_idname,
|
||||
icon='TRASH',
|
||||
text="Unlink From Scene",
|
||||
)
|
||||
# 6 - RIGHT
|
||||
if active_override:
|
||||
pie.operator(
|
||||
OUTLINER_OT_delete_override_library_hierarchy.bl_idname,
|
||||
icon='X',
|
||||
text='Delete Override Hierarchy',
|
||||
).remove_from_file = True
|
||||
else:
|
||||
pie.operator(
|
||||
'outliner.id_operation', icon='TRASH', text='Unlink From Scene'
|
||||
).type = 'UNLINK'
|
||||
# 6 - RIGHT
|
||||
if context.area.type == 'VIEW_3D':
|
||||
op = pie.operator('object.delete', icon='X', text="Delete From File")
|
||||
op.use_global = True
|
||||
op.confirm = False
|
||||
elif any([type(id) in (bpy.types.Object, bpy.types.Collection) for id in context.selected_ids]):
|
||||
# NOTE: Objects and other types of IDs cannot be deleted with the same operator, strangely enough.
|
||||
# If we wanted to support that, we could implement a wrapper operator, but it's a very rare case, so this feels fine.
|
||||
pie.operator('outliner.delete', icon='X', text='Delete From File')
|
||||
else:
|
||||
pie.operator('outliner.id_operation', icon='X', text="Delete IDs From File").type='DELETE'
|
||||
# 2 - BOTTOM
|
||||
pie.operator('outliner.pie_purge_orphans', icon='ORPHAN_DATA')
|
||||
# 8 - TOP
|
||||
id = get_active_id(context)
|
||||
if id:
|
||||
remap = pie.operator(
|
||||
'outliner.remap_users_ui', icon='FILE_REFRESH', text="Remap Users"
|
||||
OBJECT_OT_pie_delete.bl_idname,
|
||||
icon='X',
|
||||
text="Delete From File",
|
||||
)
|
||||
if id.library:
|
||||
remap.library_path_source = id.library.filepath
|
||||
# 2 - BOTTOM
|
||||
pie.operator(
|
||||
'outliner.pie_purge_orphans',
|
||||
icon='ORPHAN_DATA',
|
||||
)
|
||||
# 8 - TOP
|
||||
if active_id:
|
||||
remap = pie.operator(
|
||||
'outliner.remap_users_ui',
|
||||
icon='FILE_REFRESH',
|
||||
text="Remap Users",
|
||||
)
|
||||
if active_id.library:
|
||||
remap.library_path_source = active_id.library.filepath
|
||||
else:
|
||||
pie.separator()
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(OUTLINER_OT_list_users_of_datablock.bl_idname, icon='LOOP_BACK')
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(
|
||||
OUTLINER_OT_list_dependencies_of_datablock.bl_idname, icon='LOOP_FORWARDS'
|
||||
)
|
||||
pie.operator(OUTLINER_OT_list_dependencies_of_datablock.bl_idname, icon='LOOP_FORWARDS')
|
||||
# 1 - BOTTOM - LEFT
|
||||
if context.area.type == 'OUTLINER' and active_id:
|
||||
pie.operator(
|
||||
'outliner.id_operation',
|
||||
text="Unlink From Collection",
|
||||
icon="OUTLINER_COLLECTION",
|
||||
icon='OUTLINER_COLLECTION',
|
||||
).type = 'UNLINK'
|
||||
else:
|
||||
pie.separator()
|
||||
# 3 - BOTTOM - RIGHT
|
||||
if context.area.type == 'OUTLINER' and active_id:
|
||||
pie.operator(
|
||||
'outliner.delete', text="Delete Hierarchy", icon="OUTLINER"
|
||||
'outliner.delete',
|
||||
text="Delete Hierarchy",
|
||||
icon='OUTLINER',
|
||||
).hierarchy = True
|
||||
else:
|
||||
pie.operator(
|
||||
OBJECT_OT_instancer_empty_to_collection.bl_idname, icon='LINKED'
|
||||
)
|
||||
pie.operator(OBJECT_OT_instancer_empty_to_collection.bl_idname, icon='LINKED')
|
||||
|
||||
|
||||
class OUTLINER_OT_pie_purge_orphans(Operator):
|
||||
"""Clear all orphaned data-blocks without any users from the file"""
|
||||
|
||||
bl_idname = "outliner.pie_purge_orphans"
|
||||
bl_idname = 'outliner.pie_purge_orphans'
|
||||
bl_label = "Purge Unused Data"
|
||||
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
"""Call Blender's purge function, but in a way that works from any editor, not only the 3D View."""
|
||||
|
||||
orig_ui = None
|
||||
@@ -112,9 +131,7 @@ class OUTLINER_OT_pie_purge_orphans(Operator):
|
||||
area = context.area
|
||||
|
||||
with context.temp_override(area=area):
|
||||
ret = bpy.ops.outliner.orphans_purge(
|
||||
do_local_ids=True, do_linked_ids=True, do_recursive=True
|
||||
)
|
||||
ret = bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||
|
||||
if orig_ui:
|
||||
context.area.ui_type = orig_ui
|
||||
@@ -123,18 +140,16 @@ class OUTLINER_OT_pie_purge_orphans(Operator):
|
||||
|
||||
|
||||
class OBJECT_OT_unlink_from_scene(Operator):
|
||||
"""Unlink selected objects or collections from the current scene"""
|
||||
"""Unlink selected objects or collections from all collections of the current scene"""
|
||||
|
||||
bl_idname = "object.unlink_from_scene"
|
||||
bl_idname = 'object.unlink_from_scene'
|
||||
bl_label = "Unlink Selected From Scene"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if context.area.type == 'OUTLINER':
|
||||
any_selected = bool(
|
||||
get_objects_to_unlink(context) or get_collections_to_unlink(context)
|
||||
)
|
||||
any_selected = bool(get_objects_to_unlink(context) or get_collections_to_unlink(context))
|
||||
elif context.area.type == 'VIEW_3D':
|
||||
any_selected = bool(get_objects_to_unlink(context))
|
||||
else:
|
||||
@@ -144,7 +159,7 @@ class OBJECT_OT_unlink_from_scene(Operator):
|
||||
cls.poll_message_set("Nothing is selected.")
|
||||
return any_selected
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
unlink_collections_from_scene(get_collections_to_unlink(context), context.scene)
|
||||
unlink_objects_from_scene(get_objects_to_unlink(context), context.scene)
|
||||
|
||||
@@ -156,7 +171,7 @@ class RelationshipOperatorMixin:
|
||||
datablock_storage: StringProperty()
|
||||
library_filepath: StringProperty()
|
||||
|
||||
def get_datablock(self, context) -> ID | None:
|
||||
def get_datablock(self, context: Context) -> ID | None:
|
||||
if self.datablock_name and self.datablock_storage:
|
||||
storage = getattr(bpy.data, self.datablock_storage)
|
||||
lib_path = self.library_filepath or None
|
||||
@@ -167,30 +182,30 @@ class RelationshipOperatorMixin:
|
||||
return context.active_object
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
id = get_active_id(context)
|
||||
if context.area.type == 'OUTLINER':
|
||||
if id == None:
|
||||
if id is None:
|
||||
cls.poll_message_set("No active ID.")
|
||||
return False
|
||||
return True
|
||||
elif context.area.type == '3D_VIEW':
|
||||
if id == None:
|
||||
if id is None:
|
||||
cls.poll_message_set("No active object.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def invoke(self, context, _event):
|
||||
def invoke(self, context: Context, _event: Event) -> set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self, width=600)
|
||||
|
||||
def get_datablocks_to_display(self, id: ID) -> list[ID]:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_label(self):
|
||||
def get_label(self) -> str:
|
||||
return "Listing datablocks that reference this:"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
layout.use_property_decorate = False
|
||||
layout.use_property_split = True
|
||||
@@ -211,9 +226,9 @@ class RelationshipOperatorMixin:
|
||||
id_row = split.row(align=True)
|
||||
name_row = id_row.row()
|
||||
name_row.enabled = False
|
||||
name_row.prop(datablock, 'name', icon=get_datablock_icon(datablock), text="")
|
||||
name_row.prop(datablock, 'name', icon=get_datablock_icon(datablock), text='')
|
||||
fake_user_row = id_row.row()
|
||||
fake_user_row.prop(datablock, 'use_fake_user', text="")
|
||||
fake_user_row.prop(datablock, 'use_fake_user', text='')
|
||||
|
||||
layout.separator()
|
||||
|
||||
@@ -230,14 +245,15 @@ class RelationshipOperatorMixin:
|
||||
row = layout.row()
|
||||
name_row = row.row()
|
||||
name_row.enabled = False
|
||||
name_row.prop(user, 'name', icon=get_datablock_icon(user), text="")
|
||||
name_row.prop(user, 'name', icon=get_datablock_icon(user), text='')
|
||||
op_row = row.row()
|
||||
op = op_row.operator(type(self).bl_idname, text="", icon='LOOP_FORWARDS')
|
||||
op = op_row.operator(type(self).bl_idname, text='', icon='LOOP_FORWARDS')
|
||||
op.datablock_name = user.name
|
||||
storage = get_id_storage_by_type_str(user.id_type)[1]
|
||||
storage = get_id_storage_by_type_str(user.id_type)
|
||||
if not storage:
|
||||
print("Error: Can't find storage: ", user.name, user.id_type)
|
||||
op.datablock_storage = storage
|
||||
else:
|
||||
op.datablock_storage = storage[1]
|
||||
if user.library:
|
||||
op.library_filepath = user.library.filepath
|
||||
name_row.prop(
|
||||
@@ -247,14 +263,22 @@ class RelationshipOperatorMixin:
|
||||
text="",
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context) -> set[str]:
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_datablock_users(datablock: ID) -> list[ID]:
|
||||
user_map = bpy.data.user_map()
|
||||
users = user_map[datablock]
|
||||
if datablock in users:
|
||||
users.remove(datablock)
|
||||
return sorted(users, key=lambda u: (str(type(u)), u.name))
|
||||
|
||||
|
||||
class OUTLINER_OT_list_users_of_datablock(RelationshipOperatorMixin, Operator):
|
||||
"""Show list of users of this datablock"""
|
||||
|
||||
bl_idname = "object.list_datablock_users"
|
||||
bl_idname = 'object.list_datablock_users'
|
||||
bl_label = "List Datablock Users"
|
||||
|
||||
datablock_name: StringProperty()
|
||||
@@ -262,18 +286,16 @@ class OUTLINER_OT_list_users_of_datablock(RelationshipOperatorMixin, Operator):
|
||||
library_filepath: StringProperty()
|
||||
|
||||
def get_datablocks_to_display(self, datablock: ID) -> list[ID]:
|
||||
user_map = bpy.data.user_map()
|
||||
users = user_map[datablock]
|
||||
return sorted(users, key=lambda u: (str(type(u)), u.name))
|
||||
return get_datablock_users(datablock)
|
||||
|
||||
|
||||
class OUTLINER_OT_list_dependencies_of_datablock(RelationshipOperatorMixin, Operator):
|
||||
"""Show list of dependencies of this datablock"""
|
||||
|
||||
bl_idname = "object.list_datablock_dependencies"
|
||||
bl_idname = 'object.list_datablock_dependencies'
|
||||
bl_label = "List Datablock Dependencies"
|
||||
|
||||
def get_label(self):
|
||||
def get_label(self) -> str:
|
||||
return "Listing datablocks that are referenced by this:"
|
||||
|
||||
def get_datablocks_to_display(self, datablock: ID) -> list[ID]:
|
||||
@@ -283,23 +305,26 @@ class OUTLINER_OT_list_dependencies_of_datablock(RelationshipOperatorMixin, Oper
|
||||
return sorted(dependencies, key=lambda u: (str(type(u)), u.name))
|
||||
|
||||
|
||||
class RemapTarget(bpy.types.PropertyGroup):
|
||||
class RemapTarget(PropertyGroup):
|
||||
pass
|
||||
|
||||
|
||||
class OUTLINER_OT_remap_users_ui(bpy.types.Operator):
|
||||
class OUTLINER_OT_remap_users_ui(Operator):
|
||||
"""Remap users of selected IDs to any other ID of the same type"""
|
||||
|
||||
bl_idname = "outliner.remap_users_ui"
|
||||
bl_idname = 'outliner.remap_users_ui'
|
||||
bl_label = "Remap Users"
|
||||
bl_options = {'INTERNAL', 'UNDO'}
|
||||
|
||||
def update_library_path(self, context):
|
||||
def update_library_path(self, context: Context) -> None:
|
||||
# Prepare the ID selector.
|
||||
remap_targets = context.scene.remap_targets
|
||||
remap_targets.clear()
|
||||
source_ids = get_selected_ids_of_active_type(context)
|
||||
for id in get_id_storage_by_type_str(source_ids[0].id_type)[0]:
|
||||
storage_info = get_id_storage_by_type_str(source_ids[0].id_type)
|
||||
if not storage_info:
|
||||
return
|
||||
for id in storage_info[0]:
|
||||
if id in source_ids:
|
||||
continue
|
||||
if (self.library_path == 'Local Data' and not id.library) or (
|
||||
@@ -314,19 +339,20 @@ class OUTLINER_OT_remap_users_ui(bpy.types.Operator):
|
||||
update=update_library_path,
|
||||
)
|
||||
library_path_source: StringProperty()
|
||||
id_name_target: StringProperty(
|
||||
name="Target ID Name", description="Name of the ID we're remapping users to"
|
||||
)
|
||||
id_name_target: StringProperty(name="Target ID Name", description="Name of the ID we're remapping users to")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
source_ids = get_selected_ids_of_active_type(context)
|
||||
if not source_ids:
|
||||
cls.poll_message_set("No selected IDs.")
|
||||
return False
|
||||
if any(src_id.override_library for src_id in source_ids if src_id):
|
||||
cls.poll_message_set("Cannot remap users of library overrides.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context, _event):
|
||||
def invoke(self, context: Context, _event: Event) -> set[str]:
|
||||
# Populate the remap_targets string list with possible options based on
|
||||
# selection context.
|
||||
|
||||
@@ -334,19 +360,21 @@ class OUTLINER_OT_remap_users_ui(bpy.types.Operator):
|
||||
remap_target_libraries = context.scene.remap_target_libraries
|
||||
remap_target_libraries.clear()
|
||||
local = remap_target_libraries.add()
|
||||
local.name = "Local Data"
|
||||
local.name = 'Local Data'
|
||||
source_ids = get_selected_ids_of_active_type(context)
|
||||
for lib in bpy.data.libraries:
|
||||
for id in lib.users_id:
|
||||
if type(id) == type(source_ids[0]):
|
||||
if isinstance(id, type(source_ids[0])):
|
||||
lib_entry = remap_target_libraries.add()
|
||||
lib_entry.name = lib.filepath
|
||||
break
|
||||
|
||||
container = get_id_storage_by_type_str(source_ids[0].id_type)[0]
|
||||
storage_info = get_id_storage_by_type_str(source_ids[0].id_type)
|
||||
assert storage_info
|
||||
container = storage_info[0]
|
||||
|
||||
self.library_path = "Local Data"
|
||||
suffixed_id = next((id for id in source_ids if id.name[-4] == "."), None)
|
||||
self.library_path = 'Local Data'
|
||||
suffixed_id = next((id for id in source_ids if id.name[-4] == '.'), None)
|
||||
|
||||
if suffixed_id:
|
||||
default_target = container.get(suffixed_id.name[:-4])
|
||||
@@ -355,12 +383,12 @@ class OUTLINER_OT_remap_users_ui(bpy.types.Operator):
|
||||
if default_target.library:
|
||||
self.library_path = default_target.library.filepath
|
||||
else:
|
||||
self.id_name_target = ""
|
||||
self.id_name_target = ''
|
||||
self.library_path = 'Local Data'
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=600)
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
@@ -371,7 +399,7 @@ class OUTLINER_OT_remap_users_ui(bpy.types.Operator):
|
||||
for i, source_id in enumerate(source_ids):
|
||||
row = layout.row()
|
||||
split = row.split()
|
||||
if i==0:
|
||||
if i == 0:
|
||||
split.row().label(text="Anything that was referencing these:")
|
||||
else:
|
||||
split.row()
|
||||
@@ -399,7 +427,7 @@ class OUTLINER_OT_remap_users_ui(bpy.types.Operator):
|
||||
icon=id_icon,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
source_ids = get_selected_ids_of_active_type(context)
|
||||
target_id = get_id(self.id_name_target, source_ids[0].id_type, self.library_path)
|
||||
assert source_ids and target_id, "Error: Failed to find source or target."
|
||||
@@ -409,33 +437,154 @@ class OUTLINER_OT_remap_users_ui(bpy.types.Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_pie_delete(Operator):
|
||||
"""Delete selected objects or collections from the file.\n\nShift: Show dependent datablocks before confirming deletion"""
|
||||
|
||||
bl_idname = 'object.pie_delete'
|
||||
bl_label = "Delete From File"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@staticmethod
|
||||
def _get_items_to_delete(context: Context) -> list[ID]:
|
||||
if context.area and context.area.type == 'OUTLINER':
|
||||
return list(context.selected_ids or [])
|
||||
return list(context.selected_objects or [])
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not cls._get_items_to_delete(context):
|
||||
cls.poll_message_set("Nothing selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
if not event or not event.shift:
|
||||
return self.execute(context)
|
||||
|
||||
self._users_per_obj: list[tuple[ID, list[ID]]] = []
|
||||
for item in self._get_items_to_delete(context):
|
||||
users = get_datablock_users(item)
|
||||
if context.scene in users:
|
||||
users.remove(context.scene)
|
||||
self._users_per_obj.append((item, users))
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=400) # type: ignore[union-attr]
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
layout = self.layout
|
||||
if not layout:
|
||||
return
|
||||
layout.use_property_decorate = False
|
||||
|
||||
for obj, users in self._users_per_obj:
|
||||
if len(self._users_per_obj) > 1 and users:
|
||||
header, body = layout.panel(
|
||||
f'delete_dialog_{obj.name}',
|
||||
default_closed=len(self._users_per_obj) > 3,
|
||||
)
|
||||
header_row = header.row(align=True)
|
||||
else:
|
||||
body = layout
|
||||
header_row = body.row(align=True)
|
||||
if len(self._users_per_obj) > 1:
|
||||
header_row.label(text='', icon='BLANK1')
|
||||
header_row.prop(obj, 'name', icon=get_datablock_icon(obj), text='', emboss=False)
|
||||
header_row.label(text=f'{len(users)} users')
|
||||
|
||||
if body and users:
|
||||
for user in users:
|
||||
split = body.split(factor=0.15)
|
||||
split.row()
|
||||
row = split.row()
|
||||
row.prop(
|
||||
user,
|
||||
'name',
|
||||
icon=get_datablock_icon(user),
|
||||
text='',
|
||||
emboss=False,
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
count = len(self._get_items_to_delete(context))
|
||||
if context.area and context.area.type == 'OUTLINER':
|
||||
items = self._get_items_to_delete(context)
|
||||
if any(isinstance(id, (Object, Collection)) for id in items):
|
||||
ret = bpy.ops.outliner.delete()
|
||||
else:
|
||||
ret = bpy.ops.outliner.id_operation(type='DELETE')
|
||||
else:
|
||||
ret = bpy.ops.object.delete(use_global=True, confirm=False)
|
||||
if ret == {'FINISHED'}:
|
||||
self.report({'INFO'}, f"Deleted {count} datablock(s).")
|
||||
return ret
|
||||
|
||||
|
||||
class OUTLINER_OT_delete_override_library_hierarchy(Operator):
|
||||
"""Delete the library override hierarchy that the active datablock belongs to"""
|
||||
|
||||
bl_idname = 'outliner.delete_override_library_hierarchy'
|
||||
bl_label = "Delete Override Hierarchy"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
remove_from_file: BoolProperty(
|
||||
name="Remove from File",
|
||||
default=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def description(cls, _context: Context, properties: OperatorProperties) -> str:
|
||||
if properties.remove_from_file:
|
||||
return cls.bl_description
|
||||
return "Unlink the library override hierarchy that the active datablock belongs to from the active Scene"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
active_id = get_active_id(context)
|
||||
if not active_id:
|
||||
cls.poll_message_set("No active datablock.")
|
||||
return False
|
||||
if not active_id.override_library:
|
||||
cls.poll_message_set("Active datablock is not a library override.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
active_id = get_active_id(context)
|
||||
assert active_id and active_id.override_library
|
||||
root = active_id.override_library.hierarchy_root
|
||||
assert root
|
||||
root_name = root.name
|
||||
|
||||
count = remove_override_hierarchy(root, remove_from_file=self.remove_from_file, scene=context.scene)
|
||||
|
||||
word = "Deleted" if self.remove_from_file else "Unlinked"
|
||||
self.report(
|
||||
{'INFO'},
|
||||
f"{word} {count} override datablock(s) from hierarchy of {root_name!r}.",
|
||||
)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_instancer_empty_to_collection(Operator):
|
||||
"""Replace the Empty that instances a collection, with the collection itself"""
|
||||
|
||||
bl_idname = "object.instancer_empty_to_collection"
|
||||
bl_idname = 'object.instancer_empty_to_collection'
|
||||
bl_label = "Instancer Empty To Collection"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
obj = get_active_id(context)
|
||||
|
||||
if not (
|
||||
obj
|
||||
and obj.type == 'EMPTY'
|
||||
and obj.instance_type == 'COLLECTION'
|
||||
and obj.instance_collection
|
||||
):
|
||||
if not (obj and obj.type == 'EMPTY' and obj.instance_type == 'COLLECTION' and obj.instance_collection):
|
||||
cls.poll_message_set("Active object is not an instancer empty.")
|
||||
return False
|
||||
if obj.instance_collection in set(context.scene.collection.children):
|
||||
cls.poll_message_set(
|
||||
f'Collection "{obj.instance_collection.name}" is already in the scene.'
|
||||
)
|
||||
cls.poll_message_set(f'Collection "{obj.instance_collection.name}" is already in the scene.')
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
obj = get_active_id(context)
|
||||
|
||||
coll = obj.instance_collection
|
||||
@@ -447,66 +596,97 @@ class OBJECT_OT_instancer_empty_to_collection(Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_objects_to_unlink(context) -> list[Object]:
|
||||
def get_objects_to_unlink(context: Context) -> list[Object]:
|
||||
if context.area.type == 'OUTLINER':
|
||||
selected_objs = [id for id in context.selected_ids if type(id) == Object]
|
||||
selected_objs = [id for id in context.selected_ids if isinstance(id, Object)]
|
||||
elif context.area.type == 'VIEW_3D':
|
||||
selected_objs = context.selected_objects
|
||||
selected_objs = list(context.selected_objects)
|
||||
else:
|
||||
return []
|
||||
|
||||
scene_objs = set(context.scene.objects)
|
||||
return [ob for ob in selected_objs if ob in scene_objs]
|
||||
|
||||
|
||||
def unlink_objects_from_scene(objects, scene):
|
||||
def unlink_objects_from_scene(objects: list[Object], scene: Scene) -> None:
|
||||
for obj in objects:
|
||||
for coll in [scene.collection] + scene.collection.children_recursive:
|
||||
if obj.name in coll.objects:
|
||||
if coll.override_library:
|
||||
continue
|
||||
if obj in set(coll.objects):
|
||||
coll.objects.unlink(obj)
|
||||
|
||||
|
||||
def get_collections_to_unlink(context) -> list[Collection]:
|
||||
def get_collections_to_unlink(context: Context) -> list[Collection]:
|
||||
if context.area.type == 'OUTLINER':
|
||||
return [id for id in context.selected_ids if type(id) == Collection]
|
||||
return [id for id in context.selected_ids if isinstance(id, Collection)]
|
||||
return []
|
||||
|
||||
|
||||
def unlink_collections_from_scene(collections_to_unlink, scene):
|
||||
def unlink_collections_from_scene(collections_to_unlink: list[Collection], scene: Scene) -> None:
|
||||
for coll_to_unlink in collections_to_unlink:
|
||||
for coll in scene.collection.children_recursive:
|
||||
if coll_to_unlink.name in coll.children:
|
||||
for coll in scene.collection.children_recursive + [scene.collection]:
|
||||
if coll_to_unlink in set(coll.children):
|
||||
coll.children.unlink(coll_to_unlink)
|
||||
|
||||
|
||||
def get_active_id(context) -> ID | None:
|
||||
def remove_override_hierarchy(root: ID, *, remove_from_file: bool, scene: Scene | None = None) -> int:
|
||||
"""Find and remove all datablocks belonging to the override hierarchy rooted at `root`.
|
||||
|
||||
If `remove_from_file` is True, permanently deletes from bpy.data via batch_remove.
|
||||
Otherwise unlinks objects and collections from `scene` (required in this case).
|
||||
Returns the number of datablocks found in the hierarchy.
|
||||
"""
|
||||
root_ptr = root.as_pointer()
|
||||
|
||||
hierarchy_ids = [
|
||||
datablock
|
||||
for *_, container_str in get_id_info()
|
||||
for datablock in getattr(bpy.data, container_str)
|
||||
if (ol := datablock.override_library) and (hr := ol.hierarchy_root) and hr.as_pointer() == root_ptr
|
||||
]
|
||||
|
||||
if remove_from_file:
|
||||
bpy.data.batch_remove(ids=hierarchy_ids)
|
||||
else:
|
||||
assert scene is not None
|
||||
unlink_objects_from_scene([db for db in hierarchy_ids if isinstance(db, Object)], scene)
|
||||
unlink_collections_from_scene([db for db in hierarchy_ids if isinstance(db, Collection)], scene)
|
||||
|
||||
return len(hierarchy_ids)
|
||||
|
||||
|
||||
def get_active_id(context: Context) -> ID | None:
|
||||
if context.area.ui_type == 'OUTLINER':
|
||||
return context.id
|
||||
else:
|
||||
return context.active_object
|
||||
|
||||
|
||||
# List of datablock type information tuples:
|
||||
# (type_class, type_enum_string, bpy.data.<collprop_name>)
|
||||
def get_id_info() -> list[tuple[type, str, str]]:
|
||||
bpy_prop_collection = type(bpy.data.objects)
|
||||
"""Return a list of datablock type information tuples:
|
||||
(type_class, type_enum_string, bpy.data.<collprop_name>)
|
||||
"""
|
||||
id_info = []
|
||||
for prop_name in dir(bpy.data):
|
||||
prop = getattr(bpy.data, prop_name)
|
||||
if type(prop) == bpy_prop_collection:
|
||||
if isinstance(prop, bpy_prop_collection):
|
||||
if len(prop) == 0:
|
||||
# We can't get full info about the ID type if there isn't at least one entry of it.
|
||||
# But we shouldn't need it, since we don't have any entries of it!
|
||||
continue
|
||||
typ = type(prop[0])
|
||||
if issubclass(typ, bpy.types.NodeTree):
|
||||
typ = bpy.types.NodeTree
|
||||
if issubclass(typ, NodeTree):
|
||||
typ = NodeTree
|
||||
id_info.append((typ, prop[0].id_type, prop_name))
|
||||
return id_info
|
||||
|
||||
|
||||
def get_id_storage_by_type_str(typ_name: str):
|
||||
for typ, typ_str, container_str in get_id_info():
|
||||
def get_id_storage_by_type_str(typ_name: str) -> tuple[bpy_prop_collection, str] | None:
|
||||
for _typ, typ_str, container_str in get_id_info():
|
||||
if typ_str == typ_name:
|
||||
return getattr(bpy.data, container_str), container_str
|
||||
return None
|
||||
|
||||
|
||||
def get_datablock_types_enum_items() -> list[tuple[str, str, str, str, int]]:
|
||||
@@ -518,40 +698,26 @@ def get_datablock_types_enum_items() -> list[tuple[str, str, str, str, int]]:
|
||||
ret.append(('SCREEN', 'Screen', 'Screen', 'RESTRICT_VIEW_OFF', len(ret)))
|
||||
ret.append(('METABALL', 'Metaball', 'Metaball', 'OUTLINER_OB_META', len(ret)))
|
||||
ret.append(('CACHE_FILE', 'Cache File', 'Cache File', 'MOD_MESHDEFORM', len(ret)))
|
||||
ret.append(
|
||||
(
|
||||
'POINT_CLOUD',
|
||||
'Point Cloud',
|
||||
'Point Cloud',
|
||||
'OUTLINER_OB_POINTCLOUD',
|
||||
len(ret),
|
||||
)
|
||||
)
|
||||
ret.append(
|
||||
('HAIR_CURVES', 'Hair Curves', 'Hair Curves', 'OUTLINER_OB_CURVES', len(ret))
|
||||
)
|
||||
ret.append(('POINT_CLOUD', 'Point Cloud', 'Point Cloud', 'OUTLINER_OB_POINTCLOUD', len(ret)))
|
||||
ret.append(('HAIR_CURVES', 'Hair Curves', 'Hair Curves', 'OUTLINER_OB_CURVES', len(ret)))
|
||||
ret.append(('PAINT_CURVE', 'Paint Curve', 'Paint Curve', 'FORCE_CURVE', len(ret)))
|
||||
ret.append(('MOVIE_CLIP', 'Movie Clip', 'Movie Clip', 'FILE_MOVIE', len(ret)))
|
||||
return ret
|
||||
|
||||
|
||||
# List of 5-tuples that can be used to define the items of an EnumProperty.
|
||||
ID_TYPE_ENUM_ITEMS: list[tuple[str, str, str, str, int]] = (
|
||||
get_datablock_types_enum_items()
|
||||
)
|
||||
ID_TYPE_ENUM_ITEMS: list[tuple[str, str, str, str, int]] = get_datablock_types_enum_items()
|
||||
|
||||
# Map datablock type enum strings to their name and icon strings.
|
||||
ID_TYPE_INFO: dict[str, tuple[str, str]] = {
|
||||
tup[0]: (tup[1], tup[3]) for tup in ID_TYPE_ENUM_ITEMS
|
||||
}
|
||||
ID_TYPE_INFO: dict[str, tuple[str, str]] = {tup[0]: (tup[1], tup[3]) for tup in ID_TYPE_ENUM_ITEMS}
|
||||
|
||||
|
||||
def get_datablock_icon(id) -> str:
|
||||
def get_datablock_icon(id: ID) -> str:
|
||||
id_type = get_fundamental_id_type(id)[1]
|
||||
return ID_TYPE_INFO[id_type][1]
|
||||
|
||||
|
||||
def get_fundamental_id_type(datablock: ID) -> tuple[Any, str]:
|
||||
def get_fundamental_id_type(datablock: ID) -> tuple[type[ID], str]:
|
||||
"""Certain datablocks have very specific types.
|
||||
This function should return their fundamental type, ie. parent class."""
|
||||
id_info = get_id_info()
|
||||
@@ -559,20 +725,19 @@ def get_fundamental_id_type(datablock: ID) -> tuple[Any, str]:
|
||||
if isinstance(datablock, typ):
|
||||
return typ, typ_str
|
||||
|
||||
raise Exception(
|
||||
f"Failed to get fundamental ID type of ID: {datablock.name}, {type(datablock)}"
|
||||
)
|
||||
raise Exception(f"Failed to get fundamental ID type of ID: {datablock.name}, {type(datablock)}")
|
||||
|
||||
|
||||
def get_selected_ids_of_active_type(context):
|
||||
def get_selected_ids_of_active_type(context: Context) -> list[ID]:
|
||||
active_id = get_active_id(context)
|
||||
return [
|
||||
id for id in context.selected_ids
|
||||
if type(id) == type(active_id)
|
||||
]
|
||||
return [id for id in (context.selected_ids or []) if active_id and isinstance(id, type(active_id))]
|
||||
|
||||
def get_id(id_name: str, id_type: str, lib_path="") -> ID:
|
||||
container = get_id_storage_by_type_str(id_type)[0]
|
||||
|
||||
def get_id(id_name: str, id_type: str, lib_path: str = '') -> ID | None:
|
||||
storage_info = get_id_storage_by_type_str(id_type)
|
||||
if not storage_info:
|
||||
return None
|
||||
container = storage_info[0]
|
||||
if lib_path and lib_path != 'Local Data':
|
||||
return container.get((id_name, lib_path))
|
||||
return container.get((id_name, None))
|
||||
@@ -591,33 +756,35 @@ def get_library_icon(lib_path: str) -> str:
|
||||
|
||||
registry = [
|
||||
OUTLINER_MT_relationship_pie,
|
||||
OBJECT_OT_pie_delete,
|
||||
OUTLINER_OT_pie_purge_orphans,
|
||||
OBJECT_OT_unlink_from_scene,
|
||||
OUTLINER_OT_list_users_of_datablock,
|
||||
OUTLINER_OT_list_dependencies_of_datablock,
|
||||
RemapTarget,
|
||||
OUTLINER_OT_remap_users_ui,
|
||||
OUTLINER_OT_delete_override_library_hierarchy,
|
||||
OBJECT_OT_instancer_empty_to_collection,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.remap_targets = CollectionProperty(type=RemapTarget)
|
||||
bpy.types.Scene.remap_target_libraries = CollectionProperty(type=RemapTarget)
|
||||
def register() -> None:
|
||||
Scene.remap_targets = CollectionProperty(type=RemapTarget)
|
||||
Scene.remap_target_libraries = CollectionProperty(type=RemapTarget)
|
||||
|
||||
for keymap_name in ("Object Mode", "Outliner"):
|
||||
for keymap_name in ('Object Mode', 'Outliner'):
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name=keymap_name,
|
||||
pie_name=OUTLINER_MT_relationship_pie.bl_idname,
|
||||
hotkey_kwargs={'type': "X", 'value': "PRESS"},
|
||||
hotkey_kwargs={'type': 'X', 'value': 'PRESS'},
|
||||
on_drag=False,
|
||||
)
|
||||
|
||||
|
||||
def unregister():
|
||||
def unregister() -> None:
|
||||
try:
|
||||
del bpy.types.Scene.remap_targets
|
||||
del bpy.types.Scene.remap_target_libraries
|
||||
del Scene.remap_targets
|
||||
del Scene.remap_target_libraries
|
||||
except AttributeError:
|
||||
# Blender Log also implements these same things, which can result in errors.
|
||||
pass
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
@@ -6,8 +6,8 @@ import os
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu
|
||||
from bl_ui.properties_paint_common import BrushAssetShelf
|
||||
from bpy.types import Context, Menu, UILayout
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
@@ -16,8 +16,7 @@ class PIE_MT_sculpt_brush_select(Menu):
|
||||
bl_idname = "PIE_MT_sculpt_brush_select"
|
||||
bl_label = "Sculpt Brush Select"
|
||||
|
||||
def draw(self, context):
|
||||
global brush_icons
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
pie.scale_y = 1.2
|
||||
@@ -42,9 +41,7 @@ class PIE_MT_sculpt_brush_select(Menu):
|
||||
brush_row = col.row()
|
||||
brush_row.scale_y = 0.75
|
||||
brush_row.scale_x = 0.15
|
||||
BrushAssetShelf.draw_popup_selector(
|
||||
brush_row, context, brush, show_name=False
|
||||
)
|
||||
BrushAssetShelf.draw_popup_selector(brush_row, context, brush, show_name=False)
|
||||
name_row = col.row().box()
|
||||
if brush:
|
||||
name_row.label(text=brush.name)
|
||||
@@ -75,7 +72,7 @@ class PIE_MT_sculpt_brush_select_contrast(Menu):
|
||||
bl_idname = "PIE_MT_sculpt_brush_select_contrast"
|
||||
bl_label = "Contrast Brushes"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
@@ -99,7 +96,7 @@ class PIE_MT_sculpt_brush_select_transform(Menu):
|
||||
bl_idname = "PIE_MT_sculpt_brush_select_transform"
|
||||
bl_label = "Transform Brushes"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
@@ -124,7 +121,7 @@ class PIE_MT_sculpt_brush_select_volume(Menu):
|
||||
bl_idname = "PIE_MT_sculpt_brush_select_volume"
|
||||
bl_label = "Volume Brushes"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
draw_brush_operator(pie, 'Blob', 'blob')
|
||||
@@ -148,7 +145,7 @@ class PIE_MT_sculpt_brush_select_special(Menu):
|
||||
bl_idname = "PIE_MT_sculpt_brush_select_special"
|
||||
bl_label = "Special Brushes"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
draw_brush_operator(pie, 'Grab Cloth', 'cloth')
|
||||
@@ -168,11 +165,11 @@ class PIE_MT_sculpt_brush_select_special(Menu):
|
||||
draw_brush_operator(pie, 'Smear Multires Displacement', 'displacement_smear')
|
||||
|
||||
|
||||
def blender_uses_brush_assets():
|
||||
def blender_uses_brush_assets() -> bool:
|
||||
return 'asset_activate' in dir(bpy.ops.brush)
|
||||
|
||||
|
||||
def draw_brush_operator(layout, brush_name: str, brush_icon: str = ""):
|
||||
def draw_brush_operator(layout: UILayout, brush_name: str, brush_icon: str = ""):
|
||||
"""Draw a brush select operator in the provided UI element with the pre-4.3 icons.
|
||||
brush_name must match the name of the Brush Asset.
|
||||
brush_icon must match the name of a file in this add-on's icons folder.
|
||||
@@ -205,7 +202,6 @@ brush_icons = {}
|
||||
|
||||
|
||||
def create_icons():
|
||||
global brush_icons
|
||||
icons_directory = Path(__file__).parent / "icons"
|
||||
|
||||
for icon_path in icons_directory.iterdir():
|
||||
@@ -215,7 +211,6 @@ def create_icons():
|
||||
|
||||
|
||||
def release_icons():
|
||||
global brush_icons
|
||||
for value in brush_icons.values():
|
||||
bpy.app.icons.release(value)
|
||||
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Bone, Context, EditBone, Menu, Object, Operator, PoseBone
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
SEL_SIMILAR = "mesh.select_similar"
|
||||
|
||||
|
||||
class PIE_MT_select_similar_object(Menu):
|
||||
bl_idname = "PIE_MT_select_similar_object"
|
||||
bl_label = "Select Similar"
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
pie.operator("object.select_grouped", text="Collection", icon='OUTLINER_COLLECTION').type = 'COLLECTION'
|
||||
# 6 - RIGHT
|
||||
pie.operator("object.select_grouped", text="Type", icon='OBJECT_DATA').type = 'TYPE'
|
||||
# 2 - BOTTOM
|
||||
pie.operator("object.select_grouped", text="Colour", icon='COLOR').type = 'COLOR'
|
||||
# 8 - TOP
|
||||
pie.operator("object.select_grouped", text="Pass", icon='RENDERLAYERS').type = 'PASS'
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator("object.select_grouped", text="Keying Set", icon='KEYINGSET').type = 'KEYINGSET'
|
||||
# 9 - TOP - RIGHT
|
||||
if context.active_object and context.active_object.type == 'LIGHT':
|
||||
pie.operator("object.select_grouped", text="Light Type", icon='LIGHT').type = 'LIGHT_TYPE'
|
||||
else:
|
||||
pie.separator()
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator("object.select_grouped", text="Hook", icon='HOOK').type = 'HOOK'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.separator()
|
||||
|
||||
|
||||
class PIE_MT_select_similar_mesh(Menu):
|
||||
bl_idname = "PIE_MT_select_similar_mesh"
|
||||
bl_label = "Select Similar"
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
mode = context.tool_settings.mesh_select_mode
|
||||
if mode[0]:
|
||||
_draw_vert_slots(pie)
|
||||
elif mode[1]:
|
||||
_draw_edge_slots(pie)
|
||||
elif mode[2]:
|
||||
_draw_face_slots(pie)
|
||||
|
||||
|
||||
class PIE_MT_select_similar_mesh_edge_more(Menu):
|
||||
bl_idname = "PIE_MT_select_similar_mesh_edge_more"
|
||||
bl_label = "Select Similar: More"
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
mode = context.tool_settings.mesh_select_mode
|
||||
if mode[1]:
|
||||
_draw_edge_more_slots(pie)
|
||||
|
||||
|
||||
def _draw_vert_slots(pie):
|
||||
# 4 - LEFT
|
||||
pie.operator(SEL_SIMILAR, text="Connected Edge Count", icon='EDGESEL').type = 'VERT_EDGES'
|
||||
# 6 - RIGHT
|
||||
pie.separator()
|
||||
# 2 - BOTTOM
|
||||
pie.operator('mesh.select_similar_region', text="Face Regions", icon='FACE_MAPS')
|
||||
# 8 - TOP
|
||||
pie.operator(SEL_SIMILAR, text="Vertex Groups", icon='GROUP_VERTEX').type = 'VERT_GROUPS'
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(SEL_SIMILAR, text="Normal", icon='NORMALS_VERTEX').type = 'VERT_NORMAL'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(SEL_SIMILAR, text="Vertex Crease", icon='EDGE_CREASE').type = 'VERT_CREASE'
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(SEL_SIMILAR, text="Connected Face Count", icon='FACESEL').type = 'VERT_FACES'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
|
||||
|
||||
def _draw_edge_slots(pie):
|
||||
# 4 - LEFT
|
||||
pie.operator('wm.call_menu_pie', text="More...", icon='THREE_DOTS').name = (
|
||||
PIE_MT_select_similar_mesh_edge_more.bl_idname
|
||||
)
|
||||
# 6 - RIGHT
|
||||
pie.operator(SEL_SIMILAR, text="Seam", icon='EDGE_SEAM').type = 'EDGE_SEAM'
|
||||
# 2 - BOTTOM
|
||||
pie.operator(SEL_SIMILAR, text="Length", icon='DRIVER_DISTANCE').type = 'EDGE_LENGTH'
|
||||
# 8 - TOP
|
||||
pie.operator(SEL_SIMILAR, text="Sharp", icon='EDGE_SHARP').type = 'EDGE_SHARP'
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(SEL_SIMILAR, text="Direction", icon='ORIENTATION_NORMAL').type = 'EDGE_DIR'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(SEL_SIMILAR, text="Edge Crease", icon='EDGE_CREASE').type = 'EDGE_CREASE'
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(SEL_SIMILAR, text="Connected Face Count", icon='FACESEL').type = 'EDGE_FACES'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator(SEL_SIMILAR, text="Bevel Weight", icon='EDGE_BEVEL').type = 'EDGE_BEVEL'
|
||||
|
||||
|
||||
def _draw_edge_more_slots(pie):
|
||||
# 4 - LEFT
|
||||
# 6 - RIGHT
|
||||
pie.operator(SEL_SIMILAR, text="Freestyle Edge Marks", icon='EDGESEL').type = 'EDGE_FREESTYLE'
|
||||
# 2 - BOTTOM
|
||||
pie.operator(SEL_SIMILAR, text="Face Angle", icon='NORMALS_FACE').type = 'EDGE_FACE_ANGLE'
|
||||
# 8 - TOP
|
||||
pie.operator('mesh.select_similar_region', text="Face Regions", icon='FACE_MAPS')
|
||||
# 7 - TOP - LEFT
|
||||
# 9 - TOP - RIGHT
|
||||
# 1 - BOTTOM - LEFT
|
||||
# 3 - BOTTOM - RIGHT
|
||||
|
||||
|
||||
def _draw_face_slots(pie):
|
||||
# 4 - LEFT
|
||||
pie.operator(SEL_SIMILAR, text="Flat/Smooth", icon='SHADING_SOLID').type = 'FACE_SMOOTH'
|
||||
# 6 - RIGHT
|
||||
pie.operator(SEL_SIMILAR, text="Material", icon='MATERIAL').type = 'FACE_MATERIAL'
|
||||
# 2 - BOTTOM
|
||||
pie.operator('mesh.select_similar_region', text="Face Regions", icon='FACE_MAPS')
|
||||
# 8 - TOP
|
||||
pie.operator(SEL_SIMILAR, text="Coplanar", icon='GRID').type = 'FACE_COPLANAR'
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(SEL_SIMILAR, text="Area", icon='SNAP_FACE').type = 'FACE_AREA'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(SEL_SIMILAR, text="Sides", icon='MOD_EDGESPLIT').type = 'FACE_SIDES'
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(SEL_SIMILAR, text="Perimeter", icon='MOD_CURVE_TO_TUBE').type = 'FACE_PERIMETER'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator(SEL_SIMILAR, text="Normal", icon='NORMALS_FACE').type = 'FACE_NORMAL'
|
||||
|
||||
|
||||
class PIE_MT_select_similar_pose(Menu):
|
||||
bl_idname = "PIE_MT_select_similar_pose"
|
||||
bl_label = "Select Similar"
|
||||
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
pie.operator(POSE_OT_select_parent_bone.bl_idname, text="Parent", icon='FILE_PARENT')
|
||||
# 6 - RIGHT
|
||||
pie.operator("pose.select_grouped", text="Direct Children", icon='CON_CHILDOF').type = 'CHILDREN_IMMEDIATE'
|
||||
# 2 - BOTTOM
|
||||
pie.operator("pose.select_grouped", text="Colour", icon='COLOR').type = 'COLOR'
|
||||
# 8 - TOP
|
||||
pie.operator(POSE_OT_select_similar_custom_shape.bl_idname, text="Custom Shape", icon='OBJECT_DATA')
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator("pose.select_grouped", text="Collection", icon='OUTLINER_COLLECTION').type = 'COLLECTION'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator("pose.select_grouped", text="Siblings", icon='PARTICLES').type = 'SIBLINGS'
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(POSE_OT_select_similar_deform.bl_idname, text="Deform", icon='MOD_ARMATURE')
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator("pose.select_grouped", text="All Children", icon='CON_CHILDOF').type = 'CHILDREN'
|
||||
|
||||
|
||||
class PIE_MT_select_similar_armature(Menu):
|
||||
bl_idname = "PIE_MT_select_similar_armature"
|
||||
bl_label = "Select Similar"
|
||||
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
pie.operator(POSE_OT_select_parent_bone.bl_idname, text="Parent", icon='FILE_PARENT')
|
||||
# 6 - RIGHT
|
||||
pie.operator("armature.select_similar", text="Direct Children", icon='CON_CHILDOF').type = 'CHILDREN_IMMEDIATE'
|
||||
# 2 - BOTTOM
|
||||
pie.operator("armature.select_similar", text="Colour", icon='COLOR').type = 'COLOR'
|
||||
# 8 - TOP
|
||||
pie.operator("armature.select_similar", text="Shape", icon='OBJECT_DATA').type = 'SHAPE'
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator("armature.select_similar", text="Collection", icon='OUTLINER_COLLECTION').type = 'BONE_COLLECTION'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator("armature.select_similar", text="Siblings", icon='PARTICLES').type = 'SIBLINGS'
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.separator()
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator("armature.select_similar", text="All Children", icon='CON_CHILDOF').type = 'CHILDREN'
|
||||
|
||||
|
||||
class POSE_OT_select_similar_deform(Operator):
|
||||
bl_idname = "pose.select_similar_deform"
|
||||
bl_label = "Select Similar Deform"
|
||||
bl_description = "Select all visible bones that share the active bone's Deform setting"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not context.active_pose_bone:
|
||||
cls.poll_message_set("No active pose bone")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context: Context):
|
||||
target = context.active_pose_bone.bone.use_deform
|
||||
for pose_bone in context.visible_pose_bones:
|
||||
pose_bone.select = (pose_bone.bone.use_deform == target)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_select_similar_custom_shape(Operator):
|
||||
bl_idname = "pose.select_similar_custom_shape"
|
||||
bl_label = "Select Similar Custom Shape"
|
||||
bl_description = "Select all visible bones that share the active bone's custom shape object"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not context.active_pose_bone:
|
||||
cls.poll_message_set("No active pose bone")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context: Context):
|
||||
target = context.active_pose_bone.custom_shape
|
||||
for pose_bone in context.visible_pose_bones:
|
||||
pose_bone.select = (pose_bone.custom_shape == target)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class POSE_OT_select_parent_bone(Operator):
|
||||
"""Select parent of the current bone"""
|
||||
|
||||
bl_idname = "pose.select_parent_bone"
|
||||
bl_label = "Select Parent Bone"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
bones = (context.selected_pose_bones or context.selected_bones or [])[:]
|
||||
if not bones:
|
||||
cls.poll_message_set("No selected bones.")
|
||||
return False
|
||||
if not any((bone.parent for bone in bones)):
|
||||
cls.poll_message_set("No selected bones with parents.")
|
||||
return False
|
||||
if context.mode not in ('POSE', 'PAINT_WEIGHT', 'EDIT_ARMATURE'):
|
||||
cls.poll_message_set("Must be in Pose, Weight Paint, or Armature Edit mode.")
|
||||
return False
|
||||
if context.mode == 'PAINT_WEIGHT' and not context.pose_object:
|
||||
cls.poll_message_set("No pose mode armature.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
bones = (context.selected_pose_bones or context.selected_bones or [])[:]
|
||||
deselect_all_bones(context)
|
||||
for bone in bones:
|
||||
if bone.parent:
|
||||
print("Parent: ", bone.parent)
|
||||
reveal_and_select_bone(context, bone.parent, extend_selection=True)
|
||||
else:
|
||||
print("No parent!")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def reveal_and_select_bone(context, bone: Bone | EditBone | PoseBone, extend_selection=False, set_active=True):
|
||||
rig = active_rig(context)
|
||||
|
||||
reveal_bone(bone)
|
||||
if not extend_selection:
|
||||
deselect_all_bones(context)
|
||||
|
||||
if context.mode == 'EDIT_ARMATURE':
|
||||
if isinstance(bone, PoseBone):
|
||||
ebone = bone.id_data.data.edit_bones[bone.name]
|
||||
else:
|
||||
ebone = bone.id_data.edit_bones[bone.name]
|
||||
ebone.select = True
|
||||
ebone.select_head = True
|
||||
ebone.select_tail = True
|
||||
else:
|
||||
if isinstance(bone, PoseBone):
|
||||
pbone = bone
|
||||
bone = bone.bone
|
||||
elif isinstance(bone, Bone):
|
||||
pbone = rig.pose.bones.get(bone.name)
|
||||
pbone.hide = False
|
||||
pbone.select = True
|
||||
if set_active:
|
||||
set_active_bone(context, bone)
|
||||
|
||||
|
||||
def active_rig(context) -> Object | None:
|
||||
"""Return the active rig even if we're in weight paint mode."""
|
||||
rig = context.pose_object or context.active_object
|
||||
if not rig:
|
||||
return
|
||||
if rig.type == 'ARMATURE':
|
||||
return rig
|
||||
|
||||
|
||||
def deselect_all_bones(context):
|
||||
if context.mode == 'EDIT_ARMATURE':
|
||||
bones = context.selected_editable_bones
|
||||
else:
|
||||
bones = [pb for pb in context.selected_pose_bones]
|
||||
for bone in bones:
|
||||
bone.select = False
|
||||
if context.mode == 'EDIT_ARMATURE':
|
||||
bone.select_head = False
|
||||
bone.select_tail = False
|
||||
|
||||
|
||||
def reveal_bone(bone: Bone | EditBone | PoseBone):
|
||||
ensure_visible_bone_collection(bone)
|
||||
bone.hide = False
|
||||
|
||||
|
||||
def ensure_visible_bone_collection(bone: Bone | EditBone | PoseBone):
|
||||
"""If target bone not in any enabled collections, enable first one."""
|
||||
if isinstance(bone, PoseBone):
|
||||
bone = bone.bone
|
||||
|
||||
armature = bone.id_data
|
||||
collections = armature.collections
|
||||
|
||||
if len(bone.collections) == 0:
|
||||
return
|
||||
|
||||
if not any([coll.is_visible_effectively for coll in bone.collections]):
|
||||
coll = bone.collections[0]
|
||||
while coll:
|
||||
if collections.is_solo_active:
|
||||
coll.is_solo = True
|
||||
else:
|
||||
coll.is_visible = True
|
||||
coll = coll.parent
|
||||
|
||||
|
||||
def set_active_bone(context, bone: Bone | EditBone | PoseBone):
|
||||
"""Set the active bone, regardless of if we're in edit mode or not.
|
||||
Also account for active vertex group when in weight paint mode.
|
||||
"""
|
||||
|
||||
if not bone:
|
||||
return
|
||||
if isinstance(bone, PoseBone):
|
||||
armature = bone.id_data.data
|
||||
bone = bone.bone
|
||||
else:
|
||||
armature = bone.id_data
|
||||
|
||||
if context.mode == 'EDIT_ARMATURE':
|
||||
edit_bone = armature.edit_bones.get(bone.name)
|
||||
armature.edit_bones.active = edit_bone
|
||||
else:
|
||||
armature.bones.active = bone
|
||||
|
||||
if context.mode == 'PAINT_WEIGHT':
|
||||
if bone.name in context.active_object.vertex_groups:
|
||||
context.active_object.vertex_groups.active = context.active_object.vertex_groups[bone.name]
|
||||
|
||||
|
||||
|
||||
registry = [
|
||||
PIE_MT_select_similar_object,
|
||||
PIE_MT_select_similar_mesh,
|
||||
PIE_MT_select_similar_mesh_edge_more,
|
||||
PIE_MT_select_similar_pose,
|
||||
PIE_MT_select_similar_armature,
|
||||
POSE_OT_select_similar_deform,
|
||||
POSE_OT_select_similar_custom_shape,
|
||||
POSE_OT_select_parent_bone,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name="Object Mode",
|
||||
pie_name=PIE_MT_select_similar_object.bl_idname,
|
||||
hotkey_kwargs={'type': "G", 'value': "PRESS", 'shift': True},
|
||||
on_drag=True,
|
||||
)
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name="Mesh",
|
||||
pie_name=PIE_MT_select_similar_mesh.bl_idname,
|
||||
hotkey_kwargs={'type': "G", 'value': "PRESS", 'shift': True},
|
||||
on_drag=True,
|
||||
)
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name="Pose",
|
||||
pie_name=PIE_MT_select_similar_pose.bl_idname,
|
||||
hotkey_kwargs={'type': "G", 'value': "PRESS", 'shift': True},
|
||||
on_drag=True,
|
||||
)
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name="Armature",
|
||||
pie_name=PIE_MT_select_similar_armature.bl_idname,
|
||||
hotkey_kwargs={'type': "G", 'value': "PRESS", 'shift': True},
|
||||
on_drag=True,
|
||||
)
|
||||
@@ -1,12 +1,12 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import re
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator, Constraint, UILayout, Object
|
||||
from bpy.props import BoolProperty, StringProperty
|
||||
from bpy.types import Constraint, Context, Event, Menu, Object, Operator, OperatorProperties, UILayout
|
||||
from bpy.utils import flip_name
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
@@ -17,19 +17,19 @@ class PIE_MT_object_selection(Menu):
|
||||
bl_idname = "PIE_MT_object_selection"
|
||||
bl_label = "Object Select"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
pie.operator("object.select_parent_object", text="Parent", icon='FILE_PARENT')
|
||||
# 6 - RIGHT
|
||||
pie.operator("object.select_children_of_active", text=f"Direct Children", icon='CON_CHILDOF')
|
||||
pie.operator("object.select_children_of_active", text="Direct Children", icon='CON_CHILDOF')
|
||||
# 2 - BOTTOM
|
||||
pie.operator("object.select_all", text="Deselect All", icon='OUTLINER_DATA_POINTCLOUD').action='DESELECT'
|
||||
pie.operator("object.select_all", text="Deselect All", icon='OUTLINER_DATA_POINTCLOUD').action = 'DESELECT'
|
||||
# 8 - TOP
|
||||
pie.operator("object.select_all", text="Select All", icon='OUTLINER_OB_POINTCLOUD').action='SELECT'
|
||||
pie.operator("object.select_all", text="Select All", icon='OUTLINER_OB_POINTCLOUD').action = 'SELECT'
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator("object.select_all", text="Invert Selection", icon='CLIPUV_DEHLT').action='INVERT'
|
||||
pie.operator("object.select_all", text="Invert Selection", icon='CLIPUV_DEHLT').action = 'INVERT'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator("object.select_siblings_of_active", text="Siblings", icon='PARTICLES')
|
||||
# 1 - BOTTOM - LEFT
|
||||
@@ -46,9 +46,9 @@ class PIE_MT_object_selection_more(Menu):
|
||||
bl_idname = "PIE_MT_object_selection_more"
|
||||
bl_label = "More Object Select"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
|
||||
|
||||
# Modal selection operators.
|
||||
layout.separator()
|
||||
layout.operator("view3d.select_circle", text="Circle Select", icon='MESH_CIRCLE')
|
||||
@@ -75,36 +75,33 @@ class VIEW3D_MT_select_any_camera(Menu):
|
||||
bl_idname = "VIEW3D_MT_select_any_camera"
|
||||
bl_label = "Select Camera"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
active_cam = get_current_camera(context)
|
||||
|
||||
all_cams = [obj for obj in sorted(context.scene.objects, key=lambda o: o.name) if obj.type == 'CAMERA']
|
||||
|
||||
for cam in all_cams:
|
||||
icon = 'OUTLINER_DATA_CAMERA'
|
||||
if cam == active_cam:
|
||||
icon = 'OUTLINER_OB_CAMERA'
|
||||
layout.operator('object.select_object_by_name', text=cam.name, icon=icon).obj_name=cam.name
|
||||
layout.operator('object.select_object_by_name', text=cam.name, icon=icon).obj_name = cam.name
|
||||
|
||||
|
||||
### Object relationship selection operators.
|
||||
# Object relationship selection operators.
|
||||
|
||||
|
||||
def get_selected_parents(context) -> list:
|
||||
def get_selected_parents(context: Context) -> list[Object]:
|
||||
"""Return objects which are selected or active, and have children.
|
||||
Active object is always first."""
|
||||
objects = context.selected_objects or [context.active_object]
|
||||
parents = [o for o in objects if o.children]
|
||||
if context.active_object and context.active_object in parents:
|
||||
idx = parents.index(context.active_object)
|
||||
if idx > -1:
|
||||
parents.pop(idx)
|
||||
parents.remove(context.active_object)
|
||||
parents.insert(0, context.active_object)
|
||||
return parents
|
||||
|
||||
|
||||
def deselect_all_objects(context):
|
||||
def deselect_all_objects(context: Context):
|
||||
for obj in context.selected_objects:
|
||||
obj.select_set(False)
|
||||
|
||||
@@ -115,25 +112,20 @@ class ObjectSelectOperatorMixin:
|
||||
description="Objects that are already selected will remain selected",
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.shift:
|
||||
self.extend_selection = True
|
||||
else:
|
||||
self.extend_selection = False
|
||||
|
||||
def invoke(self, context: Context, event: Event):
|
||||
self.extend_selection = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if context.mode != 'OBJECT':
|
||||
cls.poll_message_set("Must be in Object Mode.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
if not self.extend_selection:
|
||||
deselect_all_objects(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
@@ -147,7 +139,7 @@ class OBJECT_OT_select_children_of_active(Operator, ObjectSelectOperatorMixin):
|
||||
recursive: BoolProperty(default=False, options={'SKIP_SAVE'})
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not super().poll(context):
|
||||
return False
|
||||
obj = context.active_object
|
||||
@@ -157,11 +149,11 @@ class OBJECT_OT_select_children_of_active(Operator, ObjectSelectOperatorMixin):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, props):
|
||||
def description(cls, _context: Context, props: OperatorProperties) -> str:
|
||||
recursively = " recursively" if props.recursive else ""
|
||||
return f"Select children of active object{recursively}." + "\n\nShift: Extend current selection"
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
super().execute(context)
|
||||
|
||||
counter = 0
|
||||
@@ -185,7 +177,7 @@ class OBJECT_OT_select_siblings_of_active(Operator, ObjectSelectOperatorMixin):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not super().poll(context):
|
||||
return False
|
||||
obj = context.active_object
|
||||
@@ -197,7 +189,7 @@ class OBJECT_OT_select_siblings_of_active(Operator, ObjectSelectOperatorMixin):
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
super().execute(context)
|
||||
|
||||
counter = 0
|
||||
@@ -218,7 +210,7 @@ class OBJECT_OT_select_parent_object(Operator, ObjectSelectOperatorMixin):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not super().poll(context):
|
||||
return False
|
||||
obj = context.active_object
|
||||
@@ -227,14 +219,11 @@ class OBJECT_OT_select_parent_object(Operator, ObjectSelectOperatorMixin):
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
super().execute(context)
|
||||
|
||||
active_obj = context.active_object
|
||||
|
||||
active_obj.parent.select_set(True)
|
||||
context.view_layer.objects.active = active_obj.parent
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
@@ -246,7 +235,7 @@ class OBJECT_OT_select_active_camera(Operator, ObjectSelectOperatorMixin):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
cam = get_current_camera(context)
|
||||
if not cam:
|
||||
cls.poll_message_set("No active camera.")
|
||||
@@ -256,17 +245,17 @@ class OBJECT_OT_select_active_camera(Operator, ObjectSelectOperatorMixin):
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
super().execute(context)
|
||||
cam = get_current_camera(context)
|
||||
if context.active_object and context.mode != 'MODE':
|
||||
if context.active_object and context.mode != 'MODE': # TODO: 'MODE' looks like a typo, should be 'OBJECT'?
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
cam.select_set(True)
|
||||
context.view_layer.objects.active = cam
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
### Object name-based selection operators.
|
||||
# Object name-based selection operators.
|
||||
|
||||
|
||||
class PIE_MT_select_object_name_relation(Menu):
|
||||
@@ -274,35 +263,30 @@ class PIE_MT_select_object_name_relation(Menu):
|
||||
bl_label = "Select by Name"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return bool(context.active_object)
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
active_obj = context.active_object
|
||||
|
||||
pie = layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator(
|
||||
OBJECT_OT_select_symmetry_object.bl_idname,
|
||||
text="Flip Selection",
|
||||
icon='MOD_MIRROR',
|
||||
)
|
||||
pie.operator(OBJECT_OT_select_symmetry_object.bl_idname, text="Flip Selection", icon='MOD_MIRROR')
|
||||
# 6 - RIGHT
|
||||
pie.operator('object.select_by_name_search', icon='VIEWZOOM')
|
||||
# 2 - BOTTOM
|
||||
lower_obj = context.scene.objects.get(increment_name(active_obj.name, increment=-1))
|
||||
if lower_obj:
|
||||
op = pie.operator('object.select_object_by_name', text=lower_obj.name, icon='TRIA_DOWN')
|
||||
op.obj_name = lower_obj.name
|
||||
pie.operator('object.select_object_by_name', text=lower_obj.name, icon='TRIA_DOWN').obj_name = lower_obj.name
|
||||
else:
|
||||
pie.separator()
|
||||
# 8 - TOP
|
||||
higher_obj = context.scene.objects.get(increment_name(active_obj.name, increment=1)) or context.scene.objects.get(active_obj.name + ".001")
|
||||
higher_obj = context.scene.objects.get(
|
||||
increment_name(active_obj.name, increment=1)
|
||||
) or context.scene.objects.get(active_obj.name + ".001")
|
||||
if higher_obj:
|
||||
op = pie.operator('object.select_object_by_name', text=higher_obj.name, icon='TRIA_UP')
|
||||
op.obj_name = higher_obj.name
|
||||
pie.operator('object.select_object_by_name', text=higher_obj.name, icon='TRIA_UP').obj_name = higher_obj.name
|
||||
else:
|
||||
pie.separator()
|
||||
# 7 - TOP - LEFT
|
||||
@@ -322,7 +306,7 @@ class PIE_MT_select_object_name_relation(Menu):
|
||||
else:
|
||||
pie.separator()
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator("object.select_pattern", text=f"Select Pattern...", icon='FILTER')
|
||||
pie.operator("object.select_pattern", text="Select Pattern...", icon='FILTER')
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.separator()
|
||||
|
||||
@@ -335,7 +319,7 @@ class OBJECT_OT_select_symmetry_object(Operator, ObjectSelectOperatorMixin):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
scene_obs = context.scene.objects
|
||||
sel_obs = context.selected_objects[:]
|
||||
active_obj = context.active_object
|
||||
@@ -349,7 +333,7 @@ class OBJECT_OT_select_symmetry_object(Operator, ObjectSelectOperatorMixin):
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
scene_obs = context.scene.objects
|
||||
sel_obs = context.selected_objects[:]
|
||||
active_obj = context.active_object
|
||||
@@ -357,16 +341,15 @@ class OBJECT_OT_select_symmetry_object(Operator, ObjectSelectOperatorMixin):
|
||||
flipped_objs = [scene_obs.get(flip_name(ob.name)) for ob in sel_obs]
|
||||
flipped_active = scene_obs.get(flip_name(active_obj.name))
|
||||
|
||||
notflipped = sum([flipped_obj in (obj, None) for obj, flipped_obj in zip(sel_obs, flipped_objs)])
|
||||
notflipped = sum(flipped_obj in (obj, None) for obj, flipped_obj in zip(sel_obs, flipped_objs))
|
||||
if notflipped > 0:
|
||||
self.report({'WARNING'}, f"{notflipped} objects had no opposite.")
|
||||
|
||||
super().execute(context)
|
||||
|
||||
for ob in flipped_objs:
|
||||
if not ob:
|
||||
continue
|
||||
ob.select_set(True)
|
||||
if ob:
|
||||
ob.select_set(True)
|
||||
|
||||
if flipped_active:
|
||||
context.view_layer.objects.active = flipped_active
|
||||
@@ -383,30 +366,25 @@ class OBJECT_OT_select_object_by_name_search(Operator, ObjectSelectOperatorMixin
|
||||
|
||||
obj_name: StringProperty(name="Object")
|
||||
|
||||
def invoke(self, context, _event):
|
||||
def invoke(self, context: Context, _event: Event):
|
||||
obj = context.active_object
|
||||
if obj:
|
||||
self.obj_name = obj.name
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
layout.prop_search(
|
||||
self, 'obj_name', context.scene, 'objects', icon='OBJECT_DATA'
|
||||
)
|
||||
layout.prop_search(self, 'obj_name', context.scene, 'objects', icon='OBJECT_DATA')
|
||||
layout.prop(self, 'extend_selection')
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
obj = context.scene.objects.get(self.obj_name)
|
||||
if not self.extend_selection:
|
||||
deselect_all_objects(context)
|
||||
|
||||
obj.select_set(True)
|
||||
context.view_layer.objects.active = obj
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
@@ -417,11 +395,9 @@ class OBJECT_OT_select_object_by_name(Operator, ObjectSelectOperatorMixin):
|
||||
bl_label = "Select Object By Name"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
obj_name: StringProperty(
|
||||
name="Object Name", description="Name of the object to select"
|
||||
)
|
||||
obj_name: StringProperty(name="Object Name", description="Name of the object to select")
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
obj = context.scene.objects.get(self.obj_name)
|
||||
|
||||
if not obj:
|
||||
@@ -433,27 +409,19 @@ class OBJECT_OT_select_object_by_name(Operator, ObjectSelectOperatorMixin):
|
||||
return {'CANCELLED'}
|
||||
|
||||
super().execute(context)
|
||||
|
||||
obj.select_set(True)
|
||||
context.view_layer.objects.active = obj
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def increment_name(name: str, increment=1, default_zfill=1) -> str:
|
||||
# Increment LAST number in the name.
|
||||
# Negative numbers will be clamped to 0.
|
||||
# Digit length will be preserved, so 10 will decrement to 09.
|
||||
# 99 will increment to 100, not 00.
|
||||
def increment_name(name: str, increment: int = 1, default_zfill: int = 1) -> str:
|
||||
"""Increment the last number in name.
|
||||
|
||||
# If no number was found, one will be added at the end of the base name.
|
||||
# The length of this in digits is set with the `default_zfill` param.
|
||||
|
||||
# This is not meant to be able to add a whole .001 suffix at the end of a name,
|
||||
# although we can account for the inverse of that easily enough.
|
||||
|
||||
if name.endswith(".001") and increment==-1:
|
||||
# Special case.
|
||||
Negative numbers are clamped to 0. Digit length is preserved (10 decrements to 09),
|
||||
except on overflow (99 increments to 100). If name contains no number, one is appended
|
||||
with default_zfill digit width. Special case: decrementing a '.001' suffix strips it.
|
||||
"""
|
||||
if name.endswith(".001") and increment == -1:
|
||||
return name[:-4]
|
||||
|
||||
numbers_in_name = re.findall(r'\d+', name)
|
||||
@@ -462,25 +430,27 @@ def increment_name(name: str, increment=1, default_zfill=1) -> str:
|
||||
|
||||
last = numbers_in_name[-1]
|
||||
incremented = str(max(0, int(last) + increment)).zfill(len(last))
|
||||
split = name.rsplit(last, 1)
|
||||
return incremented.join(split)
|
||||
return incremented.join(name.rsplit(last, 1))
|
||||
|
||||
|
||||
class OBJECT_MT_PIE_obj_constraint_targets(Menu):
|
||||
bl_label = "Constraint Targets"
|
||||
|
||||
@staticmethod
|
||||
def get_constraints_with_target(context):
|
||||
return [con for con in context.active_object.constraints if hasattr(con, 'target') and con.target in set(context.scene.objects)]
|
||||
def get_constraints_with_target(context: Context) -> list[Constraint]:
|
||||
return [
|
||||
con
|
||||
for con in context.active_object.constraints
|
||||
if hasattr(con, 'target') and con.target in set(context.scene.objects)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return cls.get_constraints_with_target(context)
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return bool(cls.get_constraints_with_target(context))
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
|
||||
for constraint in self.get_constraint_targets(context):
|
||||
for constraint in self.get_constraints_with_target(context):
|
||||
draw_select_constraint_target(layout, constraint)
|
||||
|
||||
|
||||
@@ -488,9 +458,11 @@ class OBJECT_MT_PIE_constrained_objects(Menu):
|
||||
bl_label = "Constrained Objects"
|
||||
|
||||
@staticmethod
|
||||
def get_dependent_constraints(context):
|
||||
def get_dependent_constraints(context: Context) -> list[Constraint]:
|
||||
dependent_ids = bpy.data.user_map()[context.active_object]
|
||||
dependent_objects = [id for id in dependent_ids if type(id)==Object and id in set(context.scene.objects)]
|
||||
dependent_objects = [
|
||||
id for id in dependent_ids if isinstance(id, Object) and id in set(context.scene.objects)
|
||||
]
|
||||
ret = []
|
||||
for dependent_object in dependent_objects:
|
||||
for con in dependent_object.constraints:
|
||||
@@ -500,32 +472,31 @@ class OBJECT_MT_PIE_constrained_objects(Menu):
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return cls.get_dependent_constraints(context)
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return bool(cls.get_dependent_constraints(context))
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
|
||||
for constraint in self.get_dependent_constraints(context):
|
||||
draw_select_constraint_owner(layout, constraint)
|
||||
|
||||
|
||||
def draw_select_constraint_target(layout, constraint):
|
||||
def draw_select_constraint_target(layout: UILayout, constraint: Constraint):
|
||||
icon = get_constraint_icon(constraint)
|
||||
layout.operator(
|
||||
'object.select_object_by_name',
|
||||
text=f"Constraint Target: {constraint.target.name} ({constraint.name})",
|
||||
icon=icon,
|
||||
).obj_name=constraint.target.name
|
||||
).obj_name = constraint.target.name
|
||||
|
||||
|
||||
def draw_select_constraint_owner(layout, constraint):
|
||||
def draw_select_constraint_owner(layout: UILayout, constraint: Constraint):
|
||||
icon = get_constraint_icon(constraint)
|
||||
layout.operator(
|
||||
'object.select_object_by_name',
|
||||
text=f"Constrained Object: {constraint.id_data.name} ({constraint.name})",
|
||||
icon=icon,
|
||||
).obj_name=constraint.id_data.name
|
||||
).obj_name = constraint.id_data.name
|
||||
|
||||
|
||||
def get_constraint_icon(constraint: Constraint) -> str:
|
||||
@@ -534,18 +505,19 @@ def get_constraint_icon(constraint: Constraint) -> str:
|
||||
return 'ACTION'
|
||||
|
||||
icons = UILayout.bl_rna.functions["prop"].parameters["icon"].enum_items.keys()
|
||||
# This magic number can change between blender versions. Last updated: 4.1.1
|
||||
# This magic number can change between Blender versions. Last updated: 4.1.1
|
||||
constraint_icon_magic_offset = 42
|
||||
return icons[UILayout.icon(constraint) - constraint_icon_magic_offset]
|
||||
|
||||
### Mesh selection UI.
|
||||
|
||||
# Mesh selection UI.
|
||||
|
||||
|
||||
class PIE_MT_mesh_selection(Menu):
|
||||
bl_idname = "PIE_MT_mesh_selection"
|
||||
bl_label = "Mesh Select"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
@@ -553,15 +525,15 @@ class PIE_MT_mesh_selection(Menu):
|
||||
# 6 - RIGHT
|
||||
pie.operator("mesh.select_more", text="Select More", icon='ADD')
|
||||
# 2 - BOTTOM
|
||||
pie.operator("mesh.select_all", text="Deselect All", icon='OUTLINER_DATA_POINTCLOUD').action='DESELECT'
|
||||
pie.operator("mesh.select_all", text="Deselect All", icon='OUTLINER_DATA_POINTCLOUD').action = 'DESELECT'
|
||||
# 8 - TOP
|
||||
pie.operator("mesh.select_all", text="Select All", icon='OUTLINER_OB_POINTCLOUD').action='SELECT'
|
||||
pie.operator("mesh.select_all", text="Select All", icon='OUTLINER_OB_POINTCLOUD').action = 'SELECT'
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator("mesh.select_all", text="Invert Selection", icon='CLIPUV_DEHLT').action='INVERT'
|
||||
pie.operator("mesh.select_all", text="Invert Selection", icon='CLIPUV_DEHLT').action = 'INVERT'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator("mesh.select_linked", text="Select Linked", icon='FILE_3D')
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator('wm.call_menu_pie', text='Select Loops...', icon='MOD_WAVE').name='PIE_MT_mesh_selection_loops'
|
||||
pie.operator('wm.call_menu_pie', text='Select Loops...', icon='MOD_WAVE').name = 'PIE_MT_mesh_selection_loops'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.menu("PIE_MT_mesh_selection_more", text="More...", icon='THREE_DOTS')
|
||||
|
||||
@@ -570,9 +542,8 @@ class PIE_MT_mesh_selection_more(Menu):
|
||||
bl_idname = "PIE_MT_mesh_selection_more"
|
||||
bl_label = "Mesh Selection Mode"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
|
||||
vert_mode, edge_mode, face_mode = context.tool_settings.mesh_select_mode
|
||||
|
||||
# Selection modes.
|
||||
@@ -606,13 +577,20 @@ class PIE_MT_mesh_selection_loops(Menu):
|
||||
bl_idname = "PIE_MT_mesh_selection_loops"
|
||||
bl_label = "Select Loop"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator('mesh.loop_multi_select', text="Edge Loops", icon='MOD_WAVE').ring=False
|
||||
# 6 - RIGHT
|
||||
pie.operator('mesh.loop_multi_select', text="Edge Rings", icon='CURVES').ring=True
|
||||
if bpy.app.version < (5, 1, 0):
|
||||
# 4 - LEFT
|
||||
pie.operator('mesh.loop_multi_select', text="Edge Loops", icon='MOD_WAVE').ring = False
|
||||
# 6 - RIGHT
|
||||
pie.operator('mesh.loop_multi_select', text="Edge Rings", icon='CURVES').ring = True
|
||||
else:
|
||||
# 4 - LEFT
|
||||
pie.operator('mesh.select_edge_loop_multi', text="Edge Loops", icon='MOD_WAVE')
|
||||
# 6 - RIGHT
|
||||
pie.operator('mesh.select_edge_ring_multi', text="Edge Rings", icon='CURVES')
|
||||
|
||||
# 2 - BOTTOM
|
||||
pie.operator('mesh.loop_to_region', text="Loop Inner Region", icon='SHADING_SOLID')
|
||||
# 8 - TOP
|
||||
@@ -620,31 +598,26 @@ class PIE_MT_mesh_selection_loops(Menu):
|
||||
|
||||
|
||||
registry = [
|
||||
PIE_MT_object_selection,
|
||||
PIE_MT_select_object_name_relation,
|
||||
PIE_MT_object_selection_more,
|
||||
PIE_MT_object_selection,
|
||||
PIE_MT_mesh_selection_loops,
|
||||
PIE_MT_mesh_selection_more,
|
||||
PIE_MT_mesh_selection,
|
||||
VIEW3D_MT_select_any_camera,
|
||||
|
||||
OBJECT_OT_select_children_of_active,
|
||||
OBJECT_OT_select_siblings_of_active,
|
||||
OBJECT_OT_select_parent_object,
|
||||
|
||||
OBJECT_OT_select_active_camera,
|
||||
|
||||
PIE_MT_select_object_name_relation,
|
||||
OBJECT_OT_select_symmetry_object,
|
||||
OBJECT_OT_select_object_by_name_search,
|
||||
OBJECT_OT_select_object_by_name,
|
||||
|
||||
OBJECT_MT_PIE_obj_constraint_targets,
|
||||
OBJECT_MT_PIE_constrained_objects,
|
||||
|
||||
PIE_MT_mesh_selection,
|
||||
PIE_MT_mesh_selection_more,
|
||||
PIE_MT_mesh_selection_loops,
|
||||
]
|
||||
|
||||
|
||||
def draw_edge_sharpness_in_traits_menu(self, context):
|
||||
def draw_edge_sharpness_in_traits_menu(self, _context: Context):
|
||||
self.layout.operator("mesh.edges_select_sharp", text="Edge Sharpness")
|
||||
|
||||
|
||||
@@ -658,7 +631,7 @@ def register():
|
||||
hotkey_kwargs={'type': "A", 'value': "PRESS"},
|
||||
on_drag=True,
|
||||
default_fallback_op='object.select_all',
|
||||
default_fallback_kwargs={"action": "TOGGLE"}
|
||||
default_fallback_kwargs={"action": "TOGGLE"},
|
||||
)
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name='Object Mode',
|
||||
@@ -672,7 +645,7 @@ def register():
|
||||
hotkey_kwargs={'type': "A", 'value': "PRESS"},
|
||||
on_drag=True,
|
||||
default_fallback_op='mesh.select_all',
|
||||
default_fallback_kwargs={"action": "TOGGLE"}
|
||||
default_fallback_kwargs={"action": "TOGGLE"},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.props import StringProperty
|
||||
from bpy.types import Context, Menu, Object, Operator, OperatorProperties
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
@@ -13,7 +13,7 @@ class PIE_MT_set_origin(Menu):
|
||||
bl_idname = "PIE_MT_set_origin"
|
||||
bl_label = "Set Origin"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
@@ -29,9 +29,7 @@ class PIE_MT_set_origin(Menu):
|
||||
icon='SNAP_PEEL_OBJECT',
|
||||
).type = 'ORIGIN_GEOMETRY'
|
||||
# 2 - BOTTOM
|
||||
pie.operator(
|
||||
"object.origin_set_to_bottom", text="Origin -> Bottom", icon='TRIA_DOWN'
|
||||
)
|
||||
pie.operator("object.origin_set_to_bottom", text="Origin -> Bottom", icon='TRIA_DOWN')
|
||||
# 8 - TOP
|
||||
pie.operator(
|
||||
"object.origin_set_to_selection",
|
||||
@@ -39,9 +37,7 @@ class PIE_MT_set_origin(Menu):
|
||||
icon='RESTRICT_SELECT_OFF',
|
||||
)
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(
|
||||
"object.origin_set_any_mode", text="Origin -> Cursor", icon='PIVOT_CURSOR'
|
||||
).type = 'ORIGIN_CURSOR'
|
||||
pie.operator("object.origin_set_any_mode", text="Origin -> Cursor", icon='PIVOT_CURSOR').type = 'ORIGIN_CURSOR'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(
|
||||
"object.origin_set_any_mode", text="Origin -> Mass", icon='SNAP_FACE_CENTER'
|
||||
@@ -53,7 +49,7 @@ class PIE_MT_set_origin(Menu):
|
||||
"object.origin_set_any_mode",
|
||||
text="Origin -> Volume",
|
||||
icon='SNAP_FACE_CENTER',
|
||||
).type = 'ORIGIN_CENTER_OF_MASS'
|
||||
).type = 'ORIGIN_CENTER_OF_VOLUME'
|
||||
|
||||
|
||||
class OBJECT_OT_set_origin_to_selection(Operator):
|
||||
@@ -62,7 +58,7 @@ class OBJECT_OT_set_origin_to_selection(Operator):
|
||||
bl_description = "Snap the origin to the selection"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
org_mode = 'OBJECT'
|
||||
if context.active_object:
|
||||
org_mode = context.active_object.mode
|
||||
@@ -85,15 +81,13 @@ class OBJECT_OT_set_origin_to_bottom(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not any(
|
||||
[ob.type in {'MESH', 'ARMATURE'} for ob in context.selected_objects]
|
||||
):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not any(ob.type in {'MESH', 'ARMATURE'} for ob in context.selected_objects):
|
||||
cls.poll_message_set("No mesh or armature objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
org_active_obj = context.active_object
|
||||
|
||||
for obj in context.selected_objects:
|
||||
@@ -108,14 +102,12 @@ class OBJECT_OT_set_origin_to_bottom(Operator):
|
||||
counter += int(self.origin_to_bottom(context, obj))
|
||||
|
||||
context.view_layer.objects.active = org_active_obj
|
||||
self.report(
|
||||
{'INFO'}, f"Moved the origins of {counter} objects to their lowest point."
|
||||
)
|
||||
self.report({'INFO'}, f"Moved the origins of {counter} objects to their lowest point.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@staticmethod
|
||||
def origin_to_bottom(context, obj) -> bool:
|
||||
def origin_to_bottom(context: Context, obj: Object) -> bool:
|
||||
if obj.type not in {'MESH', 'ARMATURE'}:
|
||||
return False
|
||||
|
||||
@@ -124,13 +116,11 @@ class OBJECT_OT_set_origin_to_bottom(Operator):
|
||||
try:
|
||||
if obj.type == 'MESH':
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
min_z = min([v.co.z for v in obj.data.vertices])
|
||||
min_z = min(v.co.z for v in obj.data.vertices)
|
||||
elif obj.type == 'ARMATURE':
|
||||
context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
min_z = min(
|
||||
[min([bone.head.z, bone.tail.z]) for bone in obj.data.edit_bones]
|
||||
)
|
||||
min_z = min(min(bone.head.z, bone.tail.z) for bone in obj.data.edit_bones)
|
||||
else:
|
||||
return False
|
||||
except ValueError:
|
||||
@@ -168,10 +158,10 @@ class OBJECT_OT_set_origin_any_mode(Operator):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, params):
|
||||
def description(cls, _context: Context, params: OperatorProperties) -> str:
|
||||
return cls.messages[params.type].replace("Snapped", "Snap")[:-1]
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
org_mode = 'OBJECT'
|
||||
if context.active_object:
|
||||
org_mode = context.active_object.mode
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Menu
|
||||
from bpy.types import Context, Menu
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
@@ -11,7 +11,7 @@ class PIE_MT_view_pie(Menu):
|
||||
bl_idname = "PIE_MT_view_pie"
|
||||
bl_label = "3D View"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
pie.operator('view3d.render_border', text="Set Render Border", icon='OBJECT_HIDDEN')
|
||||
@@ -24,15 +24,15 @@ class PIE_MT_view_pie(Menu):
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator('view3d.clip_border', text="Toggle Clipping Border", icon='MOD_BEVEL')
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator('view3d.view_all', text="Frame All", icon='VIEWZOOM').center=False
|
||||
pie.operator('view3d.view_all', text="Frame All", icon='VIEWZOOM').center = False
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator('view3d.clear_render_border', text="Clear Render Border", icon='X')
|
||||
# 3 - BOTTOM - RIGHT
|
||||
space = context.area.spaces.active
|
||||
if space.local_view:
|
||||
pie.operator("view3d.localview", text="Leave Local View", icon='SOLO_OFF').frame_selected=False
|
||||
pie.operator("view3d.localview", text="Leave Local View", icon='SOLO_OFF').frame_selected = False
|
||||
else:
|
||||
pie.operator("view3d.localview", text="Enter Local View", icon='SOLO_ON').frame_selected=False
|
||||
pie.operator("view3d.localview", text="Enter Local View", icon='SOLO_ON').frame_selected = False
|
||||
|
||||
|
||||
registry = [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from sys import platform
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.types import Context, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
@@ -14,16 +14,14 @@ class PIE_MT_window(Menu):
|
||||
bl_idname = "PIE_MT_window"
|
||||
bl_label = "Window"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator('screen.userpref_show', text="Preferences", icon='PREFERENCES')
|
||||
# 6 - RIGHT
|
||||
if not context.screen.show_fullscreen:
|
||||
pie.operator(
|
||||
"screen.screen_full_area", text="Maximize Area", icon='PIVOT_BOUNDBOX'
|
||||
)
|
||||
pie.operator("screen.screen_full_area", text="Maximize Area", icon='PIVOT_BOUNDBOX')
|
||||
else:
|
||||
pie.operator("wm.exit_area_fullscreen", icon='SCREEN_BACK')
|
||||
# 2 - BOTTOM
|
||||
@@ -59,7 +57,7 @@ class WM_OT_exit_area_fullscreen(Operator):
|
||||
bl_label = "Exit Area Fullscreen"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
if context.screen.show_fullscreen:
|
||||
bpy.ops.screen.screen_full_area()
|
||||
if context.screen.show_fullscreen:
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import platform, struct, urllib
|
||||
import platform
|
||||
import struct
|
||||
import urllib.parse
|
||||
|
||||
import bpy
|
||||
import addon_utils
|
||||
from bpy.types import AddonPreferences, KeyMap, KeyMapItem
|
||||
from bpy.props import BoolProperty
|
||||
import bpy
|
||||
from bl_ui.space_userpref import USERPREF_PT_interface_menus_pie
|
||||
from bpy.props import BoolProperty
|
||||
from bpy.types import AddonPreferences, Context, KeyMap, KeyMapItem, UILayout
|
||||
|
||||
from .bs_utils.hotkeys import draw_hotkey_list, get_sidebar
|
||||
from .bs_utils.prefs import get_addon_prefs
|
||||
from .bs_utils.hotkeys import get_sidebar, draw_hotkey_list
|
||||
|
||||
|
||||
class ExtraPies_AddonPrefs(
|
||||
AddonPreferences,
|
||||
USERPREF_PT_interface_menus_pie, # We use this class's `draw_centered` function to draw built-in pie settings.
|
||||
USERPREF_PT_interface_menus_pie, # We use this class's `draw_centered` function to draw built-in pie settings.
|
||||
):
|
||||
bl_idname = __package__
|
||||
|
||||
@@ -30,10 +33,11 @@ class ExtraPies_AddonPrefs(
|
||||
description="Enable some debugging UI",
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
draw_prefs(self.layout, context, compact=False)
|
||||
|
||||
def draw_prefs(layout, context, compact=False):
|
||||
|
||||
def draw_prefs(layout: UILayout, context: Context, compact: bool = False):
|
||||
prefs = get_addon_prefs(context)
|
||||
|
||||
sidebar = get_sidebar(context)
|
||||
@@ -46,9 +50,7 @@ def draw_prefs(layout, context, compact=False):
|
||||
if not compact:
|
||||
col = layout.column()
|
||||
row = col.row()
|
||||
row.operator('wm.url_open', text="Report Bug", icon='URL').url = (
|
||||
get_bug_report_url()
|
||||
)
|
||||
row.operator('wm.url_open', text="Report Bug", icon='URL').url = get_bug_report_url()
|
||||
row.prop(prefs, 'debug', icon='BLANK1', text="", emboss=False)
|
||||
col.prop(prefs, 'show_in_sidebar')
|
||||
|
||||
@@ -64,9 +66,12 @@ def draw_prefs(layout, context, compact=False):
|
||||
header.label(text="Hotkeys")
|
||||
if hotkeys_panel:
|
||||
hotkeys_panel.operator('window.restore_deleted_hotkeys', icon='KEY_RETURN')
|
||||
draw_hotkey_list(context, hotkeys_panel, sort_mode='BY_OPERATOR', compact=compact, button_draw_func=button_draw_func)
|
||||
draw_hotkey_list(
|
||||
context, hotkeys_panel, sort_mode='BY_OPERATOR', compact=compact, button_draw_func=button_draw_func
|
||||
)
|
||||
|
||||
def button_draw_func(layout, km: KeyMap, kmi: KeyMapItem, compact=False):
|
||||
|
||||
def button_draw_func(layout: UILayout, km: KeyMap, kmi: KeyMapItem, compact: bool = False):
|
||||
"""This function is passed as a callback to draw_hotkey_list, which will in turn call it
|
||||
with these parameters to draw the key combo UI, where we want to insert a "Drag" button."""
|
||||
split = layout.split(factor=0.65, align=True)
|
||||
@@ -75,17 +80,18 @@ def button_draw_func(layout, km: KeyMap, kmi: KeyMapItem, compact=False):
|
||||
if kmi.idname != 'wm.call_menu_pie_drag_only':
|
||||
return
|
||||
if not kmi.properties:
|
||||
sub.label(text="Missing properties. This should never happen!")
|
||||
layout.label(text="Missing properties. This should never happen!")
|
||||
return
|
||||
text = "" if compact else "Drag"
|
||||
|
||||
sub = split.row(align=True)
|
||||
sub.enabled = kmi.active
|
||||
sub.context_pointer_set("keymapitem", kmi)
|
||||
sub.use_property_split=False
|
||||
sub.use_property_split = False
|
||||
sub.prop(kmi.properties, 'on_drag', icon='MOUSE_MOVE', text=text)
|
||||
|
||||
def get_bug_report_url():
|
||||
|
||||
def get_bug_report_url() -> str:
|
||||
op_sys = "%s %d Bits\n" % (
|
||||
platform.platform(),
|
||||
struct.calcsize("P") * 8,
|
||||
@@ -114,4 +120,5 @@ def get_bug_report_url():
|
||||
+ urllib.parse.quote(op_sys)
|
||||
)
|
||||
|
||||
|
||||
registry = [ExtraPies_AddonPrefs]
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context, Panel
|
||||
|
||||
from .bs_utils.prefs import get_addon_prefs
|
||||
from .prefs import draw_prefs
|
||||
|
||||
class VIEW3D_PT_extra_pies(bpy.types.Panel):
|
||||
|
||||
class VIEW3D_PT_extra_pies(Panel):
|
||||
bl_label = "Extra Pies"
|
||||
bl_idname = "VIEW3D_PT_extra_pies"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
@@ -14,15 +16,16 @@ class VIEW3D_PT_extra_pies(bpy.types.Panel):
|
||||
bl_category = 'Extra Pies'
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
prefs = get_addon_prefs(context)
|
||||
return prefs.show_in_sidebar
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
|
||||
draw_prefs(layout, context, compact=True)
|
||||
|
||||
|
||||
registry = [
|
||||
VIEW3D_PT_extra_pies,
|
||||
]
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user