work
save startup blend for animation tab & whatnot
This commit is contained in:
@@ -3,7 +3,7 @@ schema_version = "1.0.0"
|
||||
id = "dynamiclinkmanager"
|
||||
name = "Dynamic Link Manager"
|
||||
tagline = "Character migrator and linked library tools"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
type = "add-on"
|
||||
|
||||
# Optional: Semantic Versioning
|
||||
|
||||
@@ -5,11 +5,71 @@
|
||||
|
||||
"""Character migrator: migrate animation, constraints, relations from original to replacement armature."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
import bpy
|
||||
|
||||
from ..utils import descendants, collection_containing_armature
|
||||
|
||||
|
||||
def _first_view3d_area(context):
|
||||
win = getattr(context, "window", None)
|
||||
if not win or not getattr(win, "screen", None):
|
||||
return None, None
|
||||
for area in win.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
return win, area
|
||||
return None, None
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _rep_active_for_animlayers(context, rep):
|
||||
"""Make rep the only selected active object in Object mode so Animation Layers (als.turn_on) applies to rep.
|
||||
|
||||
Does not restore the previous selection: Anim Layers and the UI expect rep to stay active after MigNLA.
|
||||
"""
|
||||
if context is None or rep is None:
|
||||
yield
|
||||
return
|
||||
vl = context.view_layer
|
||||
win, area = _first_view3d_area(context)
|
||||
# Ops need a VIEW_3D context; selection/active still use view_layer.
|
||||
if win and area:
|
||||
with context.temp_override(window=win, area=area):
|
||||
if context.mode != "OBJECT":
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
if context.mode != "OBJECT":
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
except Exception:
|
||||
pass
|
||||
for ob in vl.objects:
|
||||
try:
|
||||
ob.select_set(False)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
rep.select_set(True)
|
||||
except Exception:
|
||||
pass
|
||||
vl.objects.active = rep
|
||||
if bpy.context.view_layer == vl:
|
||||
bpy.context.view_layer.objects.active = rep
|
||||
yield
|
||||
|
||||
|
||||
def get_pair_manual(context):
|
||||
"""Return (orig_armature, rep_armature) from scene props, or (None, None)."""
|
||||
props = getattr(context.scene, "dynamic_link_manager", None)
|
||||
@@ -202,9 +262,10 @@ def _duplicate_action(src_action, suffix=".rep"):
|
||||
return new_action
|
||||
|
||||
|
||||
def run_mig_nla(orig, rep, report=None):
|
||||
def run_mig_nla(orig, rep, report=None, context=None):
|
||||
"""Migrate NLA: copy tracks and strips to replacement; or mirror action slot when no NLA (MigNLA).
|
||||
Actions are duplicated so repchar has independent copies."""
|
||||
Actions are duplicated so repchar has independent copies.
|
||||
Pass context so Animation Layers mirroring runs with rep as active object."""
|
||||
if not orig.animation_data:
|
||||
return
|
||||
ad = orig.animation_data
|
||||
@@ -249,7 +310,8 @@ def run_mig_nla(orig, rep, report=None):
|
||||
setattr(rad, prop, getattr(ad, prop))
|
||||
print(f"[DLM MigNLA] set rep {prop}={getattr(ad, prop)!r}")
|
||||
_slot_debug("Rep (after)", rad)
|
||||
_mirror_als_turn_on(orig, rep)
|
||||
with _rep_active_for_animlayers(context, rep):
|
||||
_mirror_als_turn_on(orig, rep)
|
||||
if report:
|
||||
report({"INFO"}, "No NLA detected, active action (duplicated) and slot copied to Replacement Armature.")
|
||||
return
|
||||
@@ -296,7 +358,8 @@ def run_mig_nla(orig, rep, report=None):
|
||||
new_strip.use_animated_time_cyclic = strip.use_animated_time_cyclic
|
||||
new_strip.use_sync_length = strip.use_sync_length
|
||||
prev_track = new_track
|
||||
_mirror_als_turn_on(orig, rep)
|
||||
with _rep_active_for_animlayers(context, rep):
|
||||
_mirror_als_turn_on(orig, rep)
|
||||
if report:
|
||||
_debug_als_lookup(orig)
|
||||
has_als = _has_als_anywhere(orig)
|
||||
@@ -566,9 +629,126 @@ def _find_base_body(armature, descendants_iter, rep_base_name=None):
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def _process_mig_bbody_mesh(orig_base, ob, context):
|
||||
"""Library overrides on rep mesh ob; copy shape key values and action from orig_base mesh."""
|
||||
# Debug: base body mesh state before override handling.
|
||||
_lib = getattr(ob.data, "library", None)
|
||||
_ol = getattr(ob.data, "override_library", None)
|
||||
_sys = getattr(_ol, "is_system_override", None) if _ol else None
|
||||
print(f"[DLM step6] {ob.name} data: linked={_lib is not None}, override={_ol is not None}, is_system_override={_sys}")
|
||||
# Library override: use hierarchy create (fully editable) when context available, else single-id override.
|
||||
if getattr(ob, "library", None):
|
||||
if context:
|
||||
try:
|
||||
ob = ob.override_hierarchy_create(
|
||||
context.scene, context.view_layer, do_fully_editable=True
|
||||
)
|
||||
except Exception:
|
||||
try:
|
||||
ob.override_create()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
ob.override_create()
|
||||
except Exception:
|
||||
pass
|
||||
if getattr(ob.data, "library", None):
|
||||
try:
|
||||
ob.data.override_create(remap_local_usages=True)
|
||||
# Make override user-editable (same as shift-click in data tab).
|
||||
ol = getattr(ob.data, "override_library", None)
|
||||
if ol is not None and getattr(ol, "is_system_override", None) is not None:
|
||||
try:
|
||||
ol.is_system_override = False
|
||||
except Exception as e:
|
||||
print(f"[DLM step6] {ob.name} set is_system_override=False: {e}")
|
||||
except Exception as e:
|
||||
print(f"[DLM step6] {ob.name} ob.data.override_create: {e}")
|
||||
elif getattr(ob.data, "override_library", None):
|
||||
ol = ob.data.override_library
|
||||
if getattr(ol, "is_system_override", False):
|
||||
try:
|
||||
ol.is_system_override = False
|
||||
except Exception as e:
|
||||
print(f"[DLM step6] {ob.name} set is_system_override=False: {e}")
|
||||
# Debug: state after override handling.
|
||||
_ol2 = getattr(ob.data, "override_library", None)
|
||||
_sys2 = getattr(_ol2, "is_system_override", None) if _ol2 else None
|
||||
print(f"[DLM step6] {ob.name} after: override={_ol2 is not None}, is_system_override={_sys2} (False=editable)")
|
||||
if ob.data.shape_keys:
|
||||
# Ensure we can write shape key values: override the Key block if it is linked.
|
||||
sk = ob.data.shape_keys
|
||||
if getattr(sk, "library", None):
|
||||
try:
|
||||
sk.override_create(remap_local_usages=True)
|
||||
except Exception as e:
|
||||
print(f"[DLM step6] {ob.name} shape_keys.override_create: {e}")
|
||||
# Copy shape key values from original base body to replacement (by matching key name).
|
||||
if orig_base and orig_base.data.shape_keys:
|
||||
rep_blocks = ob.data.shape_keys.key_blocks
|
||||
orig_blocks = orig_base.data.shape_keys.key_blocks
|
||||
n_copied = 0
|
||||
for orig_key in orig_blocks:
|
||||
rep_key = rep_blocks.get(orig_key.name)
|
||||
if rep_key is not None:
|
||||
rep_key.value = orig_key.value
|
||||
n_copied += 1
|
||||
print(f"[DLM step6] {ob.name} shapekey values: copied {n_copied}/{len(orig_blocks)} from {orig_base.name}")
|
||||
else:
|
||||
if not orig_base:
|
||||
print(f"[DLM step6] {ob.name} no orig base body found")
|
||||
elif not orig_base.data.shape_keys:
|
||||
print(f"[DLM step6] {ob.name} orig base body has no shape_keys")
|
||||
if not ob.data.shape_keys.animation_data:
|
||||
ob.data.shape_keys.animation_data_create()
|
||||
sk_ad = ob.data.shape_keys.animation_data
|
||||
# Prefer action (and slot) from original base body; fallback to name lookup.
|
||||
orig_sk_ad = None
|
||||
if orig_base and orig_base.data.shape_keys:
|
||||
orig_sk_ad = orig_base.data.shape_keys.animation_data
|
||||
action = None
|
||||
if orig_sk_ad and getattr(orig_sk_ad, "action", None):
|
||||
action = orig_sk_ad.action
|
||||
if action is None:
|
||||
body_name = ob.name
|
||||
action = (
|
||||
bpy.data.actions.get(body_name + "Action")
|
||||
or bpy.data.actions.get(ob.data.name + "Action")
|
||||
or bpy.data.actions.get(body_name + "Action.001")
|
||||
)
|
||||
if action:
|
||||
# Duplicate action so repchar has independent copy
|
||||
dup_action = _duplicate_action(action, suffix=".rep")
|
||||
# Copy slot-related props before action so slot is applied (Blender 4.4+).
|
||||
if orig_sk_ad and hasattr(sk_ad, "last_slot_identifier") and hasattr(orig_sk_ad, "last_slot_identifier") and orig_sk_ad.last_slot_identifier:
|
||||
sk_ad.last_slot_identifier = orig_sk_ad.last_slot_identifier
|
||||
sk_ad.action = dup_action
|
||||
if orig_sk_ad and getattr(orig_sk_ad, "action_slot", None) and getattr(sk_ad, "action_slot", None):
|
||||
try:
|
||||
sk_ad.action_slot = orig_sk_ad.action_slot
|
||||
except Exception:
|
||||
pass
|
||||
for prop in ("action_blend_type", "action_extrapolation", "action_influence"):
|
||||
if orig_sk_ad and hasattr(orig_sk_ad, prop) and hasattr(sk_ad, prop):
|
||||
try:
|
||||
setattr(sk_ad, prop, getattr(orig_sk_ad, prop))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def run_mig_bbody_shapekeys(orig, rep, rep_descendants, context=None):
|
||||
"""Replacement base body: library override (fully editable when context given), copy shapekey values, then shape-key action."""
|
||||
props = getattr(context.scene, "dynamic_link_manager", None) if context else None
|
||||
if props and getattr(props, "migbbody_manual_override", False):
|
||||
mo = getattr(props, "migbbody_orig_body", None)
|
||||
mr = getattr(props, "migbbody_rep_body", None)
|
||||
if mo and mr and mo.type == "MESH" and mr.type == "MESH":
|
||||
_process_mig_bbody_mesh(mo, mr, context)
|
||||
return
|
||||
|
||||
orig_descendants = list(descendants(orig))
|
||||
any_auto = False
|
||||
for ob in list(rep_descendants):
|
||||
if not _base_body_name_match(ob):
|
||||
continue
|
||||
@@ -578,111 +758,11 @@ def run_mig_bbody_shapekeys(orig, rep, rep_descendants, context=None):
|
||||
break
|
||||
else:
|
||||
continue
|
||||
any_auto = True
|
||||
orig_base = _find_base_body(orig, orig_descendants, rep_base_name=ob.name)
|
||||
# Debug: base body mesh state before override handling.
|
||||
_lib = getattr(ob.data, "library", None)
|
||||
_ol = getattr(ob.data, "override_library", None)
|
||||
_sys = getattr(_ol, "is_system_override", None) if _ol else None
|
||||
print(f"[DLM step6] {ob.name} data: linked={_lib is not None}, override={_ol is not None}, is_system_override={_sys}")
|
||||
# Library override: use hierarchy create (fully editable) when context available, else single-id override.
|
||||
if getattr(ob, "library", None):
|
||||
if context:
|
||||
try:
|
||||
ob = ob.override_hierarchy_create(
|
||||
context.scene, context.view_layer, do_fully_editable=True
|
||||
)
|
||||
except Exception:
|
||||
try:
|
||||
ob.override_create()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
ob.override_create()
|
||||
except Exception:
|
||||
pass
|
||||
if getattr(ob.data, "library", None):
|
||||
try:
|
||||
ob.data.override_create(remap_local_usages=True)
|
||||
# Make override user-editable (same as shift-click in data tab).
|
||||
ol = getattr(ob.data, "override_library", None)
|
||||
if ol is not None and getattr(ol, "is_system_override", None) is not None:
|
||||
try:
|
||||
ol.is_system_override = False
|
||||
except Exception as e:
|
||||
print(f"[DLM step6] {ob.name} set is_system_override=False: {e}")
|
||||
except Exception as e:
|
||||
print(f"[DLM step6] {ob.name} ob.data.override_create: {e}")
|
||||
elif getattr(ob.data, "override_library", None):
|
||||
ol = ob.data.override_library
|
||||
if getattr(ol, "is_system_override", False):
|
||||
try:
|
||||
ol.is_system_override = False
|
||||
except Exception as e:
|
||||
print(f"[DLM step6] {ob.name} set is_system_override=False: {e}")
|
||||
# Debug: state after override handling.
|
||||
_ol2 = getattr(ob.data, "override_library", None)
|
||||
_sys2 = getattr(_ol2, "is_system_override", None) if _ol2 else None
|
||||
print(f"[DLM step6] {ob.name} after: override={_ol2 is not None}, is_system_override={_sys2} (False=editable)")
|
||||
if ob.data.shape_keys:
|
||||
# Ensure we can write shape key values: override the Key block if it is linked.
|
||||
sk = ob.data.shape_keys
|
||||
if getattr(sk, "library", None):
|
||||
try:
|
||||
sk.override_create(remap_local_usages=True)
|
||||
except Exception as e:
|
||||
print(f"[DLM step6] {ob.name} shape_keys.override_create: {e}")
|
||||
# Copy shape key values from original base body to replacement (by matching key name).
|
||||
if orig_base and orig_base.data.shape_keys:
|
||||
rep_blocks = ob.data.shape_keys.key_blocks
|
||||
orig_blocks = orig_base.data.shape_keys.key_blocks
|
||||
n_copied = 0
|
||||
for orig_key in orig_blocks:
|
||||
rep_key = rep_blocks.get(orig_key.name)
|
||||
if rep_key is not None:
|
||||
rep_key.value = orig_key.value
|
||||
n_copied += 1
|
||||
print(f"[DLM step6] {ob.name} shapekey values: copied {n_copied}/{len(orig_blocks)} from {orig_base.name}")
|
||||
else:
|
||||
if not orig_base:
|
||||
print(f"[DLM step6] {ob.name} no orig base body found for armature {orig.name}")
|
||||
elif not orig_base.data.shape_keys:
|
||||
print(f"[DLM step6] {ob.name} orig base body has no shape_keys")
|
||||
if not ob.data.shape_keys.animation_data:
|
||||
ob.data.shape_keys.animation_data_create()
|
||||
sk_ad = ob.data.shape_keys.animation_data
|
||||
# Prefer action (and slot) from original base body; fallback to name lookup.
|
||||
orig_sk_ad = None
|
||||
if orig_base and orig_base.data.shape_keys:
|
||||
orig_sk_ad = orig_base.data.shape_keys.animation_data
|
||||
action = None
|
||||
if orig_sk_ad and getattr(orig_sk_ad, "action", None):
|
||||
action = orig_sk_ad.action
|
||||
if action is None:
|
||||
body_name = ob.name
|
||||
action = (
|
||||
bpy.data.actions.get(body_name + "Action")
|
||||
or bpy.data.actions.get(ob.data.name + "Action")
|
||||
or bpy.data.actions.get(body_name + "Action.001")
|
||||
)
|
||||
if action:
|
||||
# Duplicate action so repchar has independent copy
|
||||
dup_action = _duplicate_action(action, suffix=".rep")
|
||||
# Copy slot-related props before action so slot is applied (Blender 4.4+).
|
||||
if orig_sk_ad and hasattr(sk_ad, "last_slot_identifier") and hasattr(orig_sk_ad, "last_slot_identifier") and orig_sk_ad.last_slot_identifier:
|
||||
sk_ad.last_slot_identifier = orig_sk_ad.last_slot_identifier
|
||||
sk_ad.action = dup_action
|
||||
if orig_sk_ad and getattr(orig_sk_ad, "action_slot", None) and getattr(sk_ad, "action_slot", None):
|
||||
try:
|
||||
sk_ad.action_slot = orig_sk_ad.action_slot
|
||||
except Exception:
|
||||
pass
|
||||
for prop in ("action_blend_type", "action_extrapolation", "action_influence"):
|
||||
if orig_sk_ad and hasattr(orig_sk_ad, prop) and hasattr(sk_ad, prop):
|
||||
try:
|
||||
setattr(sk_ad, prop, getattr(orig_sk_ad, prop))
|
||||
except Exception:
|
||||
pass
|
||||
_process_mig_bbody_mesh(orig_base, ob, context)
|
||||
if not any_auto and props is not None:
|
||||
props.migbbody_manual_override = True
|
||||
|
||||
|
||||
def run_full_migration(context):
|
||||
@@ -703,7 +783,7 @@ def run_full_migration(context):
|
||||
|
||||
try:
|
||||
run_copy_attr(orig, rep)
|
||||
run_mig_nla(orig, rep)
|
||||
run_mig_nla(orig, rep, context=context)
|
||||
run_mig_cust_props(orig, rep)
|
||||
run_mig_bone_const(orig, rep, orig_to_rep)
|
||||
run_retarg_relatives(orig, rep, rep_descendants, orig_to_rep)
|
||||
|
||||
@@ -9,7 +9,7 @@ from bpy.types import Operator
|
||||
from bpy.props import StringProperty, BoolProperty
|
||||
from bpy.props import StringProperty, IntProperty
|
||||
|
||||
from ..utils import collection_containing_armature
|
||||
from ..utils.remove_original import resolve_collection_for_remove_original
|
||||
|
||||
ADDON_NAME = __package__.rsplit(".", 1)[0] if "." in __package__ else __package__
|
||||
|
||||
@@ -305,7 +305,7 @@ class DLM_OT_migrator_migrate_nla(Operator):
|
||||
return {"CANCELLED"}
|
||||
try:
|
||||
from ..ops.migrator import run_mig_nla
|
||||
run_mig_nla(orig, rep, report=self.report)
|
||||
run_mig_nla(orig, rep, report=self.report, context=context)
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
@@ -399,7 +399,16 @@ class DLM_OT_migrator_basebody_shapekeys(Operator):
|
||||
from ..utils import descendants
|
||||
rep_descendants = descendants(rep)
|
||||
run_mig_bbody_shapekeys(orig, rep, rep_descendants, context)
|
||||
self.report({"INFO"}, "Migrate BaseBody shapekeys done.")
|
||||
props = context.scene.dynamic_link_manager
|
||||
if props.migbbody_manual_override and (
|
||||
not props.migbbody_orig_body or not props.migbbody_rep_body
|
||||
):
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
"MigBBody: no CC-style base mesh matched. Pick Original/Replacement body meshes, then run again.",
|
||||
)
|
||||
else:
|
||||
self.report({"INFO"}, "Migrate BaseBody shapekeys done.")
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
@@ -531,9 +540,10 @@ class DLM_OT_migrator_remove_original(Operator):
|
||||
if removed_actions:
|
||||
self.report({"INFO"}, f"Removed {len(removed_actions)} action(s) from original")
|
||||
|
||||
props = context.scene.dynamic_link_manager
|
||||
rig_family = getattr(props, "migrator_rig_family", "RIGIFY")
|
||||
try:
|
||||
# Try to find and delete the collection containing the original character
|
||||
coll = collection_containing_armature(orig)
|
||||
coll = resolve_collection_for_remove_original(orig, rig_family, context.scene, rep)
|
||||
if coll:
|
||||
coll_name = coll.name # Store name BEFORE removal (RNA invalidates after remove)
|
||||
context.scene.dynamic_link_manager.original_character = None
|
||||
@@ -541,10 +551,14 @@ class DLM_OT_migrator_remove_original(Operator):
|
||||
bpy.data.collections.remove(coll)
|
||||
self.report({"INFO"}, f"Removed collection: {coll_name}")
|
||||
except Exception as remove_err:
|
||||
# Collection may have already been removed by another process
|
||||
self.report({"WARNING"}, f"Collection {coll_name} removal issue: {remove_err}")
|
||||
try:
|
||||
bpy.data.objects.remove(orig, do_unlink=True)
|
||||
self.report({"INFO"}, f"Removed original object: {name}")
|
||||
except Exception as e2:
|
||||
self.report({"ERROR"}, f"Could not remove original after collection failure: {e2}")
|
||||
return {"CANCELLED"}
|
||||
else:
|
||||
# Fallback: just delete the armature object
|
||||
bpy.data.objects.remove(orig, do_unlink=True)
|
||||
context.scene.dynamic_link_manager.original_character = None
|
||||
self.report({"INFO"}, f"Removed original character: {name}")
|
||||
|
||||
@@ -76,6 +76,8 @@ class DLM_PT_main_panel(Panel):
|
||||
box = layout.box()
|
||||
box.label(text="Character Migrator")
|
||||
row = box.row()
|
||||
row.prop(props, "migrator_rig_family", expand=True)
|
||||
row = box.row()
|
||||
row.prop(props, "migrator_mode", text="Automatic pair discovery")
|
||||
row = box.row()
|
||||
row.prop(props, "original_character", text="Original")
|
||||
@@ -96,6 +98,14 @@ class DLM_PT_main_panel(Panel):
|
||||
# Situational
|
||||
situational_box = layout.box()
|
||||
situational_box.label(text="Situational Fixes", icon="QUESTION")
|
||||
row = situational_box.row()
|
||||
row.prop(props, "migbbody_manual_override", text="Manual body meshes")
|
||||
row = situational_box.row()
|
||||
row.enabled = props.migbbody_manual_override
|
||||
row.prop(props, "migbbody_orig_body", text="Original body")
|
||||
row = situational_box.row()
|
||||
row.enabled = props.migbbody_manual_override
|
||||
row.prop(props, "migbbody_rep_body", text="Replacement body")
|
||||
row = situational_box.row(align=True)
|
||||
row.operator("dlm.migrator_basebody_shapekeys", text="MigBBodyShapeKeys", icon="SHAPEKEY_DATA")
|
||||
row = situational_box.row(align=True)
|
||||
|
||||
@@ -5,7 +5,14 @@
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import IntProperty, StringProperty, BoolProperty, CollectionProperty, PointerProperty
|
||||
from bpy.props import (
|
||||
IntProperty,
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
PointerProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
|
||||
class SearchPathItem(PropertyGroup):
|
||||
@@ -41,6 +48,17 @@ class DynamicLinkManagerProperties(PropertyGroup):
|
||||
)
|
||||
selected_asset_path: StringProperty(name="Selected Asset Path", default="")
|
||||
|
||||
# Character migrator: rig family (future ARP-specific migration; Remove Original uses it for collection resolution)
|
||||
migrator_rig_family: EnumProperty(
|
||||
name="Rig family",
|
||||
description="Rigify vs Auto-Rig Pro: affects Remove Original collection detection and future bone heuristics",
|
||||
items=(
|
||||
("RIGIFY", "Rigify", "Rigify rig naming and defaults"),
|
||||
("ARP", "ARP", "Auto-Rig Pro rig naming and collection behavior"),
|
||||
),
|
||||
default="RIGIFY",
|
||||
)
|
||||
|
||||
# Character migrator (manual mode)
|
||||
migrator_mode: BoolProperty(
|
||||
name="Automatic",
|
||||
@@ -60,6 +78,28 @@ class DynamicLinkManagerProperties(PropertyGroup):
|
||||
poll=lambda self, obj: obj and obj.type == "ARMATURE",
|
||||
)
|
||||
|
||||
# MigBBody: manual mesh pair when CC/iClone-style auto-detection fails
|
||||
migbbody_manual_override: BoolProperty(
|
||||
name="Manual body meshes",
|
||||
description=(
|
||||
"Enable to pick original and replacement body meshes manually. "
|
||||
"Auto-enables if MigBBody cannot find a base mesh (non-CC rigs)."
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
migbbody_orig_body: PointerProperty(
|
||||
name="Original body",
|
||||
description="Original character body mesh (shape key source)",
|
||||
type=bpy.types.Object,
|
||||
poll=lambda self, obj: obj and obj.type == "MESH",
|
||||
)
|
||||
migbbody_rep_body: PointerProperty(
|
||||
name="Replacement body",
|
||||
description="Replacement character body mesh (shape key target)",
|
||||
type=bpy.types.Object,
|
||||
poll=lambda self, obj: obj and obj.type == "MESH",
|
||||
)
|
||||
|
||||
# Tweak tools (collapsible section)
|
||||
tweak_tools_section_expanded: BoolProperty(
|
||||
name="Tweak Tools Expanded",
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
"""Resolve which collection to remove when deleting the original character armature."""
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def _parent_collection(scene, coll):
|
||||
"""Return the parent of coll in the scene tree, or None if not found."""
|
||||
if coll is None:
|
||||
return None
|
||||
master = scene.collection
|
||||
# Blender 5+: children.__contains__ expects collection name strings, not Collection objects.
|
||||
if coll.name in master.children:
|
||||
return master
|
||||
for p in bpy.data.collections:
|
||||
if coll.name in p.children:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def _depth_from_scene_root(scene, coll):
|
||||
"""Depth: number of steps walking up from coll until scene.collection (deeper = larger)."""
|
||||
d = 0
|
||||
cur = coll
|
||||
while cur is not None and cur != scene.collection:
|
||||
d += 1
|
||||
cur = _parent_collection(scene, cur)
|
||||
return d
|
||||
|
||||
|
||||
def _walk_up_chain(scene, coll):
|
||||
"""Return [inner, ..., top] where top is a direct child of scene.collection (or highest ancestor)."""
|
||||
chain = []
|
||||
cur = coll
|
||||
while cur is not None and cur != scene.collection:
|
||||
chain.append(cur)
|
||||
cur = _parent_collection(scene, cur)
|
||||
return chain
|
||||
|
||||
|
||||
def _collection_contains_object_recursive(coll, ob):
|
||||
"""True if ob is in coll.objects or in any descendant collection (recursive)."""
|
||||
if coll is None or ob is None:
|
||||
return False
|
||||
for o in coll.objects:
|
||||
if o == ob:
|
||||
return True
|
||||
for child in coll.children:
|
||||
if _collection_contains_object_recursive(child, ob):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _deepest_users_collection(scene, armature):
|
||||
"""Among armature.users_collection, pick the most nested (max depth) as the inner anchor."""
|
||||
colls = list(getattr(armature, "users_collection", []) or [])
|
||||
if not colls:
|
||||
return None
|
||||
best = colls[0]
|
||||
best_d = _depth_from_scene_root(scene, best)
|
||||
for c in colls[1:]:
|
||||
d = _depth_from_scene_root(scene, c)
|
||||
if d > best_d:
|
||||
best_d = d
|
||||
best = c
|
||||
return best
|
||||
|
||||
|
||||
def _library_ptr(lib):
|
||||
"""Stable id for comparing library datablocks."""
|
||||
return lib.filepath if lib else None
|
||||
|
||||
|
||||
def _pick_remove_target_from_chain(orig, chain, rig_family):
|
||||
"""
|
||||
From walk-up chain [inner..top], choose one collection to remove.
|
||||
Prefer the topmost under scene (chain[-1]) so nested linked setups remove the whole instance.
|
||||
If orig is linked, prefer the outermost chain entry whose library matches orig.library.
|
||||
"""
|
||||
if not chain:
|
||||
return None
|
||||
orig_lib = _library_ptr(getattr(orig, "library", None))
|
||||
|
||||
# Linked armature: outermost in chain from same library file (reversed = top -> inner)
|
||||
if orig_lib:
|
||||
for c in reversed(chain):
|
||||
if _library_ptr(getattr(c, "library", None)) == orig_lib:
|
||||
return c
|
||||
|
||||
# Local override: prefer outermost collection that participates in override hierarchy
|
||||
if getattr(orig, "override_library", None):
|
||||
for c in reversed(chain):
|
||||
if getattr(c, "override_library", None) is not None:
|
||||
return c
|
||||
|
||||
# Rigify: optional name hint — prefer collection whose name matches base armature name
|
||||
if rig_family == "RIGIFY":
|
||||
name = orig.name
|
||||
base = name.replace("_Rigify", "").replace(".001", "").rstrip("0123456789.")
|
||||
for c in reversed(chain):
|
||||
if c.name == base or base in c.name or c.name in name:
|
||||
return c
|
||||
|
||||
# Default: top of chain (direct child of scene.collection subtree root)
|
||||
return chain[-1]
|
||||
|
||||
|
||||
def resolve_collection_for_remove_original(orig, rig_family, scene, rep=None):
|
||||
"""
|
||||
Return a collection to remove for Remove Original, or None to fall back to object-only removal.
|
||||
|
||||
Walks up from the deepest users_collection so nested linked rigs remove the outer instance,
|
||||
not an inner linked child collection.
|
||||
|
||||
If rep is the replacement armature, never remove a collection whose subtree contains rep
|
||||
(avoids deleting both characters when they share a parent collection).
|
||||
|
||||
rig_family: 'RIGIFY' | 'ARP' (ARP skips Rigify name heuristics in _pick_remove_target_from_chain).
|
||||
"""
|
||||
if not orig or orig.type != "ARMATURE" or orig.name not in bpy.data.objects:
|
||||
return None
|
||||
|
||||
inner = _deepest_users_collection(scene, orig)
|
||||
if inner is None:
|
||||
return None
|
||||
|
||||
chain = _walk_up_chain(scene, inner)
|
||||
if not chain:
|
||||
return None
|
||||
|
||||
if rep is not None and rep.name in bpy.data.objects:
|
||||
chain = [c for c in chain if not _collection_contains_object_recursive(c, rep)]
|
||||
if not chain:
|
||||
return None
|
||||
|
||||
return _pick_remove_target_from_chain(orig, chain, rig_family)
|
||||
Reference in New Issue
Block a user