2026-03-11_4
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,647 @@
|
||||
import bpy
|
||||
import os
|
||||
import json
|
||||
import numpy
|
||||
import re
|
||||
from .main_functions import get_preferences
|
||||
from ..classes import BoneWidgetImportData, Widget, ColorSet
|
||||
from .. import __package__
|
||||
|
||||
JSON_DEFAULT_WIDGETS = "widgets.json"
|
||||
JSON_USER_WIDGETS = "user_widgets.json"
|
||||
JSON_COLOR_PRESETS = "custom_color_sets.json"
|
||||
|
||||
widget_data = {}
|
||||
|
||||
|
||||
def get_addon_dir():
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
|
||||
def get_custom_dir():
|
||||
pref = get_preferences(bpy.context)
|
||||
if pref.use_default_location:
|
||||
return bpy.utils.extension_path_user(
|
||||
package=__package__, path="bone_widget_custom_data", create=True
|
||||
)
|
||||
else:
|
||||
return pref.user_data_location
|
||||
|
||||
|
||||
def get_default_image_dir(image_folder):
|
||||
return os.path.abspath(os.path.join(get_addon_dir(), image_folder))
|
||||
|
||||
|
||||
def get_custom_image_dir(image_folder):
|
||||
return os.path.abspath(os.path.join(get_custom_dir(), image_folder))
|
||||
|
||||
|
||||
def get_custom_color_preset_dir():
|
||||
return os.path.abspath(os.path.join(get_custom_dir(), JSON_COLOR_PRESETS))
|
||||
|
||||
|
||||
def get_widget_directory(file):
|
||||
if file == JSON_DEFAULT_WIDGETS:
|
||||
return os.path.join(get_addon_dir(), file)
|
||||
elif file == JSON_USER_WIDGETS:
|
||||
return os.path.join(get_custom_dir(), file)
|
||||
|
||||
|
||||
def validate_json_data(data: dict, required_keys: tuple, can_be_empty: bool = True) -> bool:
|
||||
required_keys = set(required_keys)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return False
|
||||
|
||||
# Check if all required keys are present
|
||||
if not required_keys.issubset(data.keys()):
|
||||
return False
|
||||
|
||||
if not can_be_empty:
|
||||
# Check if values are not empty
|
||||
if any(not data[key] for key in required_keys):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def update_preview_collection():
|
||||
from .functions.preview_functions import create_preview_collection
|
||||
create_preview_collection()
|
||||
|
||||
|
||||
def objectDataToDico(object, custom_image):
|
||||
verts = []
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
mesh = object.evaluated_get(depsgraph).to_mesh()
|
||||
|
||||
for v in mesh.vertices:
|
||||
verts.append(tuple(numpy.array(tuple(v.co)) *
|
||||
(object.scale[0], object.scale[1], object.scale[2])))
|
||||
|
||||
polygons = []
|
||||
for p in mesh.polygons:
|
||||
polygons.append(tuple(p.vertices))
|
||||
|
||||
edges = []
|
||||
for e in mesh.edges:
|
||||
edges.append(e.key)
|
||||
|
||||
custom_image = custom_image if custom_image != "" else "user_defined.png"
|
||||
|
||||
wgts = {"vertices": verts, "edges": edges,
|
||||
"faces": polygons, "image": custom_image}
|
||||
|
||||
return (wgts)
|
||||
|
||||
|
||||
def read_widgets(filename=""):
|
||||
global widget_data
|
||||
wgts = {}
|
||||
|
||||
if not filename:
|
||||
files = [JSON_DEFAULT_WIDGETS, JSON_USER_WIDGETS]
|
||||
else:
|
||||
files = [filename]
|
||||
|
||||
for file in files:
|
||||
jsonFile = get_widget_directory(file)
|
||||
if os.path.exists(jsonFile):
|
||||
f = open(jsonFile, 'r')
|
||||
wgts.update(json.load(f))
|
||||
f.close()
|
||||
|
||||
if not filename: # if both files have been read
|
||||
widget_data = wgts.copy()
|
||||
|
||||
return (wgts)
|
||||
|
||||
|
||||
def get_widget_data(widget):
|
||||
return widget_data[widget]
|
||||
|
||||
|
||||
def write_widgets(wgts, file):
|
||||
jsonFile = get_widget_directory(file)
|
||||
# if os.path.exists(jsonFile):
|
||||
f = open(jsonFile, 'w')
|
||||
f.write(json.dumps(wgts))
|
||||
f.close()
|
||||
|
||||
|
||||
def add_remove_widgets(context, addOrRemove, items, widgets, widget_name="", custom_image=""):
|
||||
wgts = {}
|
||||
|
||||
# file from where the widget should be read or written to
|
||||
file = JSON_USER_WIDGETS
|
||||
|
||||
widget_items = []
|
||||
for widget_item in items:
|
||||
widget_items.append(widget_item[1])
|
||||
|
||||
activeShape = None
|
||||
ob_name = None
|
||||
return_message = ""
|
||||
if addOrRemove == 'add':
|
||||
wgts = read_widgets(file)
|
||||
bw_widget_prefix = get_preferences(context).widget_prefix
|
||||
for ob in widgets:
|
||||
if not widget_name:
|
||||
if ob.name.startswith(bw_widget_prefix):
|
||||
ob_name = ob.name[len(bw_widget_prefix):]
|
||||
else:
|
||||
ob_name = ob.name
|
||||
else:
|
||||
ob_name = widget_name
|
||||
|
||||
if (ob_name) not in widget_items:
|
||||
widget_items.append(ob_name)
|
||||
wgts[ob_name] = objectDataToDico(ob, custom_image)
|
||||
activeShape = ob_name
|
||||
return_message = "Widget - " + ob_name + " has been added!"
|
||||
|
||||
elif addOrRemove == 'remove':
|
||||
user_widgets = read_widgets(file)
|
||||
if widgets in user_widgets:
|
||||
wgts = user_widgets
|
||||
else:
|
||||
file = JSON_DEFAULT_WIDGETS
|
||||
wgts = read_widgets(file)
|
||||
|
||||
del wgts[widgets]
|
||||
if widgets in widget_items:
|
||||
widget_index = widget_items.index(widgets)
|
||||
activeShape = widget_items[widget_index +
|
||||
1] if widget_index == 0 else widget_items[widget_index - 1]
|
||||
widget_items.remove(widgets)
|
||||
return_message = "Widget - " + widgets + " has been removed!"
|
||||
|
||||
if activeShape is not None:
|
||||
|
||||
write_widgets(wgts, file)
|
||||
|
||||
# update the preview panel
|
||||
update_preview_collection()
|
||||
|
||||
# trigger an update and display widget
|
||||
bpy.context.window_manager.widget_list = activeShape
|
||||
|
||||
return 'INFO', return_message
|
||||
elif ob_name is not None:
|
||||
return 'WARNING', "Widget - " + ob_name + " already exists!"
|
||||
|
||||
|
||||
def export_widget_library(filepath):
|
||||
wgts = read_widgets(JSON_USER_WIDGETS)
|
||||
|
||||
if wgts:
|
||||
# variables needed for exporting widgets
|
||||
dest_dir = os.path.dirname(filepath)
|
||||
json_dir = get_custom_dir()
|
||||
image_folder = 'custom_thumbnails'
|
||||
custom_image_dir = get_custom_image_dir(image_folder)
|
||||
|
||||
filename = os.path.basename(filepath)
|
||||
if not filename:
|
||||
filename = "widget_library.zip"
|
||||
elif not filename.endswith('.zip'):
|
||||
filename += ".zip"
|
||||
|
||||
# start the zipping process
|
||||
try:
|
||||
from zipfile import ZipFile
|
||||
with ZipFile(os.path.join(dest_dir, filename), "w") as zip:
|
||||
# write the json file
|
||||
file = os.path.join(json_dir, JSON_USER_WIDGETS)
|
||||
arcname = os.path.basename(file)
|
||||
zip.write(file, arcname=arcname)
|
||||
|
||||
# write the custom images if present
|
||||
if os.path.exists(custom_image_dir):
|
||||
from pathlib import Path
|
||||
for filepath in Path(custom_image_dir).iterdir():
|
||||
arcname = os.path.join(
|
||||
image_folder, os.path.basename(filepath))
|
||||
zip.write(filepath, arcname=arcname)
|
||||
except Exception as e:
|
||||
print("Error exporting widget library: ", e)
|
||||
return 0
|
||||
|
||||
return len(wgts)
|
||||
|
||||
|
||||
def import_widget_library(filepath, action=""):
|
||||
required_data_keys = ("vertices", "faces", "edges", "image") # json data
|
||||
wgts = {}
|
||||
|
||||
from zipfile import ZipFile
|
||||
# dest_dir = os.path.abspath(os.path.join(get_addon_dir(), '..'))
|
||||
dest_dir = bpy.app.tempdir
|
||||
|
||||
widget_import = BoneWidgetImportData()
|
||||
|
||||
widget_import.import_type = "widget"
|
||||
|
||||
if os.path.exists(filepath) and action:
|
||||
try:
|
||||
|
||||
with ZipFile(filepath, 'r') as zip_file:
|
||||
# extract images
|
||||
for file in zip_file.namelist():
|
||||
if file.startswith('custom_thumbnails/'):
|
||||
zip_file.extract(file, dest_dir)
|
||||
elif file.endswith('.json'): # extract data from the .json file
|
||||
f = zip_file.read(file)
|
||||
json_data = f.decode('utf8').replace("'", '"')
|
||||
wgts = json.loads(json_data)
|
||||
|
||||
# validate wgts data type
|
||||
if not isinstance(wgts, dict):
|
||||
raise TypeError(
|
||||
f"Expected a dictionary, but got {type(wgts).__name__}")
|
||||
|
||||
current_wgts = read_widgets(JSON_USER_WIDGETS)
|
||||
|
||||
# check for duplicate names
|
||||
for name, data in sorted(wgts.items()): # sorting by keys
|
||||
widget_import.total_num_imports += 1
|
||||
# validate json data
|
||||
if not validate_json_data(data, required_data_keys):
|
||||
widget_import.failed_imports.append(Widget(name, data))
|
||||
continue
|
||||
|
||||
if action == "ASK":
|
||||
widget_import.skipped_imports.append(Widget(name, data))
|
||||
elif action == "OVERWRITE":
|
||||
widget_import.imported_items.append(Widget(name, data))
|
||||
elif action == "SKIP":
|
||||
# check for duplicates
|
||||
data_match = data == current_wgts[name]
|
||||
if data_match:
|
||||
widget_import.skipped_imports.append(
|
||||
Widget(name, data))
|
||||
# widget_import.duplicate_imports.update({name : data})
|
||||
elif name not in current_wgts:
|
||||
widget_import.imported_items.append(Widget(name, data))
|
||||
else:
|
||||
widget_import.skipped_imports.append(
|
||||
Widget(name, data))
|
||||
else:
|
||||
widget_import.failed_imports.append(Widget(name, data))
|
||||
|
||||
except TypeError as e: # Handle data type errors specifically
|
||||
print(f"Error while importing widget library: {e}")
|
||||
widget_import.json_import_error = True
|
||||
except Exception as e:
|
||||
print(f"Error while importing widget library: {e}")
|
||||
for name, data in wgts.items():
|
||||
widget_import.failed_imports.append(Widget(name, data))
|
||||
widget_import.total_num_imports = widget_import.failed()
|
||||
return widget_import
|
||||
|
||||
|
||||
def update_widget_library(new_widgets: dict[str, dict[str, list | str]],
|
||||
new_images: set[str], zip_filepath: str) -> None:
|
||||
# store the currently selected widget
|
||||
current_widget = bpy.context.window_manager.widget_list
|
||||
|
||||
wgts = read_widgets(JSON_USER_WIDGETS)
|
||||
wgts.update(new_widgets)
|
||||
|
||||
write_widgets(wgts, JSON_USER_WIDGETS)
|
||||
|
||||
# extract any images needed from zip library
|
||||
if new_images:
|
||||
from zipfile import ZipFile
|
||||
dest_dir = get_custom_dir()
|
||||
if os.path.exists(zip_filepath):
|
||||
try:
|
||||
with ZipFile(zip_filepath, 'r') as zip_file:
|
||||
for file in zip_file.namelist():
|
||||
if file.startswith('custom_thumbnails/') and file.split("/")[1] in new_images:
|
||||
zip_file.extract(file, dest_dir)
|
||||
except Exception as e:
|
||||
print("Failed to extract custom images: ", e)
|
||||
else:
|
||||
print("zip file path doesn't exist!! - ", zip_filepath)
|
||||
|
||||
# update the preview panel
|
||||
update_preview_collection()
|
||||
|
||||
# trigger an update and display original but updated widget
|
||||
bpy.context.window_manager.widget_list = current_widget
|
||||
|
||||
|
||||
def update_custom_image(image_name):
|
||||
current_widget = bpy.context.window_manager.widget_list
|
||||
current_widget_data = get_widget_data(current_widget)
|
||||
|
||||
# swap out the image
|
||||
current_widget_data['image'] = image_name
|
||||
|
||||
# update and write the new data
|
||||
wgts = read_widgets(JSON_USER_WIDGETS)
|
||||
if current_widget in wgts:
|
||||
wgts[current_widget] = current_widget_data
|
||||
write_widgets(wgts, JSON_USER_WIDGETS)
|
||||
else:
|
||||
wgts = read_widgets(JSON_DEFAULT_WIDGETS)
|
||||
wgts[current_widget] = current_widget_data
|
||||
write_widgets(wgts, JSON_DEFAULT_WIDGETS)
|
||||
|
||||
# update the preview panel
|
||||
update_preview_collection()
|
||||
|
||||
# trigger an update and display original but updated widget
|
||||
bpy.context.window_manager.widget_list = current_widget
|
||||
|
||||
|
||||
def reset_default_images():
|
||||
current_widget = bpy.context.window_manager.widget_list
|
||||
wgts = read_widgets(JSON_DEFAULT_WIDGETS)
|
||||
|
||||
for name, data in wgts.items():
|
||||
image = f"{name}.png"
|
||||
data["image"] = image
|
||||
|
||||
write_widgets(wgts, JSON_DEFAULT_WIDGETS)
|
||||
|
||||
# update the preview panel
|
||||
update_preview_collection()
|
||||
|
||||
# trigger an update and display original but updated widget
|
||||
bpy.context.window_manager.widget_list = current_widget
|
||||
|
||||
|
||||
################ COLOR PRESETS ################
|
||||
|
||||
def read_color_presets():
|
||||
presets = {}
|
||||
|
||||
# Read the JSON file
|
||||
json_file = get_custom_color_preset_dir()
|
||||
if os.path.exists(json_file):
|
||||
with open(json_file, "r") as file:
|
||||
presets = json.load(file)
|
||||
|
||||
presets = {item["name"]: item for item in presets} # convert to dictionary
|
||||
|
||||
return presets
|
||||
|
||||
|
||||
def update_color_presets(new_presets, zip_filepath):
|
||||
for preset in new_presets:
|
||||
add_color_set(bpy.context, preset)
|
||||
|
||||
# extract any images needed from zip library
|
||||
# if new_images:
|
||||
# from zipfile import ZipFile
|
||||
# dest_dir = os.path.abspath(os.path.join(get_addon_dir(), '..'))
|
||||
# if os.path.exists(zip_filepath):
|
||||
# try:
|
||||
# with ZipFile(zip_filepath, 'r') as zip_file:
|
||||
# for file in zip_file.namelist():
|
||||
# if file.startswith('custom_thumbnails/') and file.split("/")[1] in new_images:
|
||||
# zip_file.extract(file, dest_dir)
|
||||
# except:
|
||||
# pass
|
||||
|
||||
|
||||
def import_color_presets(filepath, action=""):
|
||||
required_data_keys = ("name", "normal", "select", "active") # json data
|
||||
presets = None
|
||||
|
||||
from zipfile import ZipFile
|
||||
dest_dir = get_custom_dir()
|
||||
|
||||
presets_import = BoneWidgetImportData()
|
||||
|
||||
presets_import.import_type = "colorset"
|
||||
|
||||
if os.path.exists(filepath) and action:
|
||||
try:
|
||||
with ZipFile(filepath, 'r') as zip_file:
|
||||
# extract images
|
||||
for file in zip_file.namelist():
|
||||
# if file.startswith('preset_thumbnails/'):
|
||||
# zip_file.extract(file, dest_dir)
|
||||
if file.endswith('.json'): # extract data from the .json file
|
||||
f = zip_file.read(file)
|
||||
json_data = f.decode('utf8').replace("'", '"')
|
||||
presets = json.loads(json_data)
|
||||
|
||||
# validate presets data type
|
||||
if not isinstance(presets, list):
|
||||
raise TypeError(
|
||||
f"Expected a list, but got {type(presets).__name__}")
|
||||
|
||||
current_presets = read_color_presets()
|
||||
|
||||
# check for duplicate presets
|
||||
for preset in presets:
|
||||
presets_import.total_num_imports += 1
|
||||
|
||||
# validate json data
|
||||
if not validate_json_data(preset, required_data_keys, False):
|
||||
presets_import.failed_imports.append(ColorSet(preset))
|
||||
continue
|
||||
|
||||
name = preset['name']
|
||||
if action == "ASK":
|
||||
presets_import.skipped_imports.append(ColorSet(preset))
|
||||
elif action == "OVERWRITE":
|
||||
presets_import.imported_items.append(ColorSet(preset))
|
||||
elif action == "SKIP":
|
||||
# name and colors match or just colors match
|
||||
if colors_match(preset, current_presets[name]):
|
||||
presets_import.skipped_imports.append(ColorSet(preset))
|
||||
elif not name in current_presets:
|
||||
presets_import.imported_items.append(ColorSet(preset))
|
||||
else:
|
||||
presets_import.skipped_imports.append(ColorSet(preset))
|
||||
else:
|
||||
presets_import.failed_imports.append(ColorSet(preset))
|
||||
|
||||
except TypeError as e: # Handle data type errors specifically
|
||||
print(f"Error while importing color presets: {e}")
|
||||
presets_import.json_import_error = True
|
||||
except Exception as e:
|
||||
print(f"Error while importing color presets: {e}")
|
||||
for preset in presets:
|
||||
presets_import.failed_imports.append(ColorSet(preset))
|
||||
presets_import.total_num_imports = presets_import.failed()
|
||||
return presets_import
|
||||
|
||||
|
||||
def colors_match(set1, set2):
|
||||
if isinstance(set1, dict):
|
||||
return set1['normal'] == set2['normal'] \
|
||||
and set1['select'] == set2['select'] \
|
||||
and set1['active'] == set2['active']
|
||||
elif isinstance(set1, bpy.types.ThemeBoneColorSet):
|
||||
return set1.normal == set2.normal \
|
||||
and set1.select == set2.select \
|
||||
and set1.active == set2.active
|
||||
|
||||
|
||||
def scan_armature_color_presets(context, armature):
|
||||
found_color_sets = set()
|
||||
|
||||
colorsets_import = BoneWidgetImportData()
|
||||
colorsets_import.import_type = "colorset"
|
||||
|
||||
current_color_sets = context.window_manager.custom_color_presets
|
||||
|
||||
# edit bones
|
||||
for bone in armature.bones:
|
||||
if bone.color.is_custom:
|
||||
is_unique_colorset = True
|
||||
for color_set in current_color_sets:
|
||||
if colors_match(bone.color.custom, color_set):
|
||||
is_unique_colorset = False # not unique
|
||||
break
|
||||
|
||||
color_data = (tuple(bone.color.custom.normal), tuple(
|
||||
bone.color.custom.select), tuple(bone.color.custom.active))
|
||||
if is_unique_colorset and not color_data in found_color_sets:
|
||||
color_set = {attr: list(getattr(bone.color.custom, attr)[:3]) for attr in [
|
||||
"normal", "active", "select"]}
|
||||
color_set['name'] = bone.name
|
||||
colorsets_import.skipped_imports.append(ColorSet(color_set))
|
||||
found_color_sets.add(color_data)
|
||||
|
||||
# pose bones
|
||||
pose_bone = context.object.pose.bones.get(bone.name)
|
||||
if pose_bone.color.is_custom:
|
||||
is_unique_colorset = True
|
||||
for color_set in current_color_sets:
|
||||
if colors_match(pose_bone.color.custom, color_set):
|
||||
is_unique_colorset = False # not unique
|
||||
break
|
||||
|
||||
color_data = (tuple(pose_bone.color.custom.normal), tuple(
|
||||
pose_bone.color.custom.select), tuple(pose_bone.color.custom.active))
|
||||
if is_unique_colorset and not color_data in found_color_sets:
|
||||
color_set = {attr: list(getattr(pose_bone.color.custom, attr)[
|
||||
:3]) for attr in ["normal", "active", "select"]}
|
||||
color_set['name'] = bone.name
|
||||
colorsets_import.skipped_imports.append(ColorSet(color_set))
|
||||
found_color_sets.add(color_data)
|
||||
|
||||
return colorsets_import
|
||||
|
||||
|
||||
def export_color_presets(filepath, context):
|
||||
color_presets = len(context.window_manager.custom_color_presets)
|
||||
|
||||
if color_presets:
|
||||
dest_dir = os.path.dirname(filepath)
|
||||
json_dir = get_custom_dir()
|
||||
# image_folder = 'preset_thumbnails'
|
||||
# custom_image_dir = get_custom_image_dir(image_folder)
|
||||
|
||||
filename = os.path.basename(filepath)
|
||||
if not filename:
|
||||
filename = "color_presets.zip"
|
||||
elif not filename.endswith('.zip'):
|
||||
filename += ".zip"
|
||||
|
||||
# start the zipping process
|
||||
try:
|
||||
from zipfile import ZipFile
|
||||
with ZipFile(os.path.join(dest_dir, filename), "w") as zip:
|
||||
# write the json file
|
||||
file = os.path.join(json_dir, JSON_COLOR_PRESETS)
|
||||
arcname = os.path.basename(file)
|
||||
zip.write(file, arcname=arcname)
|
||||
except Exception as e:
|
||||
print("Error exporting color presets: ", e)
|
||||
return 0
|
||||
|
||||
return color_presets
|
||||
|
||||
|
||||
def add_color_set_from_bone(context, bone, suffix_name):
|
||||
new_item = context.window_manager.custom_color_presets.add()
|
||||
|
||||
color_set = bone.color.custom
|
||||
|
||||
new_name = bone.name + suffix_name # CHANGE LATER
|
||||
|
||||
# check if the name already ends with an incremented number
|
||||
match = re.match(r"^(.*)\.(\d{3})$", new_name)
|
||||
count = int(match.group(2)) if match else 1
|
||||
base_name = match.group(1) if match else new_name
|
||||
|
||||
while any(item.name == new_name for item in context.window_manager.custom_color_presets):
|
||||
new_name = f"{base_name}.{count:03d}"
|
||||
count += 1
|
||||
|
||||
new_item.name = new_name
|
||||
|
||||
if not color_set: # new default color set
|
||||
new_item.normal = (1.0, 0.0, 0.0)
|
||||
new_item.select = (0.0, 1.0, 0.0)
|
||||
new_item.active = (0.0, 0.0, 1.0)
|
||||
else:
|
||||
new_item.normal = color_set.normal
|
||||
new_item.select = color_set.select
|
||||
new_item.active = color_set.active
|
||||
|
||||
|
||||
def add_color_set(context, color_set=None):
|
||||
new_item = context.window_manager.custom_color_presets.add()
|
||||
|
||||
base_name = "Color Set" if not color_set else color_set.name
|
||||
new_name = base_name
|
||||
|
||||
# check if the name already ends with an incremented number
|
||||
match = re.match(r"^(.*)\.(\d{3})$", base_name)
|
||||
count = int(match.group(2)) if match else 1
|
||||
base_name = match.group(1) if match else new_name
|
||||
|
||||
while any(item.name == new_name for item in context.window_manager.custom_color_presets):
|
||||
new_name = f"{base_name}.{count:03d}"
|
||||
count += 1
|
||||
|
||||
new_item.name = new_name
|
||||
|
||||
if not color_set: # new default color set
|
||||
new_item.normal = (1.0, 0.0, 0.0)
|
||||
new_item.select = (0.0, 1.0, 0.0)
|
||||
new_item.active = (0.0, 0.0, 1.0)
|
||||
else:
|
||||
new_item.normal = color_set.normal
|
||||
new_item.select = color_set.select
|
||||
new_item.active = color_set.active
|
||||
|
||||
|
||||
def save_color_sets(context):
|
||||
if not bpy.context.window_manager.turn_off_colorset_save:
|
||||
bpy.context.window_manager.turn_off_colorset_save = True
|
||||
color_sets = [{
|
||||
"name": item.name,
|
||||
"normal": list(item.normal),
|
||||
"select": list(item.select),
|
||||
"active": list(item.active)
|
||||
} for item in context.window_manager.custom_color_presets]
|
||||
|
||||
filepath = get_custom_color_preset_dir()
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump(color_sets, f, indent=4)
|
||||
bpy.context.window_manager.turn_off_colorset_save = False
|
||||
|
||||
|
||||
def load_color_presets():
|
||||
filepath = get_custom_color_preset_dir()
|
||||
if os.path.exists(filepath):
|
||||
with open(filepath, 'r') as f:
|
||||
color_sets = json.load(f)
|
||||
bpy.context.window_manager.custom_color_presets.clear()
|
||||
bpy.context.window_manager.turn_off_colorset_save = True
|
||||
for item in color_sets:
|
||||
new_item = bpy.context.window_manager.custom_color_presets.add()
|
||||
new_item.name = item["name"]
|
||||
new_item.normal = item["normal"]
|
||||
new_item.select = item["select"]
|
||||
new_item.active = item["active"]
|
||||
bpy.context.window_manager.turn_off_colorset_save = False
|
||||
@@ -0,0 +1,614 @@
|
||||
import bpy
|
||||
import numpy
|
||||
from mathutils import Matrix, Vector
|
||||
from .. import __package__
|
||||
|
||||
|
||||
def get_collection(context):
|
||||
# check user preferences for the name of the collection
|
||||
if not get_preferences(context).use_rigify_defaults:
|
||||
bw_collection_name = get_preferences(
|
||||
context).bonewidget_collection_name
|
||||
else:
|
||||
bw_collection_name = "WGTS_" + context.active_object.name
|
||||
|
||||
collection = recursive_layer_collection(
|
||||
context.scene.collection, bw_collection_name)
|
||||
if collection: # if it already exists
|
||||
return collection
|
||||
|
||||
collection = bpy.data.collections.get(bw_collection_name)
|
||||
|
||||
if collection: # if it exists but not linked to scene
|
||||
context.scene.collection.children.link(collection)
|
||||
return collection
|
||||
|
||||
else: # create a new collection
|
||||
collection = bpy.data.collections.new(bw_collection_name)
|
||||
context.scene.collection.children.link(collection)
|
||||
# hide new collection
|
||||
viewlayer_collection = context.view_layer.layer_collection.children[collection.name]
|
||||
viewlayer_collection.hide_viewport = True
|
||||
return collection
|
||||
|
||||
|
||||
def recursive_layer_collection(layer_collection, collection_name):
|
||||
found = None
|
||||
if (layer_collection.name == collection_name):
|
||||
return layer_collection
|
||||
for layer in layer_collection.children:
|
||||
found = recursive_layer_collection(layer, collection_name)
|
||||
if found:
|
||||
return found
|
||||
|
||||
|
||||
def get_view_layer_collection(context, widget=None):
|
||||
widget_collection = bpy.data.collections[bpy.data.objects[widget.name].users_collection[0].name]
|
||||
# save current active layer_collection
|
||||
saved_layer_collection = bpy.context.view_layer.layer_collection
|
||||
# actually find the view_layer we want
|
||||
layer_collection = recursive_layer_collection(
|
||||
saved_layer_collection, widget_collection.name)
|
||||
# make sure the collection (data level) is not hidden
|
||||
widget_collection.hide_viewport = False
|
||||
|
||||
# change the active view layer
|
||||
bpy.context.view_layer.active_layer_collection = layer_collection
|
||||
# make sure it isn't excluded so it can be edited
|
||||
layer_collection.exclude = False
|
||||
# return the active view layer to what it was
|
||||
bpy.context.view_layer.active_layer_collection = saved_layer_collection
|
||||
|
||||
return layer_collection
|
||||
|
||||
|
||||
def match_bone_matrix(widget, match_bone):
|
||||
if widget == None:
|
||||
return
|
||||
widget.matrix_local = match_bone.bone.matrix_local
|
||||
widget.matrix_world = match_bone.id_data.matrix_world @ match_bone.bone.matrix_local
|
||||
if match_bone.custom_shape_transform:
|
||||
# if it has a transform override, apply this to the widget loc and rot
|
||||
org_scale = widget.matrix_world.to_scale()
|
||||
org_scale_mat = Matrix.Scale(1, 4, org_scale)
|
||||
target_matrix = match_bone.custom_shape_transform.id_data.matrix_world @ match_bone.custom_shape_transform.bone.matrix_local
|
||||
loc = target_matrix.to_translation()
|
||||
loc_mat = Matrix.Translation(loc)
|
||||
rot = target_matrix.to_euler().to_matrix()
|
||||
widget.matrix_world = loc_mat @ rot.to_4x4() @ org_scale_mat
|
||||
|
||||
if match_bone.use_custom_shape_bone_size:
|
||||
ob_scale = bpy.context.scene.objects[match_bone.id_data.name].scale
|
||||
widget.scale = [match_bone.bone.length * ob_scale[0],
|
||||
match_bone.bone.length * ob_scale[1], match_bone.bone.length * ob_scale[2]]
|
||||
|
||||
# if the user has added any custom transforms to the bone widget display - calculate this too
|
||||
loc = match_bone.custom_shape_translation
|
||||
rot = match_bone.custom_shape_rotation_euler
|
||||
scale = match_bone.custom_shape_scale_xyz
|
||||
widget.scale *= scale
|
||||
widget.matrix_world = widget.matrix_world @ Matrix.LocRotScale(
|
||||
loc, rot, widget.scale)
|
||||
|
||||
widget.data.update()
|
||||
|
||||
|
||||
def from_widget_find_bone(widget):
|
||||
match_bone = None
|
||||
for ob in bpy.context.scene.objects:
|
||||
if ob.type == "ARMATURE":
|
||||
for bone in ob.pose.bones:
|
||||
if bone.custom_shape == widget:
|
||||
match_bone = bone
|
||||
return match_bone
|
||||
|
||||
|
||||
def create_widget(bone, widget, relative, size, slide, rotation, collection, use_face_data, wireframe_width):
|
||||
if not get_preferences(bpy.context).use_rigify_defaults:
|
||||
bw_widget_prefix = get_preferences(bpy.context).widget_prefix
|
||||
else:
|
||||
bw_widget_prefix = "WGT-" + bpy.context.active_object.name + "_"
|
||||
|
||||
matrix_bone = bone
|
||||
|
||||
# delete the existing shape
|
||||
if bone.custom_shape:
|
||||
bpy.data.objects.remove(
|
||||
bpy.data.objects[bone.custom_shape.name], do_unlink=True)
|
||||
|
||||
# make the data name include the prefix
|
||||
new_data = bpy.data.meshes.new(bw_widget_prefix + bone.name)
|
||||
|
||||
bone.use_custom_shape_bone_size = relative
|
||||
|
||||
# deal with face data
|
||||
faces = widget['faces'] if use_face_data else []
|
||||
|
||||
# add the verts
|
||||
new_data.from_pydata(numpy.array(
|
||||
widget['vertices']) * size, widget['edges'], faces)
|
||||
|
||||
# Create transform matrices (slide vector and rotation)
|
||||
widget_matrix = Matrix()
|
||||
|
||||
# make the slide value always relative to the bone length
|
||||
if not relative: # TODO: shift this to user preference?
|
||||
slide = Vector(slide) # turn slide into a vector
|
||||
slide *= bone.length
|
||||
trans = Matrix.Translation(slide)
|
||||
|
||||
rot = rotation.to_matrix().to_4x4()
|
||||
|
||||
# Translate then rotate the matrix
|
||||
widget_matrix = widget_matrix @ trans
|
||||
widget_matrix = widget_matrix @ rot
|
||||
|
||||
# transform the widget with this matrix
|
||||
new_data.transform(widget_matrix)
|
||||
|
||||
new_data.update(calc_edges=True)
|
||||
|
||||
new_object = bpy.data.objects.new(bw_widget_prefix + bone.name, new_data)
|
||||
|
||||
new_object.data = new_data
|
||||
new_object.name = bw_widget_prefix + bone.name
|
||||
collection.objects.link(new_object)
|
||||
|
||||
new_object.matrix_world = bpy.context.active_object.matrix_world @ matrix_bone.bone.matrix_local
|
||||
new_object.scale = [matrix_bone.bone.length,
|
||||
matrix_bone.bone.length, matrix_bone.bone.length]
|
||||
layer = bpy.context.view_layer
|
||||
layer.update()
|
||||
|
||||
bone.custom_shape = new_object
|
||||
# show faces if use face data is enabled
|
||||
bone.bone.show_wire = not use_face_data
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
bone.custom_shape_wire_width = wireframe_width
|
||||
|
||||
|
||||
def symmetrize_widget(bone, collection):
|
||||
if not get_preferences(bpy.context).use_rigify_defaults:
|
||||
bw_widget_prefix = get_preferences(bpy.context).widget_prefix
|
||||
rigify_object_name = ''
|
||||
else:
|
||||
bw_widget_prefix = "WGT-"
|
||||
rigify_object_name = bpy.context.active_object.name + "_"
|
||||
|
||||
mirror_bone = find_mirror_object(bone)
|
||||
if not mirror_bone:
|
||||
return
|
||||
|
||||
widget = bone.custom_shape
|
||||
if not widget or not widget.data:
|
||||
return
|
||||
|
||||
# clean up existing mirrored widget if it's different
|
||||
mirror_widget = mirror_bone.custom_shape
|
||||
if mirror_widget and mirror_widget != widget:
|
||||
existing = bpy.context.scene.objects.get(mirror_widget.name)
|
||||
if existing:
|
||||
bpy.data.objects.remove(existing)
|
||||
|
||||
# create mirrored mesh data
|
||||
new_data = widget.data.copy()
|
||||
for vert in new_data.vertices:
|
||||
vert.co.x *= -1 # mirror along X-axis
|
||||
|
||||
new_object = widget.copy()
|
||||
new_object.data = new_data
|
||||
new_object.name = bw_widget_prefix + rigify_object_name + mirror_bone.name
|
||||
bpy.data.collections[collection.name].objects.link(new_object)
|
||||
|
||||
# use custom shape transform if available
|
||||
transform_bone = mirror_bone.custom_shape_transform or mirror_bone
|
||||
new_object.matrix_local = transform_bone.bone.matrix_local
|
||||
new_object.scale = [transform_bone.bone.length] * 3
|
||||
new_object.data.flip_normals()
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
mirror_bone.custom_shape = new_object
|
||||
mirror_bone.bone.show_wire = bone.bone.show_wire
|
||||
mirror_bone.use_custom_shape_bone_size = bone.use_custom_shape_bone_size
|
||||
|
||||
symmetrize_color = get_preferences(bpy.context).symmetrize_color
|
||||
if bpy.app.version >= (4, 0, 0) and symmetrize_color:
|
||||
# pose bone colors
|
||||
mirror_bone.bone.color.custom.normal = bone.bone.color.custom.normal
|
||||
mirror_bone.bone.color.custom.select = bone.bone.color.custom.select
|
||||
mirror_bone.bone.color.custom.active = bone.bone.color.custom.active
|
||||
mirror_bone.bone.color.palette = bone.bone.color.palette
|
||||
|
||||
# edit bone colors
|
||||
mirror_bone.color.custom.normal = bone.color.custom.normal
|
||||
mirror_bone.color.custom.select = bone.color.custom.select
|
||||
mirror_bone.color.custom.active = bone.color.custom.active
|
||||
mirror_bone.color.palette = bone.color.palette
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
mirror_bone.custom_shape_wire_width = bone.custom_shape_wire_width
|
||||
|
||||
|
||||
def symmetrize_widget_helper(bone, collection, active_object, widgets_and_bones):
|
||||
bw_symmetry_suffix = get_preferences(bpy.context).symmetry_suffix
|
||||
bw_symmetry_suffix = bw_symmetry_suffix.split(";")
|
||||
|
||||
suffix_1 = bw_symmetry_suffix[0].replace(" ", "")
|
||||
suffix_2 = bw_symmetry_suffix[1].replace(" ", "")
|
||||
|
||||
if active_object.name.endswith(suffix_1):
|
||||
if bone.name.endswith(suffix_1) and widgets_and_bones[bone]:
|
||||
symmetrize_widget(bone, collection)
|
||||
elif active_object.name.endswith(suffix_2):
|
||||
if bone.name.endswith(suffix_2) and widgets_and_bones[bone]:
|
||||
symmetrize_widget(bone, collection)
|
||||
|
||||
|
||||
def delete_unused_widgets():
|
||||
if not get_preferences(bpy.context).use_rigify_defaults:
|
||||
bw_collection_name = get_preferences(
|
||||
bpy.context).bonewidget_collection_name
|
||||
else:
|
||||
bw_collection_name = 'WGTS_' + bpy.context.active_object.name
|
||||
|
||||
collection = recursive_layer_collection(
|
||||
bpy.context.scene.collection, bw_collection_name)
|
||||
widget_list = []
|
||||
|
||||
for ob in bpy.data.objects:
|
||||
if ob.type == 'ARMATURE':
|
||||
for bone in ob.pose.bones:
|
||||
if bone.custom_shape:
|
||||
widget_list.append(bone.custom_shape)
|
||||
|
||||
unwanted_list = [
|
||||
ob for ob in collection.all_objects if ob not in widget_list]
|
||||
|
||||
for ob in unwanted_list:
|
||||
bpy.data.objects.remove(bpy.data.objects[ob.name], do_unlink=True)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def edit_widget(active_bone):
|
||||
widget = active_bone.custom_shape
|
||||
|
||||
collection = get_view_layer_collection(bpy.context, widget)
|
||||
collection.hide_viewport = False
|
||||
|
||||
# hide all other objects in collection
|
||||
for obj in collection.collection.all_objects:
|
||||
if obj.name != widget.name:
|
||||
obj.hide_set(True)
|
||||
else:
|
||||
obj.hide_set(False) # in case user manually hid it
|
||||
|
||||
armature = active_bone.id_data
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.context.active_object.select_set(False)
|
||||
|
||||
if bpy.context.space_data.local_view:
|
||||
bpy.ops.view3d.localview()
|
||||
|
||||
# select object and make it active
|
||||
widget.select_set(True)
|
||||
bpy.context.view_layer.objects.active = widget
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.context.tool_settings.mesh_select_mode = (
|
||||
True, False, False) # enter vertex mode
|
||||
|
||||
|
||||
def return_to_armature(widget):
|
||||
bone = from_widget_find_bone(widget)
|
||||
armature = bone.id_data
|
||||
|
||||
if bpy.context.active_object.mode == 'EDIT':
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
collection = get_view_layer_collection(bpy.context, widget)
|
||||
collection.hide_viewport = True
|
||||
|
||||
# unhide all objects in the collection
|
||||
for obj in collection.collection.all_objects:
|
||||
obj.hide_set(False)
|
||||
|
||||
if bpy.context.space_data.local_view:
|
||||
bpy.ops.view3d.localview()
|
||||
|
||||
bpy.context.view_layer.objects.active = armature
|
||||
armature.select_set(True)
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
if bpy.app.version < (5, 0, 0):
|
||||
armature.data.bones[bone.name].select = True
|
||||
armature.data.bones.active = armature.data.bones[bone.name]
|
||||
|
||||
|
||||
def find_mirror_object(object):
|
||||
bw_symmetry_suffix = get_preferences(bpy.context).symmetry_suffix
|
||||
bw_symmetry_suffix = bw_symmetry_suffix.split(";")
|
||||
|
||||
suffix_1 = bw_symmetry_suffix[0].replace(" ", "")
|
||||
suffix_2 = bw_symmetry_suffix[1].replace(" ", "")
|
||||
|
||||
if object.name.endswith(suffix_1):
|
||||
suffix = suffix_2
|
||||
suffix_length = len(suffix_1)
|
||||
|
||||
elif object.name.endswith(suffix_2):
|
||||
suffix = suffix_1
|
||||
suffix_length = len(suffix_2)
|
||||
|
||||
elif object.name.endswith(suffix_1.lower()):
|
||||
suffix = suffix_2.lower()
|
||||
suffix_length = len(suffix_1)
|
||||
elif object.name.endswith(suffix_2.lower()):
|
||||
suffix = suffix_1.lower()
|
||||
suffix_length = len(suffix_2)
|
||||
else: # what if the widget ends in .001?
|
||||
print('Object suffix unknown, using blank')
|
||||
suffix = ''
|
||||
|
||||
object_name = list(object.name)
|
||||
object_base_name = object_name[:-suffix_length]
|
||||
mirrored_object_name = "".join(object_base_name) + suffix
|
||||
|
||||
if object.id_data.type == 'ARMATURE':
|
||||
return object.id_data.pose.bones.get(mirrored_object_name)
|
||||
else:
|
||||
return bpy.context.scene.objects.get(mirrored_object_name)
|
||||
|
||||
|
||||
def find_match_bones():
|
||||
bw_symmetry_suffix = get_preferences(bpy.context).symmetry_suffix
|
||||
bw_symmetry_suffix = bw_symmetry_suffix.split(";")
|
||||
|
||||
suffix_1 = bw_symmetry_suffix[0].replace(" ", "")
|
||||
suffix_2 = bw_symmetry_suffix[1].replace(" ", "")
|
||||
|
||||
widgets_and_bones = {}
|
||||
|
||||
if bpy.context.object.type == 'ARMATURE':
|
||||
for bone in bpy.context.selected_pose_bones:
|
||||
if bone.name.endswith(suffix_1) or bone.name.endswith(suffix_2):
|
||||
widgets_and_bones[bone] = bone.custom_shape
|
||||
mirror_bone = find_mirror_object(bone)
|
||||
if mirror_bone:
|
||||
widgets_and_bones[mirror_bone] = mirror_bone.custom_shape
|
||||
|
||||
armature = bpy.context.object
|
||||
active_object = bpy.context.active_pose_bone
|
||||
else:
|
||||
for shape in bpy.context.selected_objects:
|
||||
bone = from_widget_find_bone(shape)
|
||||
if bone.name.endswith(("L", "R")):
|
||||
widgets_and_bones[from_widget_find_bone(shape)] = shape
|
||||
|
||||
mirrorShape = find_mirror_object(shape)
|
||||
if mirrorShape:
|
||||
widgets_and_bones[mirrorShape] = mirrorShape
|
||||
|
||||
active_object = from_widget_find_bone(bpy.context.object)
|
||||
armature = active_object.id_data
|
||||
return (widgets_and_bones, active_object, armature)
|
||||
|
||||
|
||||
def resync_widget_names():
|
||||
if not get_preferences(bpy.context).use_rigify_defaults:
|
||||
bw_collection_name = get_preferences(
|
||||
bpy.context).bonewidget_collection_name
|
||||
bw_widget_prefix = get_preferences(bpy.context).widget_prefix
|
||||
else:
|
||||
bw_collection_name = 'WGTS_' + bpy.context.active_object.name
|
||||
bw_widget_prefix = 'WGT-' + bpy.context.active_object.name + '_'
|
||||
|
||||
widgets_and_bones = {}
|
||||
|
||||
if bpy.context.object.type == 'ARMATURE':
|
||||
for bone in bpy.context.active_object.pose.bones:
|
||||
if bone.custom_shape:
|
||||
widgets_and_bones[bone] = bone.custom_shape
|
||||
|
||||
for k, v in widgets_and_bones.items():
|
||||
if k.name != (bw_widget_prefix + k.name):
|
||||
bpy.data.objects[v.name].name = str(bw_widget_prefix + k.name)
|
||||
|
||||
|
||||
def clear_bone_widgets():
|
||||
if bpy.context.object.type == 'ARMATURE':
|
||||
for bone in bpy.context.selected_pose_bones:
|
||||
if bone.custom_shape:
|
||||
bone.custom_shape = None
|
||||
bone.custom_shape_transform = None
|
||||
|
||||
|
||||
def add_object_as_widget(context, collection):
|
||||
selected_objects = bpy.context.selected_objects
|
||||
|
||||
if len(selected_objects) != 2:
|
||||
print('Only a widget object and the pose bone(s)')
|
||||
return {'FINISHED'}
|
||||
|
||||
allowed_object_types = ['MESH', 'CURVE']
|
||||
|
||||
widget_object = None
|
||||
|
||||
for ob in selected_objects:
|
||||
if ob.type in allowed_object_types:
|
||||
widget_object = ob
|
||||
|
||||
if widget_object:
|
||||
active_bone = context.active_pose_bone
|
||||
|
||||
# deal with any existing shape
|
||||
if active_bone.custom_shape:
|
||||
bpy.data.objects.remove(
|
||||
bpy.data.objects[active_bone.custom_shape.name], do_unlink=True)
|
||||
|
||||
# duplicate shape
|
||||
widget = widget_object.copy()
|
||||
widget.data = widget.data.copy()
|
||||
# rename it
|
||||
bw_widget_prefix = get_preferences(context).widget_prefix
|
||||
widget_name = bw_widget_prefix + active_bone.name
|
||||
widget.name = widget_name
|
||||
widget.data.name = widget_name
|
||||
# link it
|
||||
collection.objects.link(widget)
|
||||
|
||||
# match transforms
|
||||
widget.matrix_world = bpy.context.active_object.matrix_world @ active_bone.bone.matrix_local
|
||||
widget.scale = [active_bone.bone.length,
|
||||
active_bone.bone.length, active_bone.bone.length]
|
||||
layer = bpy.context.view_layer
|
||||
layer.update()
|
||||
|
||||
active_bone.custom_shape = widget
|
||||
active_bone.bone.show_wire = True
|
||||
|
||||
# deselect original object
|
||||
widget_object.select_set(False)
|
||||
|
||||
|
||||
def set_bone_color(context, color, clear_both_modes=None):
|
||||
if context.object.mode == "POSE":
|
||||
if color == 'DEFAULT' and clear_both_modes != None:
|
||||
for bone in context.selected_pose_bones:
|
||||
bone.color.palette = 'DEFAULT'
|
||||
|
||||
if clear_both_modes:
|
||||
bone.bone.color.palette = 'DEFAULT'
|
||||
return
|
||||
|
||||
for bone in context.selected_pose_bones:
|
||||
bone.color.palette = color # this will get the selected bone color
|
||||
|
||||
if color == "CUSTOM":
|
||||
bone.color.custom.normal = context.scene.bw_settings.custom_pose_color_set.normal
|
||||
bone.color.custom.select = context.scene.bw_settings.custom_pose_color_set.select
|
||||
bone.color.custom.active = context.scene.bw_settings.custom_pose_color_set.active
|
||||
|
||||
# set the edit bone colors if applicable (while in pose mode)
|
||||
if get_preferences(context).edit_bone_colors == 'DEFAULT':
|
||||
bone.bone.color.palette = 'DEFAULT' # this will reset the edit bone color
|
||||
|
||||
elif get_preferences(context).edit_bone_colors == 'LINKED':
|
||||
bone.bone.color.palette = color # set the edit bone colors
|
||||
|
||||
# Set the custom color to edit bones (if applicable)
|
||||
if color == "CUSTOM":
|
||||
bone.bone.color.custom.normal = context.scene.bw_settings.custom_pose_color_set.normal
|
||||
bone.bone.color.custom.select = context.scene.bw_settings.custom_pose_color_set.select
|
||||
bone.bone.color.custom.active = context.scene.bw_settings.custom_pose_color_set.active
|
||||
|
||||
elif context.object.mode == "EDIT":
|
||||
if color == 'DEFAULT' and clear_both_modes != None:
|
||||
for edit_bone in context.selected_bones:
|
||||
edit_bone.color.palette = 'DEFAULT'
|
||||
|
||||
if clear_both_modes:
|
||||
pose_bone = context.object.pose.bones.get(edit_bone.name)
|
||||
pose_bone.color.palette = 'DEFAULT'
|
||||
|
||||
return
|
||||
|
||||
for edit_bone in context.selected_bones:
|
||||
if get_preferences(context).edit_bone_colors == 'DEFAULT':
|
||||
# this will get the edit bone color back to default
|
||||
edit_bone.color.palette = 'DEFAULT'
|
||||
|
||||
elif get_preferences(context).edit_bone_colors == 'LINKED':
|
||||
edit_bone.color.palette = color # set the edit mode color
|
||||
|
||||
# get the pose bone
|
||||
pose_bone = context.object.pose.bones.get(edit_bone.name)
|
||||
pose_bone.color.palette = color # set the pose mode color
|
||||
|
||||
if color == "CUSTOM":
|
||||
# set edit bone custom colors
|
||||
edit_bone.color.custom.normal = context.scene.bw_settings.custom_edit_color_set.normal
|
||||
edit_bone.color.custom.select = context.scene.bw_settings.custom_edit_color_set.select
|
||||
edit_bone.color.custom.active = context.scene.bw_settings.custom_edit_color_set.active
|
||||
# set pose bone custom colors
|
||||
pose_bone.color.custom.normal = context.scene.bw_settings.custom_edit_color_set.normal
|
||||
pose_bone.color.custom.select = context.scene.bw_settings.custom_edit_color_set.select
|
||||
pose_bone.color.custom.active = context.scene.bw_settings.custom_edit_color_set.active
|
||||
|
||||
elif get_preferences(context).edit_bone_colors == 'SEPARATE':
|
||||
edit_bone.color.palette = color # set the edit mode color
|
||||
|
||||
if color == "CUSTOM":
|
||||
# set edit bone custom colors
|
||||
edit_bone.color.custom.normal = context.scene.bw_settings.custom_edit_color_set.normal
|
||||
edit_bone.color.custom.select = context.scene.bw_settings.custom_edit_color_set.select
|
||||
edit_bone.color.custom.active = context.scene.bw_settings.custom_edit_color_set.active
|
||||
|
||||
|
||||
def copy_bone_color(context, bone):
|
||||
live_update_current_state = context.scene.bw_settings.live_update_on
|
||||
context.scene.bw_settings.live_update_on = False
|
||||
|
||||
if bone.color.is_custom:
|
||||
if context.object.mode == 'POSE':
|
||||
context.scene.bw_settings.custom_pose_color_set.normal = bone.color.custom.normal
|
||||
context.scene.bw_settings.custom_pose_color_set.select = bone.color.custom.select
|
||||
context.scene.bw_settings.custom_pose_color_set.active = bone.color.custom.active
|
||||
else:
|
||||
context.scene.bw_settings.custom_edit_color_set.normal = bone.color.custom.normal
|
||||
context.scene.bw_settings.custom_edit_color_set.select = bone.color.custom.select
|
||||
context.scene.bw_settings.custom_edit_color_set.active = bone.color.custom.active
|
||||
elif bone.color.palette != "DEFAULT": # bone has a theme assigned
|
||||
theme = bone.color.palette
|
||||
theme_id = int(theme[-2:]) - 1
|
||||
theme_color_set = bpy.context.preferences.themes[0].bone_color_sets[theme_id]
|
||||
|
||||
palette = context.scene.bw_settings.custom_pose_color_set if context.object.mode == 'POSE' \
|
||||
else context.scene.bw_settings.custom_edit_color_set
|
||||
|
||||
palette.normal = theme_color_set.normal
|
||||
palette.select = theme_color_set.select
|
||||
palette.active = theme_color_set.active
|
||||
|
||||
context.scene.bw_settings.live_update_on = live_update_current_state
|
||||
|
||||
|
||||
def update_bone_color(self, context):
|
||||
if context.scene.bw_settings.live_update_on:
|
||||
set_bone_color(context, "CUSTOM")
|
||||
|
||||
|
||||
def advanced_options_toggled(self, context):
|
||||
if self.advanced_options:
|
||||
self.global_size_advanced = (self.global_size_simple,) * 3
|
||||
self.slide_advanced[1] = self.slide_simple
|
||||
else:
|
||||
self.global_size_simple = self.global_size_advanced[1]
|
||||
self.slide_simple = self.slide_advanced[1]
|
||||
|
||||
|
||||
def bone_color_items(self, context):
|
||||
items = [("DEFAULT", "Default Colors", "", "", 0)]
|
||||
for i in range(1, 16):
|
||||
items.append((f"THEME{i:02}", f"Theme {i:02}",
|
||||
"", f"COLORSET_{i:02}_VEC", i))
|
||||
return items
|
||||
|
||||
|
||||
def bone_color_items_short(self, context):
|
||||
items = []
|
||||
for i in range(1, 16):
|
||||
items.append((f"THEME{i:02}", f"Theme {i:02}",
|
||||
"", f"COLORSET_{i:02}_VEC", i))
|
||||
items.append(("CUSTOM", "Custom", "", "COLOR", 16))
|
||||
return items
|
||||
|
||||
|
||||
def live_update_toggle(self, context):
|
||||
context.scene.bw_settings.live_update_on = self.live_update_toggle
|
||||
|
||||
|
||||
def get_preferences(context):
|
||||
return context.preferences.addons[__package__].preferences
|
||||
@@ -0,0 +1,301 @@
|
||||
import bpy
|
||||
import bpy.utils.previews
|
||||
from .json_functions import read_widgets, get_widget_data, get_default_image_dir, get_custom_image_dir, JSON_USER_WIDGETS
|
||||
import os
|
||||
from .. import __package__
|
||||
from mathutils import Vector
|
||||
|
||||
preview_collections = {}
|
||||
|
||||
|
||||
def create_preview_collection():
|
||||
if preview_collections:
|
||||
del bpy.types.WindowManager.widget_list
|
||||
for pcoll in preview_collections.values():
|
||||
bpy.utils.previews.remove(pcoll)
|
||||
preview_collections.clear()
|
||||
|
||||
pcoll = bpy.utils.previews.new()
|
||||
pcoll.widget_list = ()
|
||||
preview_collections["widgets"] = pcoll
|
||||
|
||||
bpy.types.WindowManager.widget_list = bpy.props.EnumProperty(
|
||||
items=generate_previews(), name="Shape", description="Shape", update=preview_update
|
||||
)
|
||||
|
||||
|
||||
def generate_previews():
|
||||
enum_items = []
|
||||
|
||||
pcoll = preview_collections["widgets"]
|
||||
if pcoll.widget_list:
|
||||
return pcoll.widget_list
|
||||
|
||||
directory = get_default_image_dir('thumbnails')
|
||||
custom_directory = get_custom_image_dir("custom_thumbnails")
|
||||
|
||||
if directory and os.path.exists(directory):
|
||||
widget_data = {item[0]: item[1].get(
|
||||
"image", "missing_image.png") for item in read_widgets().items()}
|
||||
widget_names = sorted(widget_data.keys())
|
||||
|
||||
for i, name in enumerate(widget_names):
|
||||
image = widget_data.get(name, "")
|
||||
if image is not None:
|
||||
filepath = os.path.join(directory, image)
|
||||
|
||||
# try in custom_thumbnails if above failed
|
||||
if not os.path.exists(filepath):
|
||||
filepath = os.path.join(custom_directory, image)
|
||||
|
||||
# if image still not found, let the user know
|
||||
if not os.path.exists(filepath):
|
||||
filepath = os.path.join(directory, "missing_image.png")
|
||||
|
||||
icon = pcoll.get(name)
|
||||
if not icon:
|
||||
thumb = pcoll.load(name, filepath, 'IMAGE')
|
||||
else:
|
||||
thumb = pcoll[name]
|
||||
|
||||
face_data_info = "Contains Face Data" if get_widget_data(
|
||||
name).get("faces") else ""
|
||||
enum_items.append((name, name, face_data_info, thumb.icon_id, i))
|
||||
|
||||
pcoll.widget_list = enum_items
|
||||
return enum_items
|
||||
|
||||
|
||||
def preview_update(self, context):
|
||||
generate_previews()
|
||||
|
||||
|
||||
def get_preview_default():
|
||||
return bpy.context.preferences.addons[__package__].preferences.preview_default
|
||||
|
||||
|
||||
def copy_custom_image(filepath, filename):
|
||||
if os.path.exists(filepath):
|
||||
image_directory = get_custom_image_dir('custom_thumbnails')
|
||||
destination_path = os.path.join(image_directory, filename)
|
||||
|
||||
try:
|
||||
# create custom thumbnail folder if not existing
|
||||
if not os.path.exists(image_directory):
|
||||
os.makedirs(image_directory)
|
||||
|
||||
import shutil
|
||||
shutil.copyfile(filepath, destination_path)
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def remove_custom_image(filename):
|
||||
image_directory = get_custom_image_dir('custom_thumbnails')
|
||||
destination_path = os.path.join(image_directory, filename)
|
||||
|
||||
if os.path.isfile(destination_path):
|
||||
# make sure the image is only used once - else stop
|
||||
count = 0
|
||||
for v in read_widgets(JSON_USER_WIDGETS).values():
|
||||
if v.get("image") == filename:
|
||||
count += 1
|
||||
if count > 1:
|
||||
return False
|
||||
|
||||
try:
|
||||
os.remove(destination_path)
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
#### Thumbnail Render Functions ####
|
||||
def create_wireframe_copy(obj, use_color, color, thickness):
|
||||
copy = obj.copy()
|
||||
copy.data = obj.data.copy()
|
||||
if not use_color:
|
||||
copy.color = color
|
||||
|
||||
# Create a new Geometry Nodes modifier
|
||||
geo_mod = copy.modifiers.new(name="BoneWidget_WireFrame", type='NODES')
|
||||
|
||||
# Create a new node group and assign it to the modifier
|
||||
node_group = bpy.data.node_groups.new(
|
||||
name="BONEWIDGET_GeometryGroup", type='GeometryNodeTree')
|
||||
geo_mod.node_group = node_group
|
||||
|
||||
# Add input and output sockets
|
||||
node_group.interface.new_socket(
|
||||
name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
|
||||
node_group.interface.new_socket(
|
||||
name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
|
||||
# Add Thickness input
|
||||
thickness_socket = node_group.interface.new_socket(
|
||||
name="Thickness", in_out="INPUT", socket_type="NodeSocketFloat")
|
||||
thickness_socket.default_value = 0.5
|
||||
thickness_socket.min_value = 0.01
|
||||
thickness_socket.max_value = 2
|
||||
|
||||
# Create nodes
|
||||
node_input = node_group.nodes.new('NodeGroupInput')
|
||||
node_output = node_group.nodes.new('NodeGroupOutput')
|
||||
node_uv_sphere = node_group.nodes.new('GeometryNodeMeshUVSphere')
|
||||
node_mesh_to_curve = node_group.nodes.new('GeometryNodeMeshToCurve')
|
||||
node_curve_circle = node_group.nodes.new(
|
||||
'GeometryNodeCurvePrimitiveCircle')
|
||||
node_instance_on_points = node_group.nodes.new(
|
||||
'GeometryNodeInstanceOnPoints')
|
||||
node_curve_to_mesh = node_group.nodes.new('GeometryNodeCurveToMesh')
|
||||
node_join_geometry = node_group.nodes.new('GeometryNodeJoinGeometry')
|
||||
|
||||
# Set initial values (internal)
|
||||
node_uv_sphere.inputs["Segments"].default_value = 8
|
||||
node_uv_sphere.inputs["Rings"].default_value = 8
|
||||
node_curve_circle.inputs["Resolution"].default_value = 8
|
||||
|
||||
# Position nodes for better visualization (optional)
|
||||
node_input.location = (-400, 0)
|
||||
node_uv_sphere.location = (-150, 100)
|
||||
node_mesh_to_curve.location = (-150, -50)
|
||||
node_curve_circle.location = (-150, -150)
|
||||
node_instance_on_points.location = (100, 250)
|
||||
node_curve_to_mesh.location = (100, -100)
|
||||
node_join_geometry.location = (350, 0)
|
||||
node_output.location = (550, 0)
|
||||
|
||||
# Connect nodes
|
||||
node_group.links.new(
|
||||
node_input.outputs["Geometry"], node_instance_on_points.inputs["Points"])
|
||||
node_group.links.new(
|
||||
node_input.outputs["Geometry"], node_mesh_to_curve.inputs["Mesh"])
|
||||
node_group.links.new(
|
||||
node_input.outputs["Thickness"], node_uv_sphere.inputs["Radius"])
|
||||
node_group.links.new(
|
||||
node_input.outputs["Thickness"], node_curve_circle.inputs["Radius"])
|
||||
node_group.links.new(
|
||||
node_uv_sphere.outputs["Mesh"], node_instance_on_points.inputs["Instance"])
|
||||
node_group.links.new(
|
||||
node_mesh_to_curve.outputs["Curve"], node_curve_to_mesh.inputs["Curve"])
|
||||
node_group.links.new(
|
||||
node_curve_circle.outputs["Curve"], node_curve_to_mesh.inputs["Profile Curve"])
|
||||
node_group.links.new(
|
||||
node_instance_on_points.outputs["Instances"], node_join_geometry.inputs["Geometry"])
|
||||
node_group.links.new(
|
||||
node_curve_to_mesh.outputs["Mesh"], node_join_geometry.inputs["Geometry"])
|
||||
node_group.links.new(
|
||||
node_join_geometry.outputs["Geometry"], node_output.inputs["Geometry"])
|
||||
|
||||
# scale this so it isn't so sensitive
|
||||
geo_mod["Socket_2"] = (thickness / 10)
|
||||
|
||||
return copy
|
||||
|
||||
|
||||
def setup_viewport(context):
|
||||
area = context.area
|
||||
space = context.space_data
|
||||
region_3d = space.region_3d
|
||||
original_view_matrix = region_3d.view_matrix.copy()
|
||||
|
||||
bpy.ops.view3d.view_selected()
|
||||
|
||||
return original_view_matrix
|
||||
|
||||
|
||||
def restore_viewport_position(context, view_matrix, view_perspective):
|
||||
if context.space_data.type == 'VIEW_3D':
|
||||
region_3d = context.space_data.region_3d
|
||||
|
||||
# Restore viewport matrix position
|
||||
region_3d.view_matrix = view_matrix
|
||||
|
||||
# Restore viewport perspective
|
||||
region_3d.view_perspective = view_perspective
|
||||
|
||||
|
||||
def render_widget_thumbnail(image_name, widget_object, image_directory):
|
||||
if image_directory: # If True save to the current directory but...
|
||||
if bpy.data.filepath: # Check the file has been saved
|
||||
image_directory = os.path.dirname(bpy.data.filepath)
|
||||
else:
|
||||
# Fall back if it hasn't been saved
|
||||
image_directory = os.path.expanduser("~")
|
||||
# add '.png' to the name
|
||||
image_name = image_name + '.png'
|
||||
|
||||
else: # if False use the add-on location
|
||||
image_directory = get_custom_image_dir('custom_thumbnails')
|
||||
|
||||
destination_path = os.path.join(image_directory, image_name)
|
||||
|
||||
scene = bpy.context.scene
|
||||
scene.render.engine = 'BLENDER_WORKBENCH'
|
||||
scene.render.resolution_x, scene.render.resolution_y = (512, 512)
|
||||
scene.render.resolution_percentage = 100
|
||||
scene.render.image_settings.file_format = 'PNG'
|
||||
scene.render.image_settings.color_mode = 'RGBA'
|
||||
scene.view_settings.view_transform = 'Standard'
|
||||
scene.render.film_transparent = True
|
||||
scene.display.shading.light = 'FLAT'
|
||||
scene.display.shading.color_type = 'OBJECT'
|
||||
scene.render.filepath = image_directory
|
||||
|
||||
# Reframe Camera
|
||||
camera = scene.camera
|
||||
obj = widget_object
|
||||
frame_object_with_padding(camera, obj, padding=0.1)
|
||||
|
||||
bpy.ops.render.render(write_still=False)
|
||||
bpy.data.images['Render Result'].save_render(
|
||||
filepath=bpy.path.abspath(destination_path))
|
||||
|
||||
return bpy.path.abspath(destination_path)
|
||||
|
||||
|
||||
def add_camera_from_view(context):
|
||||
name = "BoneWidget_Thumbnail_Camera"
|
||||
|
||||
region_3d = context.region_data
|
||||
space = context.space_data
|
||||
|
||||
if region_3d is None or space.type != 'VIEW_3D':
|
||||
print("This must be run from a 3D Viewport.")
|
||||
return None
|
||||
|
||||
# Create camera data and object
|
||||
cam_data = bpy.data.cameras.new(name)
|
||||
cam_obj = bpy.data.objects.new(name, cam_data)
|
||||
context.scene.collection.objects.link(cam_obj)
|
||||
|
||||
# Align camera to current viewport
|
||||
cam_obj.matrix_world = region_3d.view_matrix.inverted()
|
||||
|
||||
# Make it the active camera
|
||||
context.scene.camera = cam_obj
|
||||
|
||||
return cam_obj
|
||||
|
||||
|
||||
def frame_object_with_padding(camera, obj, padding=0.1):
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
|
||||
# Get bounding box corners in world space
|
||||
coords = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
|
||||
|
||||
# Find center of bounding box
|
||||
center = sum(coords, Vector()) / len(coords)
|
||||
|
||||
# Scale each point away from the center to apply padding
|
||||
scaled_coords = [(center + (co - center) * (1 + padding)) for co in coords]
|
||||
|
||||
# Flatten the list of Vectors into a list of floats
|
||||
flat_coords = [v for co in scaled_coords for v in co]
|
||||
|
||||
# Use the camera fitting function
|
||||
cam_location, _ = camera.camera_fit_coords(depsgraph, flat_coords)
|
||||
camera.location = cam_location
|
||||
Reference in New Issue
Block a user