diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d89075f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv +solstice.zip diff --git a/dist.py b/dist.py new file mode 100644 index 0000000..0b0c82a --- /dev/null +++ b/dist.py @@ -0,0 +1,6 @@ +# Zip src/solstice +import shutil +import os + +os.remove('solstice.zip') +shutil.make_archive('solstice', 'zip', 'src/') diff --git a/src/solstice/__init__.py b/src/solstice/__init__.py new file mode 100644 index 0000000..df50e2f --- /dev/null +++ b/src/solstice/__init__.py @@ -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() diff --git a/src/solstice/align_island.py b/src/solstice/align_island.py new file mode 100644 index 0000000..62f7da1 --- /dev/null +++ b/src/solstice/align_island.py @@ -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"} diff --git a/src/solstice/export_constraint.py b/src/solstice/export_constraint.py new file mode 100644 index 0000000..c137e7e --- /dev/null +++ b/src/solstice/export_constraint.py @@ -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'}