feat: Add export constraint tool
This commit is contained in:
parent
e6ff1aca20
commit
9340c074b8
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
venv
|
||||||
|
solstice.zip
|
6
dist.py
Normal file
6
dist.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Zip src/solstice
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
|
||||||
|
os.remove('solstice.zip')
|
||||||
|
shutil.make_archive('solstice', 'zip', 'src/')
|
50
src/solstice/__init__.py
Normal file
50
src/solstice/__init__.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
bl_info = {
|
||||||
|
"name": "Solstice Toolbox",
|
||||||
|
"blender": (3, 5, 0),
|
||||||
|
"category": "Misc",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test if the libraries are already loaded
|
||||||
|
# This is useful for reloading the addon during development
|
||||||
|
|
||||||
|
if "solstice.align_island" in sys.modules:
|
||||||
|
# Purge any existing modules with 'solstice.' prefix
|
||||||
|
for module_name in list(sys.modules.keys()):
|
||||||
|
if module_name.startswith("solstice."):
|
||||||
|
del sys.modules[module_name]
|
||||||
|
|
||||||
|
from .align_island import AlignIsland
|
||||||
|
from .export_constraint import ExportConstraint
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
def uv_menu_func(self, context):
|
||||||
|
self.layout.separator()
|
||||||
|
self.layout.label(text="Solstice Toolbox", icon="PLUGIN")
|
||||||
|
self.layout.operator(AlignIsland.bl_idname)
|
||||||
|
|
||||||
|
|
||||||
|
def bone_menu_func(self, context):
|
||||||
|
self.layout.separator()
|
||||||
|
self.layout.label(text="Solstice Toolbox", icon="PLUGIN")
|
||||||
|
self.layout.operator(ExportConstraint.bl_idname)
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_class(AlignIsland)
|
||||||
|
bpy.utils.register_class(ExportConstraint)
|
||||||
|
bpy.types.IMAGE_MT_uvs.append(uv_menu_func)
|
||||||
|
bpy.types.VIEW3D_MT_object.append(bone_menu_func)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.types.IMAGE_MT_uvs.remove(uv_menu_func)
|
||||||
|
bpy.types.VIEW3D_MT_object.remove(bone_menu_func)
|
||||||
|
bpy.utils.unregister_class(AlignIsland)
|
||||||
|
bpy.utils.unregister_class(ExportConstraint)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
register()
|
106
src/solstice/align_island.py
Normal file
106
src/solstice/align_island.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import math
|
||||||
|
import bpy
|
||||||
|
import bpy.types
|
||||||
|
import logging
|
||||||
|
import bmesh
|
||||||
|
|
||||||
|
|
||||||
|
# Thanks
|
||||||
|
# https://github.com/nutti/Magic-UV/blob/master/src/magic_uv/common.py#L1243
|
||||||
|
def get_uv_editable_objects(context):
|
||||||
|
if compat.check_version(2, 80, 0) < 0:
|
||||||
|
objs = []
|
||||||
|
else:
|
||||||
|
objs = [
|
||||||
|
o for o in bpy.data.objects
|
||||||
|
if compat.get_object_select(o) and o.type == 'MESH'
|
||||||
|
]
|
||||||
|
|
||||||
|
ob = context.active_object
|
||||||
|
if ob is not None:
|
||||||
|
objs.append(ob)
|
||||||
|
|
||||||
|
objs = list(set(objs))
|
||||||
|
return objs
|
||||||
|
|
||||||
|
|
||||||
|
class AlignIsland(bpy.types.Operator):
|
||||||
|
"""
|
||||||
|
Utility to align a UV island to grid direction based on the selected edge.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bl_idname = "uv.solstice_align_island"
|
||||||
|
bl_label = "Align Island"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
self.report({"INFO"}, "Aligning UV island")
|
||||||
|
|
||||||
|
objs = get_uv_editable_objects(context)
|
||||||
|
for obj in objs:
|
||||||
|
|
||||||
|
bm = bmesh.from_edit_mesh(obj.data)
|
||||||
|
if common.check_version(2, 73, 0) >= 0:
|
||||||
|
bm.faces.ensure_lookup_table()
|
||||||
|
uv_layer = bm.loops.layers.uv.verify()
|
||||||
|
|
||||||
|
# Get the selected UV edge
|
||||||
|
selected_edges = []
|
||||||
|
for edge, selected in uv_map.edge_selection.items():
|
||||||
|
if selected.value:
|
||||||
|
selected_edges.append(edge)
|
||||||
|
|
||||||
|
if len(selected_edges) != 1:
|
||||||
|
self.report(
|
||||||
|
{"ERROR"},
|
||||||
|
"Please select a single edge. Got: {} edge(s).".format(
|
||||||
|
len(selected_edges)))
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
# Get the UV island that the edge belongs to
|
||||||
|
selected_edge = selected_edges[0]
|
||||||
|
self.report({"INFO"}, f"Selected edge: {selected_edge.index}")
|
||||||
|
|
||||||
|
selected_island = None
|
||||||
|
for island in uv_map.data.islands:
|
||||||
|
if selected_edge in island.edges:
|
||||||
|
selected_island = island
|
||||||
|
break
|
||||||
|
|
||||||
|
if selected_island is None:
|
||||||
|
self.report({"ERROR"}, "Could not find UV island")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
# Get the UV coordinates of the selected edge
|
||||||
|
selected_edge_uv = []
|
||||||
|
for vert in selected_edge.vertices:
|
||||||
|
selected_edge_uv.append(selected_island.uv[vert].uv)
|
||||||
|
|
||||||
|
# Check the direction of the edge in UV space as degrees
|
||||||
|
edge_direction = (selected_edge_uv[1] -
|
||||||
|
selected_edge_uv[0]).to_track_quat(
|
||||||
|
"X", "Y").to_euler().z
|
||||||
|
|
||||||
|
# Choose the closest grid direction
|
||||||
|
if edge_direction < 45 or edge_direction > 315:
|
||||||
|
grid_direction = 0
|
||||||
|
elif edge_direction < 135:
|
||||||
|
grid_direction = 90
|
||||||
|
elif edge_direction < 225:
|
||||||
|
grid_direction = 180
|
||||||
|
else:
|
||||||
|
grid_direction = 270
|
||||||
|
|
||||||
|
# Calculate the rotation needed to align the island to the grid direction
|
||||||
|
rotation = grid_direction - edge_direction
|
||||||
|
|
||||||
|
self.report({"INFO"},
|
||||||
|
f"Edge direction: {math.degrees(edge_direction)}")
|
||||||
|
self.report({"INFO"}, f"Grid direction: {grid_direction}")
|
||||||
|
|
||||||
|
# Select the island
|
||||||
|
bpy.ops.uv.select_linked()
|
||||||
|
|
||||||
|
bpy.ops.uv.loop_rotate(radians=math.radians(rotation))
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
104
src/solstice/export_constraint.py
Normal file
104
src/solstice/export_constraint.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
def write_constraints_copy_rotation(f, constraint):
|
||||||
|
f.write(" (constraint 'copy_rotation'\n")
|
||||||
|
f.write(" influence: {}\n".format(constraint.influence))
|
||||||
|
f.write(" target: {}\n".format(constraint.target.name))
|
||||||
|
f.write(" subtarget: {}\n".format(constraint.subtarget))
|
||||||
|
f.write(" owner_space: {}\n".format(constraint.owner_space))
|
||||||
|
f.write(" target_space: {}\n".format(constraint.target_space))
|
||||||
|
f.write(" use_xyz: [{} {} {}]\n".format(
|
||||||
|
constraint.use_x,
|
||||||
|
constraint.use_y,
|
||||||
|
constraint.use_z,
|
||||||
|
))
|
||||||
|
f.write(" invert_xyz: [{} {} {}]\n".format(
|
||||||
|
constraint.invert_x,
|
||||||
|
constraint.invert_y,
|
||||||
|
constraint.invert_z,
|
||||||
|
))
|
||||||
|
f.write(" mix_mode: {}\n".format(constraint.mix_mode))
|
||||||
|
f.write(" euler_order: {}\n".format(constraint.euler_order))
|
||||||
|
f.write(" )")
|
||||||
|
|
||||||
|
|
||||||
|
def write_constraint_limit_rotation(f, constraint):
|
||||||
|
f.write(" (constraint 'limit_rotation'\n")
|
||||||
|
f.write(" influence: {}\n".format(constraint.influence))
|
||||||
|
f.write(" owner_space: {}\n".format(constraint.owner_space))
|
||||||
|
f.write(" use_limit_xyz: [{} {} {}]\n".format(
|
||||||
|
constraint.use_limit_x,
|
||||||
|
constraint.use_limit_y,
|
||||||
|
constraint.use_limit_z,
|
||||||
|
))
|
||||||
|
f.write(" min_xyz: [{} {} {}]\n".format(
|
||||||
|
constraint.min_x,
|
||||||
|
constraint.min_y,
|
||||||
|
constraint.min_z,
|
||||||
|
))
|
||||||
|
f.write(" max_xyz: [{} {} {}]\n".format(
|
||||||
|
constraint.max_x,
|
||||||
|
constraint.max_y,
|
||||||
|
constraint.max_z,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def write_bone_constraints(context, filepath):
|
||||||
|
# Get the active object
|
||||||
|
obj = context.active_object
|
||||||
|
|
||||||
|
# Open the file for writing
|
||||||
|
with open(filepath, 'w') as file:
|
||||||
|
# Iterate over all bones
|
||||||
|
for bone in obj.pose.bones:
|
||||||
|
# Write bone name
|
||||||
|
file.write("(bone '{}'".format(bone.name))
|
||||||
|
|
||||||
|
# Iterate over all bone constraints
|
||||||
|
for constraint in bone.constraints:
|
||||||
|
if not constraint.enabled or constraint.mute or not constraint.active:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Write constraint type and settings
|
||||||
|
file.write("\n")
|
||||||
|
|
||||||
|
if isinstance(constraint, bpy.types.CopyRotationConstraint):
|
||||||
|
write_constraints_copy_rotation(file, constraint)
|
||||||
|
elif isinstance(constraint, bpy.types.LimitRotationConstraint):
|
||||||
|
write_constraint_limit_rotation(file, constraint)
|
||||||
|
else:
|
||||||
|
# Unknown constraint type
|
||||||
|
# Print python class name
|
||||||
|
file.write("; unknown constraint.\n; class: {}\n".format(
|
||||||
|
constraint.__class__.__name__))
|
||||||
|
file.write("; (constraint '{}'\n".format(constraint.type))
|
||||||
|
for prop in dir(constraint):
|
||||||
|
if not prop.startswith('_') and not callable(
|
||||||
|
getattr(constraint, prop)):
|
||||||
|
file.write("; {}: {}\n".format(
|
||||||
|
prop, getattr(constraint, prop)))
|
||||||
|
|
||||||
|
file.write("; )\n")
|
||||||
|
file.write(")\n")
|
||||||
|
|
||||||
|
|
||||||
|
class ExportConstraint(bpy.types.Operator):
|
||||||
|
bl_idname = "solstice.export_bone_constraints"
|
||||||
|
bl_label = "Export Bone Constraints"
|
||||||
|
|
||||||
|
filepath: bpy.props.StringProperty()
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
# Do something with the selected file path
|
||||||
|
print("Selected file:", self.filepath)
|
||||||
|
# Call the function or perform the desired actions using the file path
|
||||||
|
|
||||||
|
write_bone_constraints(context, self.filepath)
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# Open the file browser dialog
|
||||||
|
context.window_manager.fileselect_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
Loading…
Reference in New Issue
Block a user