diff --git a/simple_deform_helper/__init__.py b/simple_deform_helper/__init__.py new file mode 100644 index 000000000..58b44ad5c --- /dev/null +++ b/simple_deform_helper/__init__.py @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +from . import ( + panel, # + gizmo, + utils, + update, + translate, + operators, + preferences, +) + +bl_info = { + "name": "Simple Deform Helper", + "author": "AIGODLIKE Community(BlenderCN辣椒,小萌新)", + "version": (0, 2, 0), + "blender": (3, 0, 0), + "location": "3D View -> Select an object and the active modifier is simple deformation", + "description": "Simple Deform visualization adjustment tool", + "doc_url": "https://github.com/AIGODLIKE/simple_deform_helper/blob/main/README.md", + "category": "3D View" +} + +""" +# ------------------------- +__init__.py: + Register All Module + +gizmo/__init__.py: + Register All Gizmo + + /angle_and_factor.py: + Ctrl Modifier Angle + + /bend_axis.py: + Bend Method Switch Direction Gizmo + + /set_deform_axis.py: + Three Switch Deform Axis Operator Gizmo + + /up_down_limits_point.py: + Main control part + use utils.py PublicProperty._get_limits_point_and_bound_box_co + Obtain and calculate boundary box and limit point data + + +draw.py: + Draw 3D Bound And Line + +gizmo.json: + Draw Custom Shape Vertex Data + +operator.py: + Set Deform Axis Operator + +panel.py: + Draw Gizmo Tool Property in Options and Tool Settings Right + +preferences.py: + Addon Preferences + +translate.py: + temporary only Cn translate + +update.py: + In Change Depsgraph When Update Addon Data And Del Redundant Empty + +utils.py: + Main documents used + Most computing operations are placed in classes GizmoUtils +# ------------------------- +""" +module_tuple = ( + panel, + gizmo, + utils, + update, + translate, + operators, + preferences, +) + + +def register(): + for item in module_tuple: + item.register() + + +def unregister(): + for item in module_tuple: + item.unregister() diff --git a/simple_deform_helper/draw.py b/simple_deform_helper/draw.py new file mode 100644 index 000000000..f04160e82 --- /dev/null +++ b/simple_deform_helper/draw.py @@ -0,0 +1,181 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +import blf +import bpy +import gpu +from gpu_extras.batch import batch_for_shader +from mathutils import Vector + +from .update import ChangeActiveObject, simple_update +from .utils import GizmoUtils + + +class DrawPublic(GizmoUtils): + G_HandleData = {} # Save draw Handle + + @classmethod + def draw_3d_shader(cls, pos, indices, color=None, *, shader_name='3D_UNIFORM_COLOR', draw_type='LINES'): + shader = gpu.shader.from_builtin(shader_name) + if draw_type == 'POINTS': + batch = batch_for_shader(shader, draw_type, {'pos': pos}) + else: + batch = batch_for_shader( + shader, draw_type, {'pos': pos}, indices=indices) + + shader.bind() + if color: + shader.uniform_float('color', color) + + batch.draw(shader) + + @property + def draw_poll(self) -> bool: + if simple_update.timers_update_poll(): + is_switch_obj = ChangeActiveObject.is_change_active_object(False) + if self.poll_simple_deform_public(bpy.context) and not is_switch_obj: + return True + return False + + +class DrawText(DrawPublic): + font_info = { + 'font_id': 0, + 'handler': None, + } + text_key = 'handler_text' + + @classmethod + def add_text_handler(cls): + key = cls.text_key + if key not in cls.G_HandleData: + cls.G_HandleData[key] = bpy.types.SpaceView3D.draw_handler_add( + DrawText().draw_text_handler, (), 'WINDOW', 'POST_PIXEL') + + @classmethod + def del_text_handler(cls): + key = cls.text_key + if key in cls.G_HandleData: + bpy.types.SpaceView3D.draw_handler_remove( + cls.G_HandleData[key], 'WINDOW') + cls.G_HandleData.pop(key) + + @classmethod + def obj_is_scale(cls) -> bool: + ob = bpy.context.object + scale_error = ob and (ob.scale != Vector((1, 1, 1))) + return scale_error + + def draw_text_handler(self): + if self.draw_poll and self.obj_is_scale(): + self.draw_scale_text() + + def draw_scale_text(self): + obj = bpy.context.object + font_id = self.font_info['font_id'] + blf.position(font_id, 200, 80, 0) + blf.size(font_id, 15, 72) + blf.color(font_id, 1, 1, 1, 1) + blf.draw( + font_id, + f'The scaling value of the object {obj.name_full} is not 1,' + f' which will cause the deformation of the simple deformation modifier.' + f' Please apply the scaling before deformation') + + @classmethod + def draw_text(cls, x, y, text='Hello Word', font_id=0, size=10, *, color=(0.5, 0.5, 0.5, 1), dpi=72, column=0): + blf.position(font_id, x, y - (size * (column + 1)), 0) + blf.size(font_id, size, dpi) + blf.draw(font_id, text) + blf.color(font_id, *color) + + +class DrawHandler(DrawText): + @classmethod + def add_handler(cls): + if 'handler' not in cls.G_HandleData: + cls.G_HandleData['handler'] = bpy.types.SpaceView3D.draw_handler_add( + Draw3D().draw, (), 'WINDOW', 'POST_VIEW') + + cls.add_text_handler() + + @classmethod + def del_handler(cls): + data = bpy.data + if data.meshes.get(cls.G_NAME): + data.meshes.remove(data.meshes.get(cls.G_NAME)) + + if data.objects.get(cls.G_NAME): + data.objects.remove(data.objects.get(cls.G_NAME)) + + if 'handler' in cls.G_HandleData: + bpy.types.SpaceView3D.draw_handler_remove( + cls.G_HandleData['handler'], 'WINDOW') + cls.G_HandleData.clear() + + cls.del_text_handler() + + +class Draw3D(DrawHandler): + + def draw(self): + gpu.state.blend_set('ALPHA') + gpu.state.line_width_set(1) + + gpu.state.blend_set('ALPHA') + gpu.state.depth_test_set('ALWAYS') + + if self.draw_poll: + self.draw_3d(bpy.context) + + def draw_3d(self, context): + if not self.modifier_origin_is_available: + self.draw_bound_box() + elif self.simple_deform_show_gizmo_poll(context): + # draw bound box + self.draw_bound_box() + self.draw_deform_mesh() + self.draw_limits_line() + self.draw_limits_bound_box() + + self.draw_text_handler() + elif self.poll_simple_deform_show_bend_axis_witch(context): + self.draw_bound_box() + + def draw_bound_box(self): + coords = self.matrix_calculation(self.obj_matrix_world, + self.tow_co_to_coordinate(self.modifier_bound_co)) + self.draw_3d_shader(coords, self.G_INDICES, self.pref.bound_box_color) + + def draw_limits_bound_box(self): + self.draw_3d_shader(self.modifier_limits_bound_box, + self.G_INDICES, + self.pref.limits_bound_box_color, + ) + + def draw_limits_line(self): + up_point, down_point, up_limits, down_limits = self.modifier_limits_point + # draw limits line + self.draw_3d_shader((up_limits, down_limits), ((1, 0),), (1, 1, 0, 0.5)) + # draw line + self.draw_3d_shader((up_point, down_point), ((1, 0),), (1, 1, 0, 0.3)) + + # draw pos + self.draw_3d_shader([down_point], (), (0, 1, 0, 0.5), + shader_name='3D_UNIFORM_COLOR', draw_type='POINTS') + self.draw_3d_shader([up_point], (), (1, 0, 0, 0.5), + shader_name='3D_UNIFORM_COLOR', draw_type='POINTS') + + def draw_deform_mesh(self): + ob = self.obj + deform_data = self.G_DeformDrawData + active = self.modifier + # draw deform mesh + if 'simple_deform_bound_data' in deform_data and self.pref.update_deform_wireframe: + modifiers = self.get_modifiers_parameter(self.modifier) + pos, indices, mat, mod_data, limits = deform_data['simple_deform_bound_data'] + is_limits = limits == active.limits[:] + is_mat = (ob.matrix_world == mat) + if modifiers == mod_data and is_mat and is_limits: + self.draw_3d_shader(pos, indices, self.pref.deform_wireframe_color) + + def draw_origin_error(self): + ... diff --git a/simple_deform_helper/gizmo.json b/simple_deform_helper/gizmo.json new file mode 100644 index 000000000..7f0694103 --- /dev/null +++ b/simple_deform_helper/gizmo.json @@ -0,0 +1 @@ +{"SimpleDeform_GizmoGroup_": [[-0.54, 0.12, -0.96], [-0.54, -0.82, -0.23], [-0.54, 0.19, -0.64], [0.54, 0.37, -0.37], [-0.54, 0.96, -0.12], [0.54, 0.96, -0.12], [0.32, -0.29, 0.5], [0.54, -0.4, 0.4], [0.55, 0.96, 0.96], [0.54, -0.96, -0.96], [-0.54, 0.12, -0.96], [0.54, 0.12, -0.96], [0.54, 0.96, -0.12], [-0.54, 0.96, 0.96], [0.55, 0.96, 0.96], [-0.54, 0.19, -0.64], [-0.54, -0.4, 0.4], [-0.54, 0.37, -0.37], [0.54, 0.19, -0.64], [-0.54, 0.37, -0.37], [0.54, 0.37, -0.37], [0.32, -0.94, -0.69], [-0.33, -0.94, -0.69], [-0.54, -0.96, -0.96], [0.54, 0.12, -0.96], [-0.54, 0.19, -0.64], [0.54, 0.19, -0.64], [0.32, -0.94, 0.95], [0.32, -0.94, -0.69], [0.32, -0.81, -0.23], [0.32, -0.94, -0.69], [0.32, -0.94, 0.95], [-0.33, -0.94, -0.69], [-0.33, -0.29, 0.5], [0.32, -0.29, 0.95], [0.32, -0.29, 0.5], [0.54, -0.4, 0.4], [0.54, -0.81, -0.23], [0.54, 0.37, -0.37], [0.32, -0.81, -0.23], [0.54, -0.4, 0.4], [0.32, -0.29, 0.5], [-0.54, -0.4, 0.4], [-0.33, -0.81, -0.23], [-0.33, -0.29, 0.5], [-0.54, -0.4, 0.4], [-0.54, 0.96, -0.12], [-0.54, 0.37, -0.37], [0.32, -0.94, 0.95], [-0.33, -0.29, 0.5], [-0.33, -0.81, -0.23], [-0.54, 0.12, -0.96], [-0.54, -0.96, -0.96], [-0.54, -0.82, -0.23], [0.54, 0.37, -0.37], [-0.54, 0.37, -0.37], [-0.54, 0.96, -0.12], [0.55, 0.96, 0.96], [-0.54, 0.96, 0.96], [0.32, -0.29, 0.5], [-0.54, 0.96, 0.96], [-0.54, -0.4, 0.4], [-0.33, -0.29, 0.5], [-0.54, 0.96, 0.96], [-0.33, -0.29, 0.5], [0.32, -0.29, 0.5], [0.54, -0.96, -0.96], [-0.54, -0.96, -0.96], [-0.54, 0.12, -0.96], [0.54, 0.96, -0.12], [-0.54, 0.96, -0.12], [-0.54, 0.96, 0.96], [-0.54, 0.19, -0.64], [-0.54, -0.82, -0.23], [-0.54, -0.4, 0.4], [0.54, 0.19, -0.64], [-0.54, 0.19, -0.64], [-0.54, 0.37, -0.37], [0.54, -0.96, -0.96], [0.54, -0.81, -0.23], [0.32, -0.94, -0.69], [0.54, -0.81, -0.23], [0.32, -0.81, -0.23], [0.32, -0.94, -0.69], [-0.33, -0.81, -0.23], [-0.54, -0.82, -0.23], [-0.33, -0.94, -0.69], [-0.54, -0.82, -0.23], [-0.54, -0.96, -0.96], [-0.33, -0.94, -0.69], [0.54, -0.96, -0.96], [0.32, -0.94, -0.69], [-0.54, -0.96, -0.96], [0.54, 0.12, -0.96], [-0.54, 0.12, -0.96], [-0.54, 0.19, -0.64], [0.32, -0.81, -0.23], [0.32, -0.29, 0.5], [0.32, -0.94, 0.95], [0.32, -0.29, 0.5], [0.32, -0.29, 0.95], [0.32, -0.94, 0.95], [0.54, -0.96, -0.96], [0.54, 0.12, -0.96], [0.54, 0.19, -0.64], [0.54, 0.37, -0.37], [0.54, 0.96, -0.12], [0.54, -0.4, 0.4], [0.54, 0.96, -0.12], [0.55, 0.96, 0.96], [0.54, -0.4, 0.4], [0.54, -0.96, -0.96], [0.54, 0.19, -0.64], [0.54, -0.81, -0.23], [0.54, 0.19, -0.64], [0.54, 0.37, -0.37], [0.54, -0.81, -0.23], [0.32, -0.81, -0.23], [0.54, -0.81, -0.23], [0.54, -0.4, 0.4], [-0.54, -0.4, 0.4], [-0.54, -0.82, -0.23], [-0.33, -0.81, -0.23], [-0.54, -0.4, 0.4], [-0.54, 0.96, 0.96], [-0.54, 0.96, -0.12], [-0.33, -0.81, -0.23], [-0.33, -0.94, -0.69], [0.32, -0.94, 0.95], [0.32, -0.94, 0.95], [0.32, -0.29, 0.95], [-0.33, -0.29, 0.5]], "None_GizmoGroup_": [[0.97, -0.01, 0.18], [0.98, -0.01, -0.24], [0.96, -0.01, -0.03], [0.53, -0.01, 0.24], [0.15, -0.01, 0.24], [0.22, -0.01, -0.24], [-0.59, -0.01, 0.24], [-0.97, -0.01, 0.24], [-0.9, -0.01, -0.24], [-0.19, -0.0, 0.25], [-0.09, -0.0, 0.17], [-0.06, -0.0, 0.22], [-0.4, -0.0, 0.13], [-0.28, -0.0, 0.17], [-0.31, -0.0, 0.22], [0.07, -0.0, -0.0], [-0.02, -0.0, -0.1], [0.03, -0.0, -0.13], [-0.19, -0.0, -0.25], [-0.28, 0.0, -0.17], [-0.31, 0.0, -0.22], [-0.4, -0.0, 0.13], [-0.38, -0.0, -0.0], [-0.35, -0.0, 0.1], [0.07, -0.0, -0.0], [-0.02, -0.0, 0.1], [0.01, -0.0, -0.0], [-0.06, 0.0, -0.22], [-0.19, -0.0, -0.19], [-0.19, -0.0, -0.25], [-0.4, -0.0, -0.13], [-0.38, -0.0, -0.0], [-0.44, -0.0, -0.0], [-0.06, -0.0, 0.22], [-0.02, -0.0, 0.1], [0.03, -0.0, 0.13], [-0.31, -0.0, 0.22], [-0.19, -0.0, 0.19], [-0.19, -0.0, 0.25], [0.03, -0.0, -0.13], [-0.09, 0.0, -0.17], [-0.06, 0.0, -0.22], [-0.4, -0.0, -0.13], [-0.28, 0.0, -0.17], [-0.35, -0.0, -0.1], [-0.19, -0.0, 0.25], [-0.19, -0.0, 0.19], [-0.09, -0.0, 0.17], [-0.4, -0.0, 0.13], [-0.35, -0.0, 0.1], [-0.28, -0.0, 0.17], [0.07, -0.0, -0.0], [0.01, -0.0, -0.0], [-0.02, -0.0, -0.1], [-0.19, -0.0, -0.25], [-0.19, -0.0, -0.19], [-0.28, 0.0, -0.17], [-0.4, -0.0, 0.13], [-0.44, -0.0, -0.0], [-0.38, -0.0, -0.0], [0.07, -0.0, -0.0], [0.03, -0.0, 0.13], [-0.02, -0.0, 0.1], [-0.06, 0.0, -0.22], [-0.09, 0.0, -0.17], [-0.19, -0.0, -0.19], [-0.4, -0.0, -0.13], [-0.35, -0.0, -0.1], [-0.38, -0.0, -0.0], [-0.06, -0.0, 0.22], [-0.09, -0.0, 0.17], [-0.02, -0.0, 0.1], [-0.31, -0.0, 0.22], [-0.28, -0.0, 0.17], [-0.19, -0.0, 0.19], [0.03, -0.0, -0.13], [-0.02, -0.0, -0.1], [-0.09, 0.0, -0.17], [-0.4, -0.0, -0.13], [-0.31, 0.0, -0.22], [-0.28, 0.0, -0.17]], "SimpleDeform_Bend_Direction_": [[-3.04, -0.0, 1.68], [-2.79, -0.0, 1.35], [-2.79, 0.1, 1.35], [0.0, 0.0, 0.28], [-0.36, -0.03, 0.31], [0.0, -0.03, 0.28], [-2.22, 0.0, 0.87], [-1.9, 0.04, 0.71], [-2.22, 0.12, 0.87], [-2.22, 0.0, 0.87], [-1.9, -0.04, 0.71], [-1.9, 0.0, 0.71], [-3.04, -0.0, 1.68], [-2.79, -0.1, 1.35], [-2.79, -0.0, 1.35], [-2.79, 0.1, 1.35], [-2.51, 0.0, 1.06], [-2.51, 0.13, 1.06], [0.0, 0.03, 0.28], [-0.36, 0.0, 0.31], [0.0, 0.0, 0.28], [-1.9, 0.0, 0.71], [-1.43, -0.04, 0.53], [-1.43, 0.0, 0.53], [-1.43, 0.0, 0.53], [-0.86, -0.04, 0.38], [-0.86, 0.0, 0.38], [-0.86, 0.0, 0.38], [-0.36, -0.03, 0.31], [-0.36, 0.0, 0.31], [-1.9, 0.0, 0.71], [-1.43, 0.04, 0.53], [-1.9, 0.04, 0.71], [-1.43, 0.0, 0.53], [-0.86, 0.04, 0.38], [-1.43, 0.04, 0.53], [-0.86, 0.0, 0.38], [-0.36, 0.03, 0.31], [-0.86, 0.04, 0.38], [-2.22, 0.12, 0.87], [-2.51, 0.0, 1.06], [-2.22, 0.0, 0.87], [-2.79, -0.1, 1.35], [-2.51, 0.0, 1.06], [-2.79, -0.0, 1.35], [-2.22, -0.12, 0.87], [-2.51, 0.0, 1.06], [-2.51, -0.13, 1.06], [3.04, -0.0, 1.68], [2.79, 0.1, 1.35], [2.79, -0.0, 1.35], [0.36, -0.03, 0.31], [0.0, 0.0, 0.28], [0.0, -0.03, 0.28], [1.9, 0.04, 0.71], [2.22, 0.0, 0.87], [2.22, 0.12, 0.87], [2.22, 0.0, 0.87], [1.9, -0.04, 0.71], [2.22, -0.12, 0.87], [3.04, -0.0, 1.68], [2.79, -0.0, 1.35], [2.79, -0.1, 1.35], [2.79, 0.1, 1.35], [2.51, 0.0, 1.06], [2.79, -0.0, 1.35], [0.36, 0.0, 0.31], [0.0, 0.03, 0.28], [0.0, 0.0, 0.28], [1.9, 0.0, 0.71], [1.43, -0.04, 0.53], [1.9, -0.04, 0.71], [1.43, 0.0, 0.53], [0.86, -0.04, 0.38], [1.43, -0.04, 0.53], [0.86, 0.0, 0.38], [0.36, -0.03, 0.31], [0.86, -0.04, 0.38], [1.43, 0.04, 0.53], [1.9, 0.0, 0.71], [1.9, 0.04, 0.71], [0.86, 0.04, 0.38], [1.43, 0.0, 0.53], [1.43, 0.04, 0.53], [0.36, 0.03, 0.31], [0.86, 0.0, 0.38], [0.86, 0.04, 0.38], [2.51, 0.0, 1.06], [2.22, 0.12, 0.87], [2.22, 0.0, 0.87], [2.51, 0.0, 1.06], [2.79, -0.1, 1.35], [2.79, -0.0, 1.35], [2.22, -0.12, 0.87], [2.51, 0.0, 1.06], [2.22, 0.0, 0.87], [0.0, 0.0, 0.28], [-0.36, 0.0, 0.31], [-0.36, -0.03, 0.31], [-2.22, 0.0, 0.87], [-1.9, 0.0, 0.71], [-1.9, 0.04, 0.71], [-2.22, 0.0, 0.87], [-2.22, -0.12, 0.87], [-1.9, -0.04, 0.71], [-2.79, 0.1, 1.35], [-2.79, -0.0, 1.35], [-2.51, 0.0, 1.06], [0.0, 0.03, 0.28], [-0.36, 0.03, 0.31], [-0.36, 0.0, 0.31], [-1.9, 0.0, 0.71], [-1.9, -0.04, 0.71], [-1.43, -0.04, 0.53], [-1.43, 0.0, 0.53], [-1.43, -0.04, 0.53], [-0.86, -0.04, 0.38], [-0.86, 0.0, 0.38], [-0.86, -0.04, 0.38], [-0.36, -0.03, 0.31], [-1.9, 0.0, 0.71], [-1.43, 0.0, 0.53], [-1.43, 0.04, 0.53], [-1.43, 0.0, 0.53], [-0.86, 0.0, 0.38], [-0.86, 0.04, 0.38], [-0.86, 0.0, 0.38], [-0.36, 0.0, 0.31], [-0.36, 0.03, 0.31], [-2.22, 0.12, 0.87], [-2.51, 0.13, 1.06], [-2.51, 0.0, 1.06], [-2.79, -0.1, 1.35], [-2.51, -0.13, 1.06], [-2.51, 0.0, 1.06], [-2.22, -0.12, 0.87], [-2.22, 0.0, 0.87], [-2.51, 0.0, 1.06], [0.36, -0.03, 0.31], [0.36, 0.0, 0.31], [0.0, 0.0, 0.28], [1.9, 0.04, 0.71], [1.9, 0.0, 0.71], [2.22, 0.0, 0.87], [2.22, 0.0, 0.87], [1.9, 0.0, 0.71], [1.9, -0.04, 0.71], [2.79, 0.1, 1.35], [2.51, 0.13, 1.06], [2.51, 0.0, 1.06], [0.36, 0.0, 0.31], [0.36, 0.03, 0.31], [0.0, 0.03, 0.28], [1.9, 0.0, 0.71], [1.43, 0.0, 0.53], [1.43, -0.04, 0.53], [1.43, 0.0, 0.53], [0.86, 0.0, 0.38], [0.86, -0.04, 0.38], [0.86, 0.0, 0.38], [0.36, 0.0, 0.31], [0.36, -0.03, 0.31], [1.43, 0.04, 0.53], [1.43, 0.0, 0.53], [1.9, 0.0, 0.71], [0.86, 0.04, 0.38], [0.86, 0.0, 0.38], [1.43, 0.0, 0.53], [0.36, 0.03, 0.31], [0.36, 0.0, 0.31], [0.86, 0.0, 0.38], [2.51, 0.0, 1.06], [2.51, 0.13, 1.06], [2.22, 0.12, 0.87], [2.51, 0.0, 1.06], [2.51, -0.13, 1.06], [2.79, -0.1, 1.35], [2.22, -0.12, 0.87], [2.51, -0.13, 1.06], [2.51, 0.0, 1.06]], "Sphere_GizmoGroup_": [[-0.0, -0.71, 0.0], [0.61, 0.35, 0.0], [-0.61, 0.35, 0.0], [-0.61, 0.35, 0.0], [-0.71, -0.0, 0.0], [-0.0, -0.71, 0.0], [-0.71, -0.0, 0.0], [-0.61, -0.35, 0.0], [-0.0, -0.71, 0.0], [-0.61, -0.35, 0.0], [-0.35, -0.61, 0.0], [-0.0, -0.71, 0.0], [-0.0, -0.71, 0.0], [0.35, -0.61, 0.0], [0.61, -0.35, 0.0], [0.61, -0.35, 0.0], [0.71, -0.0, 0.0], [0.61, 0.35, 0.0], [0.61, 0.35, 0.0], [0.35, 0.61, 0.0], [-0.0, 0.71, 0.0], [-0.0, 0.71, 0.0], [-0.35, 0.61, 0.0], [-0.61, 0.35, 0.0], [-0.0, -0.71, 0.0], [0.61, -0.35, 0.0], [0.61, 0.35, 0.0], [0.61, 0.35, 0.0], [-0.0, 0.71, 0.0], [-0.61, 0.35, 0.0]]} \ No newline at end of file diff --git a/simple_deform_helper/gizmo/__init__.py b/simple_deform_helper/gizmo/__init__.py new file mode 100644 index 000000000..756b41aec --- /dev/null +++ b/simple_deform_helper/gizmo/__init__.py @@ -0,0 +1,32 @@ +import bpy + +from .angle_and_factor import AngleGizmoGroup, AngleGizmo +from .bend_axis import BendAxiSwitchGizmoGroup, CustomGizmo +from .set_deform_axis import SetDeformGizmoGroup +from .up_down_limits_point import UpDownLimitsGizmo, UpDownLimitsGizmoGroup +from ..draw import Draw3D + +class_list = ( + UpDownLimitsGizmo, + UpDownLimitsGizmoGroup, + + AngleGizmo, + AngleGizmoGroup, + + CustomGizmo, + BendAxiSwitchGizmoGroup, + + SetDeformGizmoGroup, +) + +register_class, unregister_class = bpy.utils.register_classes_factory(class_list) + + +def register(): + Draw3D.add_handler() + register_class() + + +def unregister(): + Draw3D.del_handler() + unregister_class() diff --git a/simple_deform_helper/gizmo/angle_and_factor.py b/simple_deform_helper/gizmo/angle_and_factor.py new file mode 100644 index 000000000..c0c69ed3b --- /dev/null +++ b/simple_deform_helper/gizmo/angle_and_factor.py @@ -0,0 +1,161 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +import math + +from bpy.types import Gizmo +from bpy.types import ( + GizmoGroup, +) + +from ..update import ChangeActiveModifierParameter +from ..utils import GizmoUtils, GizmoGroupUtils + + +class AngleUpdate(GizmoUtils): + int_value_degrees: float + tmp_value_angle: float + + def get_snap(self, delta, tweak): + is_snap = 'SNAP' in tweak + is_precise = 'PRECISE' in tweak + if is_snap and is_precise: + delta = round(delta) + elif is_snap: + delta //= 5 + delta *= 5 + elif is_precise: + delta /= self.mouse_dpi + delta //= 0.01 + delta *= 0.01 + return delta + + def update_prop_value(self, event, tweak): + def v(va): + self.target_set_value('angle', math.radians(va)) + + not_c_l = not event.alt and not event.ctrl + is_only_shift = event.shift and not_c_l + + change_angle = self.get_delta(event) + if is_only_shift: + change_angle /= 50 + new_value = self.tmp_value_angle - change_angle + old_value = self.target_get_value('angle') + snap_value = self.get_snap(new_value, tweak) + + is_shift = event.type == 'LEFT_SHIFT' + if is_only_shift: + if event.value == 'PRESS': + self.init_mouse_region_x = event.mouse_region_x + self.tmp_value_angle = int(math.degrees(old_value)) + v(self.tmp_value_angle) + return + + value = (self.tmp_value_angle - change_angle) // 0.01 * 0.01 + v(value) + return + + elif not_c_l and not event.shift and is_shift and event.value == 'RELEASE': + self.init_mouse_region_x = event.mouse_region_x + # new_value = self.tmp_value_angle = math.degrees(old_value) + return + v(snap_value) + + def update_gizmo_matrix(self, context): + matrix = context.object.matrix_world + point = self.modifier_bound_co[1] + self.matrix_basis = self.obj_matrix_world + self.matrix_basis.translation = matrix @ point + + def update_header_text(self, context): + te = self.translate_text + text = te(self.modifier.deform_method.title()) + ' ' + + if self.modifier_is_use_angle_value: + value = round(math.degrees(self.modifier_angle), 3) + text += self.translate_header_text('Angle', value) + else: + value = round(self.modifier.factor, 3) + text += self.translate_header_text('Coefficient', value) + context.area.header_text_set(text) + + +class AngleGizmo(Gizmo, AngleUpdate): + bl_idname = 'ViewSimpleAngleGizmo' + + bl_target_properties = ( + {'id': 'up_limits', 'type': 'FLOAT', 'array_length': 1}, + {'id': 'down_limits', 'type': 'FLOAT', 'array_length': 1}, + {'id': 'angle', 'type': 'FLOAT', 'array_length': 1}, + ) + + __slots__ = ( + 'draw_type', + 'mouse_dpi', + 'empty_object', + 'custom_shape', + 'tmp_value_angle', + 'int_value_degrees', + 'init_mouse_region_y', + 'init_mouse_region_x', + ) + + def setup(self): + self.init_setup() + + def invoke(self, context, event): + self.init_invoke(context, event) + self.int_value_degrees = self.target_get_value('angle') + angle = math.degrees(self.int_value_degrees) + self.tmp_value_angle = angle + return {'RUNNING_MODAL'} + + def modal(self, context, event, tweak): + self.clear_point_cache() + + self.update_prop_value(event, tweak) + self.update_deform_wireframe() + self.update_header_text(context) + ChangeActiveModifierParameter.update_modifier_parameter() + self.tag_redraw(context) + return self.event_handle(event) + + def exit(self, context, cancel): + context.area.header_text_set(None) + if cancel: + self.target_set_value('angle', self.int_value_degrees) + + +class AngleGizmoGroup(GizmoGroup, GizmoGroupUtils): + """ShowGizmo + """ + bl_idname = 'OBJECT_GGT_SimpleDeformGizmoGroup' + bl_label = 'AngleGizmoGroup' + + @classmethod + def poll(cls, context): + return cls.simple_deform_show_gizmo_poll(context) + + def setup(self, context): + sd_name = AngleGizmo.bl_idname + + add_data = [ + ('angle', + sd_name, + {'draw_type': 'SimpleDeform_GizmoGroup_', + 'color': (1.0, 0.5, 1.0), + 'alpha': 0.3, + 'color_highlight': (1.0, 1.0, 1.0), + 'alpha_highlight': 0.3, + 'use_draw_modal': True, + 'scale_basis': 0.1, + 'use_draw_value': True, + 'mouse_dpi': 5, + }), + ] + + self.generate_gizmo(add_data) + + def refresh(self, context): + self.angle.target_set_prop('angle', + context.object.modifiers.active, + 'angle') diff --git a/simple_deform_helper/gizmo/bend_axis.py b/simple_deform_helper/gizmo/bend_axis.py new file mode 100644 index 000000000..b9d7a5909 --- /dev/null +++ b/simple_deform_helper/gizmo/bend_axis.py @@ -0,0 +1,115 @@ +import math + +from bpy.types import GizmoGroup +from bpy_types import Gizmo +from mathutils import Euler, Vector + +from ..utils import GizmoUtils, GizmoGroupUtils + + +class CustomGizmo(Gizmo, GizmoUtils): + """Draw Custom Gizmo""" + bl_idname = '_Custom_Gizmo' + draw_type: str + custom_shape: dict + + def setup(self): + self.init_setup() + + def draw(self, context): + self.draw_custom_shape(self.custom_shape[self.draw_type]) + + def draw_select(self, context, select_id): + self.draw_custom_shape( + self.custom_shape[self.draw_type], select_id=select_id) + + def invoke(self, context, event): + self.init_invoke(context, event) + return {'RUNNING_MODAL'} + + def modal(self, context, event, tweak): + self.update_empty_matrix() + return {'RUNNING_MODAL'} + + +class BendAxiSwitchGizmoGroup(GizmoGroup, GizmoGroupUtils): + bl_idname = 'OBJECT_GGT_SimpleDeformGizmoGroup_display_bend_axis_switch_gizmo' + bl_label = 'SimpleDeformGizmoGroup_display_bend_axis_switch_gizmo' + + @classmethod + def poll(cls, context): + return cls.poll_simple_deform_show_bend_axis_witch(context) + + def setup(self, context): + _draw_type = 'SimpleDeform_Bend_Direction_' + _color_a = 1, 0, 0 + _color_b = 0, 1, 0 + + for na, axis, rot, positive in ( + ('top_a', 'X', (math.radians(90), 0, math.radians(90)), True), + ('top_b', 'X', (math.radians(90), 0, 0), True), + + ('bottom_a', 'X', (math.radians(90), 0, math.radians(90)), False), + ('bottom_b', 'X', (math.radians(90), 0, 0), False), + + ('left_a', 'Y', (math.radians(90), 0, 0), False), + ('left_b', 'Y', (0, 0, 0), False), + + ('right_a', 'Y', (math.radians(90), 0, 0), True), + ('right_b', 'Y', (0, 0, 0), True), + + ('front_a', 'Z', (0, 0, 0), False), + ('front_b', 'X', (0, 0, 0), False), + + ('back_a', 'Z', (0, 0, 0), True), + ('back_b', 'X', (0, 0, 0), True),): + _a = (na.split('_')[1] == 'a') + setattr(self, na, self.gizmos.new(CustomGizmo.bl_idname)) + gizmo = getattr(self, na) + gizmo.mode = na + gizmo.draw_type = _draw_type + gizmo.color = _color_a if _a else _color_b + gizmo.alpha = 0.3 + gizmo.color_highlight = 1.0, 1.0, 1.0 + gizmo.alpha_highlight = 1 + gizmo.use_draw_modal = True + gizmo.scale_basis = 0.2 + gizmo.use_draw_value = True + ops = gizmo.target_set_operator( + 'simple_deform_gizmo.deform_axis') + ops.Deform_Axis = axis + ops.X_Value = rot[0] + ops.Y_Value = rot[1] + ops.Z_Value = rot[2] + ops.Is_Positive = positive + + def draw_prepare(self, context): + ob = context.object + mat = ob.matrix_world + top, bottom, left, right, front, back = self.modifier_bound_box_pos + + rad = math.radians + for_list = ( + ('top_a', top, (0, 0, 0),), + ('top_b', top, (0, 0, rad(90)),), + + ('bottom_a', bottom, (0, rad(180), 0),), + ('bottom_b', bottom, (0, rad(180), rad(90)),), + + ('left_a', left, (rad(-90), 0, rad(90)),), + ('left_b', left, (0, rad(-90), 0),), + + ('right_a', right, (rad(90), 0, rad(90)),), + ('right_b', right, (0, rad(90), 0),), + + ('front_a', front, (rad(90), 0, 0),), + ('front_b', front, (rad(90), rad(90), 0),), + + ('back_a', back, (rad(-90), 0, 0),), + ('back_b', back, (rad(-90), rad(-90), 0),), + ) + for i, j, w, in for_list: + gizmo = getattr(self, i, False) + rot = Euler(w, 'XYZ').to_matrix().to_4x4() + gizmo.matrix_basis = mat.to_euler().to_matrix().to_4x4() @ rot + gizmo.matrix_basis.translation = self.obj_matrix_world @ Vector(j) diff --git a/simple_deform_helper/gizmo/set_deform_axis.py b/simple_deform_helper/gizmo/set_deform_axis.py new file mode 100644 index 000000000..34671b0b8 --- /dev/null +++ b/simple_deform_helper/gizmo/set_deform_axis.py @@ -0,0 +1,51 @@ +from bpy.types import GizmoGroup +from mathutils import Vector + +from ..utils import GizmoGroupUtils + + +class SetDeformGizmoGroup(GizmoGroup, GizmoGroupUtils): + bl_idname = 'OBJECT_GGT_SetDeformGizmoGroup' + bl_label = 'SetDeformGizmoGroup' + + @classmethod + def poll(cls, context): + return cls.simple_deform_show_gizmo_poll(context) and cls.pref_().show_set_axis_button + + def setup(self, context): + data_path = 'object.modifiers.active.deform_axis' + set_enum = 'wm.context_set_enum' + + for axis in ('X', 'Y', 'Z'): + # show toggle axis button + gizmo = self.gizmos.new('GIZMO_GT_button_2d') + gizmo.icon = f'EVENT_{axis.upper()}' + gizmo.draw_options = {'BACKDROP', 'HELPLINE'} + ops = gizmo.target_set_operator(set_enum) + ops.data_path = data_path + ops.value = axis + gizmo.color = (0, 0, 0) + gizmo.alpha = 0.3 + gizmo.color_highlight = 1.0, 1.0, 1.0 + gizmo.alpha_highlight = 0.3 + gizmo.use_draw_modal = True + gizmo.use_draw_value = True + gizmo.scale_basis = 0.1 + setattr(self, f'deform_axis_{axis.lower()}', gizmo) + + def draw_prepare(self, context): + bound = self.modifier_bound_co + if bound: + obj = self.get_depsgraph(self.obj) + dimensions = obj.dimensions + + def mat(f): + b = bound[0] + co = (b[0] + (max(dimensions) * f), + b[1], + b[2] - (min(dimensions) * 0.3)) + return self.obj_matrix_world @ Vector(co) + + self.deform_axis_x.matrix_basis.translation = mat(0) + self.deform_axis_y.matrix_basis.translation = mat(0.3) + self.deform_axis_z.matrix_basis.translation = mat(0.6) diff --git a/simple_deform_helper/gizmo/up_down_limits_point.py b/simple_deform_helper/gizmo/up_down_limits_point.py new file mode 100644 index 000000000..eec8a813a --- /dev/null +++ b/simple_deform_helper/gizmo/up_down_limits_point.py @@ -0,0 +1,274 @@ +import math +from time import time + +import bpy +from bpy.types import Gizmo, GizmoGroup +from bpy_extras import view3d_utils +from mathutils import Vector + +from ..update import ChangeActiveModifierParameter +from ..utils import GizmoUtils, GizmoGroupUtils + + +class GizmoProperty(GizmoUtils): + ctrl_mode: str + int_value_up_limits: int + int_value_down_limits: int + + @property + def is_up_limits_mode(self): + return self.ctrl_mode == 'up_limits' + + @property + def is_down_limits_mode(self): + return self.ctrl_mode == 'down_limits' + + @property + def limit_scope(self): + return self.pref.modifiers_limits_tolerance + + @property + def limits_min_value(self): + return self.modifier_down_limits + self.limit_scope + + @property + def limits_max_value(self): + return self.modifier_up_limits - self.limit_scope + + # ----get func + + def get_up_limits_value(self, event): + delta = self.get_delta(event) + mid = self.middle_limits_value + self.limit_scope + min_value = mid if self.is_middle_mode else self.limits_min_value + return self.value_limit(delta, min_value=min_value) + + def get_down_limits_value(self, event): + delta = self.get_delta(event) + mid = self.middle_limits_value - self.limit_scope + max_value = mid if self.is_middle_mode else self.limits_max_value + return self.value_limit(delta, max_value=max_value) + + def get_delta(self, event): + context = bpy.context + x, y = view3d_utils.location_3d_to_region_2d( + context.region, context.space_data.region_3d, self.point_up) + x2, y2 = view3d_utils.location_3d_to_region_2d( + context.region, context.space_data.region_3d, self.point_down) + + mouse_line_distance = math.sqrt(((event.mouse_region_x - x2) ** 2) + + ((event.mouse_region_y - y2) ** 2)) + straight_line_distance = math.sqrt(((x2 - x) ** 2) + + ((y2 - y) ** 2)) + delta = mouse_line_distance / straight_line_distance + 0 + + v_up = Vector((x, y)) + v_down = Vector((x2, y2)) + limits_angle = v_up - v_down + + mouse_v = Vector((event.mouse_region_x, event.mouse_region_y)) + + mouse_angle = mouse_v - v_down + angle_ = mouse_angle.angle(limits_angle) + if angle_ > (math.pi / 2): + delta = 0 + return delta + + +class GizmoUpdate(GizmoProperty): + # ---update gizmo matrix + def update_gizmo_matrix(self, context): + self.align_orientation_to_user_perspective(context) + self.align_point_to_limits_point() + + def align_orientation_to_user_perspective(self, context): + rotation = context.space_data.region_3d.view_matrix.inverted().to_quaternion() + matrix = rotation.to_matrix().to_4x4() + self.matrix_basis = matrix + + def align_point_to_limits_point(self): + if self.is_up_limits_mode: + self.matrix_basis.translation = self.point_limits_up + elif self.is_down_limits_mode: + self.matrix_basis.translation = self.point_limits_down + + # ---- set prop + def set_prop_value(self, event): + if self.is_up_limits_mode: + self.set_up_value(event) + elif self.is_down_limits_mode: + self.set_down_value(event) + + def set_down_value(self, event): + value = self.get_down_limits_value(event) + self.target_set_value('down_limits', value) + if event.ctrl: + self.target_set_value('up_limits', value + self.difference_value) + elif self.is_middle_mode: + if self.origin_mode == 'LIMITS_MIDDLE': + mu = self.middle_limits_value + v = mu - (value - mu) + self.target_set_value('up_limits', v) + elif self.origin_mode == 'MIDDLE': + self.target_set_value('up_limits', 1 - value) + else: + self.target_set_value('up_limits', self.modifier_up_limits) + else: + self.target_set_value('up_limits', self.modifier_up_limits) + + def set_up_value(self, event): + value = self.get_up_limits_value(event) + self.target_set_value('up_limits', value) + if event.ctrl: + self.target_set_value('down_limits', value - self.difference_value) + elif self.is_middle_mode: + if self.origin_mode == 'LIMITS_MIDDLE': + mu = self.middle_limits_value + value = mu - (value - mu) + self.target_set_value('down_limits', value) + elif self.origin_mode == 'MIDDLE': + self.target_set_value('down_limits', 1 - value) + else: + self.target_set_value('down_limits', self.modifier_down_limits) + else: + self.target_set_value('down_limits', self.modifier_down_limits) + + # ------- + def update_header_text(self, context): + origin = self.obj_origin_property_group + mode = origin.bl_rna.properties['origin_mode'].enum_items[origin.origin_mode].name + + te = self.translate_text + t = self.translate_header_text + text = te(self.modifier.deform_method.title()) + ' ' + te(mode) + ' ' + if self.is_up_limits_mode: + value = round(self.modifier_up_limits, 3) + text += t('Up limit', value) + elif self.is_down_limits_mode: + value = round(self.modifier_down_limits, 3) + text += t('Down limit', value) + context.area.header_text_set(text) + + +class UpDownLimitsGizmo(Gizmo, GizmoUpdate): + bl_idname = 'UpDownLimitsGizmo' + bl_label = 'UpDownLimitsGizmo' + bl_target_properties = ( + {'id': 'up_limits', 'type': 'FLOAT', 'array_length': 1}, + {'id': 'down_limits', 'type': 'FLOAT', 'array_length': 1}, + ) + bl_options = {'UNDO', 'GRAB_CURSOR'} + + __slots__ = ( + 'mod', + 'up_limits', + 'down_limits', + 'draw_type', + 'mouse_dpi', + 'ctrl_mode', + 'difference_value', + 'middle_limits_value', + 'init_mouse_region_y', + 'init_mouse_region_x', + 'custom_shape', + 'int_value_up_limits', + 'int_value_down_limits', + ) + difference_value: float + middle_limits_value: float + + def setup(self): + self.mouse_dpi = 10 + self.init_setup() + + def invoke(self, context, event): + self.init_invoke(context, event) + + if self.is_up_limits_mode: + self.int_value_up_limits = up_limits = self.modifier_up_limits + self.target_set_value('up_limits', up_limits) + elif self.is_down_limits_mode: + self.int_value_down_limits = down_limits = self.modifier_down_limits + self.target_set_value('down_limits', down_limits) + return {'RUNNING_MODAL'} + + def exit(self, context, cancel): + context.area.header_text_set(None) + if cancel: + if self.is_up_limits_mode: + self.target_set_value('up_limits', self.int_value_up_limits) + elif self.is_down_limits_mode: + self.target_set_value( + 'down_limits', self.int_value_down_limits) + + def modal(self, context, event, tweak): + st = time() + self.clear_point_cache() + + if self.modifier_is_use_origin_axis: + self.new_origin_empty_object() + # return {'RUNNING_MODAL'} + + self.difference_value = self.modifier_up_limits - self.modifier_down_limits + self.middle_limits_value = (self.modifier_up_limits + self.modifier_down_limits) / 2 + + try: + self.set_prop_value(event) + self.clear_point_cache() + self.update_object_origin_matrix() + except Exception as e: + print(e.args) + # ... + # return {'FINISHED'} + self.update_header_text(context) + return_handle = self.event_handle(event) + ChangeActiveModifierParameter.update_modifier_parameter() + self.update_deform_wireframe() + print('run modal time:', time() - st) + return return_handle + + +class UpDownLimitsGizmoGroup(GizmoGroup, GizmoGroupUtils): + bl_idname = 'OBJECT_GGT_UpDownLimitsGizmoGroup' + bl_label = 'UpDownLimitsGizmoGroup' + + @classmethod + def poll(cls, context): + + return cls.simple_deform_show_gizmo_poll(context) + + def setup(self, context): + sd_name = UpDownLimitsGizmo.bl_idname + gizmo_data = [ + ('up_limits', + sd_name, + {'ctrl_mode': 'up_limits', + 'draw_type': 'Sphere_GizmoGroup_', + 'mouse_dpi': 1000, + 'color': (1.0, 0, 0), + 'alpha': 0.5, + 'color_highlight': (1.0, 1.0, 1.0), + 'alpha_highlight': 0.3, + 'use_draw_modal': True, + 'scale_basis': 0.1, + 'use_draw_value': True, }), + ('down_limits', + sd_name, + {'ctrl_mode': 'down_limits', + 'draw_type': 'Sphere_GizmoGroup_', + 'mouse_dpi': 1000, + 'color': (0, 1.0, 0), + 'alpha': 0.5, + 'color_highlight': (1.0, 1.0, 1.0), + 'alpha_highlight': 0.3, + 'use_draw_modal': True, + 'scale_basis': 0.1, + 'use_draw_value': True, }), + ] + self.generate_gizmo(gizmo_data) + + def refresh(self, context): + pro = context.object.SimpleDeformGizmo_PropertyGroup + for i in (self.down_limits, self.up_limits): + for j in ('down_limits', 'up_limits'): + i.target_set_prop(j, pro, j) diff --git a/simple_deform_helper/operators.py b/simple_deform_helper/operators.py new file mode 100644 index 000000000..0601f9882 --- /dev/null +++ b/simple_deform_helper/operators.py @@ -0,0 +1,64 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +import bpy +from bpy.types import Operator +from bpy.props import FloatProperty, StringProperty, BoolProperty + +from .utils import GizmoUtils + + +class DeformAxisOperator(Operator, GizmoUtils): + bl_idname = 'simple_deform_gizmo.deform_axis' + bl_label = 'deform_axis' + bl_description = 'deform_axis operator' + bl_options = {'REGISTER'} + + Deform_Axis: StringProperty(default='X', options={'SKIP_SAVE'}) + + X_Value: FloatProperty(default=-0, options={'SKIP_SAVE'}) + Y_Value: FloatProperty(default=-0, options={'SKIP_SAVE'}) + Z_Value: FloatProperty(default=-0, options={'SKIP_SAVE'}) + + Is_Positive: BoolProperty(default=True, options={'SKIP_SAVE'}) + + def invoke(self, context, event): + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def modal(self, context, event): + self.clear_point_cache() + mod = context.object.modifiers.active + mod.deform_axis = self.Deform_Axis + empty = self.new_origin_empty_object() + is_positive = self.is_positive(mod.angle) + + for limit, value in (('max_x', self.X_Value), + ('min_x', self.X_Value), + ('max_y', self.Y_Value), + ('min_y', self.Y_Value), + ('max_z', self.Z_Value), + ('min_z', self.Z_Value), + ): + setattr(empty.constraints[self.G_NAME_CON_LIMIT], limit, value) + + if ((not is_positive) and self.Is_Positive) or (is_positive and (not self.Is_Positive)): + mod.angle = mod.angle * -1 + + if not event.ctrl: + self.pref.display_bend_axis_switch_gizmo = False + return {'FINISHED'} + + +class_list = ( + DeformAxisOperator, +) + +register_class, unregister_class = bpy.utils.register_classes_factory(class_list) + + +def register(): + register_class() + + +def unregister(): + unregister_class() diff --git a/simple_deform_helper/panel.py b/simple_deform_helper/panel.py new file mode 100644 index 000000000..e45e244c4 --- /dev/null +++ b/simple_deform_helper/panel.py @@ -0,0 +1,77 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +import bpy +from bpy.types import Panel, VIEW3D_HT_tool_header + +from .utils import GizmoUtils + + +class SimpleDeformHelperToolPanel(Panel, GizmoUtils): + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'Tool' + bl_context = '.objectmode' + bl_label = 'Simple Deform Helper' + bl_idname = 'VIEW3D_PT_simple_deform_helper' + bl_parent_id = 'VIEW3D_PT_tools_object_options' + + @classmethod + def poll(cls, context): + show_in_tool_options = GizmoUtils.pref_().show_gizmo_property_location == 'ToolOptions' + return cls.poll_simple_deform_public(context) and show_in_tool_options + + def draw(self, context): + if self.poll(context): + self.draw_property(self.layout, context) + + @staticmethod + def draw_property(layout, context): + if GizmoUtils.poll_simple_deform_public(context): + cls = SimpleDeformHelperToolPanel + pref = cls.pref_() + + obj = context.object + mod = obj.modifiers.active + prop = obj.SimpleDeformGizmo_PropertyGroup + + ctrl_obj = mod.origin.SimpleDeformGizmo_PropertyGroup if mod.origin else prop + + layout.prop(ctrl_obj, + 'origin_mode', + text='') + layout.prop(pref, + 'update_deform_wireframe', + icon='MOD_WIREFRAME', + text='') + layout.prop(pref, + 'show_set_axis_button', + icon='EMPTY_AXIS', + text='') + if pref.modifier_deform_method_is_bend: + layout.prop(pref, + 'display_bend_axis_switch_gizmo', + toggle=1) + layout.prop(pref, + 'modifiers_limits_tolerance', + text='') + + def draw_settings(self, context): + show_in_settings = GizmoUtils.pref_().show_gizmo_property_location == 'ToolSettings' + if show_in_settings: + SimpleDeformHelperToolPanel.draw_property(self.layout, context) + + +class_list = ( + SimpleDeformHelperToolPanel, +) + +register_class, unregister_class = bpy.utils.register_classes_factory(class_list) + + +def register(): + register_class() + VIEW3D_HT_tool_header.append(SimpleDeformHelperToolPanel.draw_settings) + + +def unregister(): + unregister_class() + VIEW3D_HT_tool_header.remove(SimpleDeformHelperToolPanel.draw_settings) diff --git a/simple_deform_helper/preferences.py b/simple_deform_helper/preferences.py new file mode 100644 index 000000000..5870ac675 --- /dev/null +++ b/simple_deform_helper/preferences.py @@ -0,0 +1,176 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +import bpy +from bpy.props import (FloatProperty, + PointerProperty, + FloatVectorProperty, + EnumProperty, + BoolProperty) +from bpy.types import ( + AddonPreferences, + PropertyGroup, +) + +from .utils import GizmoUtils + + +class SimpleDeformGizmoAddonPreferences(AddonPreferences, GizmoUtils): + bl_idname = GizmoUtils.G_ADDON_NAME + + deform_wireframe_color: FloatVectorProperty( + name='Deform Wireframe', + description='Draw Deform Wireframe Color', + default=(1, 1, 1, 0.3), + soft_max=1, + soft_min=0, + size=4, subtype='COLOR') + bound_box_color: FloatVectorProperty( + name='Bound Box', + description='Draw Bound Box Color', + default=(1, 0, 0, 0.5), + soft_max=1, + soft_min=0, + size=4, + subtype='COLOR') + limits_bound_box_color: FloatVectorProperty( + name='Upper and lower limit Bound Box Color', + description='Draw Upper and lower limit Bound Box Color', + default=(0.3, 1, 0.2, 0.5), + soft_max=1, + soft_min=0, + size=4, + subtype='COLOR') + modifiers_limits_tolerance: FloatProperty( + name='Upper and lower limit tolerance', + description='Minimum value between upper and lower limits', + default=0.05, + max=1, + min=0.0001 + ) + display_bend_axis_switch_gizmo: BoolProperty( + name='Show Toggle Bend Axis Gizmo', + default=False, + options={'SKIP_SAVE'}) + + update_deform_wireframe: BoolProperty( + name='Show Deform Wireframe', + default=False) + + show_set_axis_button: BoolProperty( + name='Show Set Axis Button', + default=False) + + show_gizmo_property_location: EnumProperty( + name='Gizmo Property Show Location', + items=[('ToolSettings', 'Tool Settings', ''), + ('ToolOptions', 'Tool Options', ''), + ], + default='ToolSettings' + ) + + def draw(self, context): + col = self.layout.column() + box = col.box() + for text in ("You can press the following shortcut keys when dragging values", + " Wheel: Switch Origin Ctrl Mode", + " X,Y,Z: Switch Modifier Deform Axis", + " W: Switch Deform Wireframe Show", + " A: Switch To Select Bend Axis Mode(deform_method=='BEND')",): + box.label(text=self.translate_text(text)) + + col.prop(self, 'deform_wireframe_color') + col.prop(self, 'bound_box_color') + col.prop(self, 'limits_bound_box_color') + + col.label(text='Gizmo Property Show Location') + col.prop(self, 'show_gizmo_property_location', expand=True) + + def draw_header_tool_settings(self, context): + if GizmoUtils.poll_simple_deform_public(context): + row = self.layout.row() + obj = context.object + mod = obj.modifiers.active + + row.separator(factor=0.2) + row.prop(mod, + 'deform_method', + expand=True) + row.prop(mod, + 'deform_axis', + expand=True) + + show_type = 'angle' if mod.deform_method in ('BEND', 'TWIST') else 'factor' + row.prop(mod, show_type) + + +class SimpleDeformGizmoObjectPropertyGroup(PropertyGroup, GizmoUtils): + def _limits_up(self, context): + if self.active_modifier_is_simple_deform: + self.modifier.limits[1] = self.up_limits + + up_limits: FloatProperty(name='up', + description='UP Limits(Red)', + default=1, + update=_limits_up, + max=1, + min=0) + + def _limits_down(self, context): + if self.active_modifier_is_simple_deform: + self.modifier.limits[0] = self.down_limits + + down_limits: FloatProperty(name='down', + description='Lower limit(Green)', + default=0, + update=_limits_down, + max=1, + min=0) + + origin_mode_items = ( + ('UP_LIMITS', + 'Follow Upper Limit(Red)', + 'Add an empty object origin as the rotation axis (if there is an origin, do not add it), and set the origin ' + 'position as the upper limit during operation'), + ('DOWN_LIMITS', + 'Follow Lower Limit(Green)', + 'Add an empty object origin as the rotation axis (if there is an origin, do not add it), and set the origin ' + 'position as the lower limit during operation'), + ('LIMITS_MIDDLE', + 'Middle', + 'Add an empty object origin as the rotation axis (if there is an origin, do not add it), and set the ' + 'origin position between the upper and lower limits during operation'), + ('MIDDLE', + 'Bound Middle', + 'Add an empty object origin as the rotation axis (if there is an origin, do not add it), and set the origin ' + 'position as the position between the bounding boxes during operation'), + ('NOT', 'No origin operation', ''), + ) + + origin_mode: EnumProperty(name='Origin control mode', + default='NOT', + items=origin_mode_items) + + +class_list = ( + SimpleDeformGizmoAddonPreferences, + SimpleDeformGizmoObjectPropertyGroup, +) + +register_class, unregister_class = bpy.utils.register_classes_factory(class_list) + + +def register(): + register_class() + + GizmoUtils.pref_().display_bend_axis_switch_gizmo = False + bpy.types.Object.SimpleDeformGizmo_PropertyGroup = PointerProperty( + type=SimpleDeformGizmoObjectPropertyGroup, + name='SimpleDeformGizmo_PropertyGroup') + bpy.types.VIEW3D_MT_editor_menus.append( + SimpleDeformGizmoAddonPreferences.draw_header_tool_settings) + + +def unregister(): + unregister_class() + bpy.types.VIEW3D_MT_editor_menus.remove( + SimpleDeformGizmoAddonPreferences.draw_header_tool_settings) diff --git a/simple_deform_helper/translate.py b/simple_deform_helper/translate.py new file mode 100644 index 000000000..1ba80e92b --- /dev/null +++ b/simple_deform_helper/translate.py @@ -0,0 +1,73 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +import bpy + + +def origin_text(a, b): + return "Add an empty object origin as the rotation axis (if there is an origin, " + a + \ + "), and set the origin position " + b + " during operation" + + +translations_dict = { + "zh_CN": { + ("上下文", "原文"): "翻译文字", + ("*", "Show Toggle Bend Axis Gizmo"): "显示切换弯曲轴向Gizmo", + ("*", "Gizmo Property Show Location"): "Gizmo属性显示位置", + ("*", "You can press the following shortcut keys when dragging values"): + "拖动值时可以按以下快捷键", + ("*", " Wheel: Switch Origin Ctrl Mode"): + " 滚轮: 切换原点控制模式", + ("*", " X,Y,Z: Switch Modifier Deform Axis"): + " X,Y,Z: 切换修改器型变轴", + ("*", " W: Switch Deform Wireframe Show"): + " W: 切换形变线框显示", + ("*", + " A: Switch To Select Bend Axis Mode(deform_method=='BEND')"): + " A: 切换到选择弯曲轴模式(形变方法='弯曲')", + ("*", "Show Set Axis Button"): "显示设置轴向Gizmo", + ("*", "Follow Upper Limit(Red)"): "跟随上限(红色)", + ("*", "Follow Lower Limit(Green)"): "跟随下限(绿色)", + ("*", "Lower limit(Green)"): "下限(绿色)", + ("*", "UP Limits(Red)"): "上限(红色)", + ("*", "Down limit"): "下限", + ("*", "Up limit"): "上限", + ("*", "Show Deform Wireframe"): "显示形变线框", + ("*", "Minimum value between upper and lower limits"): "上限与下限之间的最小值", + ("*", "Upper and lower limit tolerance"): "上下限容差", + ("*", "Draw Upper and lower limit Bound Box Color"): "绘制网格上限下限边界线框的颜色", + ("*", "Upper and lower limit Bound Box Color"): "上限下限边界框颜色", + ("*", "Draw Bound Box Color"): "绘制网格边界框的颜色", + ("*", "Bound Box"): "边界框颜色", + ("*", "Draw Deform Wireframe Color"): "绘制网格形变形状线框的颜色", + ("*", "Deform Wireframe"): "形变线框颜色", + ("*", "Simple Deform visualization adjustment tool"): "简易形变可视化工具", + ("*", "Select an object and the active modifier is Simple Deform"): "选择物体并且活动修改器为简易形变", + ("*", "Bound Middle"): "边界框中心", + ("*", origin_text("do not add it", "as the lower limit")): + "添加一个空物体原点作为旋转轴(如果已有原点则不添加),并在操作时设置原点位置为下限位置", + ("*", origin_text("do not add it", "as the upper limit")): + "添加一个空物体原点作为旋转轴(如果已有原点则不添加),并在操作时设置原点位置为上限位置", + ("*", origin_text("it will not be added", "between the upper and lower limits")): + "添加一个空物体原点作为旋转轴(如果已有原点则不添加),并在操作时设置原点位置为上下限之间的位置", + ("*", origin_text("do not add it", "as the position between the bounding boxes")): + "添加一个空物体原点作为旋转轴(如果已有原点则不添加),并在操作时设置原点位置为边界框之间的位置", + ("*", "No origin operation"): "不进行原点操作", + ("*", "Origin control mode"): "原点控制模式", + ("*", "Down limit"): "下限", + ("*", "Coefficient"): "系数", + ("*", "Upper limit"): "上限", + ("*", "3D View -> Select an object and the active modifier is simple deformation"): "3D视图 -> 选择一个物体," + "并且活动修改器为简易形修改器", + ("*", "3D View: Simple Deform Helper"): "3D 视图: Simple Deform Helper 简易形变助手", + ("*", "Simple Deform Helper"): "简易形变助手", + ("*", ""): "", + } +} + + +def register(): + bpy.app.translations.register(__name__, translations_dict) + + +def unregister(): + bpy.app.translations.unregister(__name__) diff --git a/simple_deform_helper/update.py b/simple_deform_helper/update.py new file mode 100644 index 000000000..19418878f --- /dev/null +++ b/simple_deform_helper/update.py @@ -0,0 +1,201 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +from functools import cache + +import bpy + +from .utils import GizmoUpdate + +gizmo = GizmoUpdate() + +"""depsgraph_update_post cannot listen to users modifying modifier parameters +Use timers to watch and use cache +""" + + +class update_public: + _events_func_list = {} + run_time = 0.2 + + @classmethod + def timers_update_poll(cls) -> bool: + return True + + @classmethod + @cache + def update_poll(cls) -> bool: + return True + + @classmethod + def _update_func_call_timer(cls): + if cls.timers_update_poll(): + for c, func_list in cls._events_func_list.items(): + if func_list and c.update_poll(): + for func in func_list: + func() + cls.clear_cache_events() + return cls.run_time + + @classmethod + def clear_cache_events(cls): + for cl in cls._events_func_list.keys(): + if getattr(cl, 'clear_cache', False): + cl.clear_cache() + + @classmethod + def clear_cache(cls): + cls.update_poll.cache_clear() + + @classmethod + def append(cls, item): + if cls not in cls._events_func_list: + cls._events_func_list[cls] = [] + cls._events_func_list[cls].append(item) + + @classmethod + def remove(cls, item): + if item in cls._events_func_list[cls]: + cls._events_func_list[cls].remove(item) + + # --------------- reg and unreg + @classmethod + def register(cls): + from bpy.app import timers + func = cls._update_func_call_timer + if not timers.is_registered(func): + timers.register(func, persistent=True) + else: + print('cls timers is registered', cls) + + @classmethod + def unregister(cls): + from bpy.app import timers + func = cls._update_func_call_timer + if timers.is_registered(func): + timers.unregister(func) + else: + print('cls timers is not registered', cls) + cls._events_func_list.clear() + + +class simple_update(update_public, GizmoUpdate): + tmp_save_data = {} + + @classmethod + def timers_update_poll(cls): + obj = bpy.context.object + if not cls.poll_context_mode_is_object(): + ... + elif not obj: + ... + elif not cls.obj_type_is_mesh_or_lattice(obj): + ... + elif cls.mod_is_simple_deform_type(obj.modifiers.active): + return True + return False + + +class ChangeActiveObject(simple_update): + @classmethod + @cache + def update_poll(cls): + return cls.is_change_active_object() + + @classmethod + def is_change_active_object(cls, change_data=True): + import bpy + obj = bpy.context.object + name = obj.name + key = 'active_object' + if key not in cls.tmp_save_data: + if change_data: + cls.tmp_save_data[key] = name + return True + + elif cls.tmp_save_data[key] != name: + if change_data: + cls.tmp_save_data[key] = name + return True + return False + + +class ChangeActiveSimpleDeformModifier(simple_update): + + @classmethod + @cache + def update_poll(cls): + return cls.is_change_active_simple_deform() + + @classmethod + def is_change_active_simple_deform(cls) -> bool: + import bpy + obj = bpy.context.object + modifiers = cls.get_modifiers_data(obj) + + def update(): + cls.tmp_save_data['modifiers'] = modifiers + + if ChangeActiveObject.update_poll(): + update() + elif 'modifiers' not in cls.tmp_save_data: + update() + elif cls.tmp_save_data['modifiers'] != modifiers: + update() + return True + return False + + @classmethod + def get_modifiers_data(cls, obj): + return {'obj': obj.name, + 'active_modifier': getattr(obj.modifiers.active, 'name', None), + 'modifiers': list(i.name for i in obj.modifiers)} + + +class ChangeActiveModifierParameter(simple_update): + key = 'active_modifier_parameter' + + @classmethod + @cache + def update_poll(cls): + return gizmo.active_modifier_is_simple_deform and cls.is_change_active_simple_parameter() + + @classmethod + def update_modifier_parameter(cls, modifier_parameter=None): + """Run this function when the gizmo is updated to avoid duplicate updates + """ + if not modifier_parameter: + modifier_parameter = cls.get_modifiers_parameter(gizmo.modifier) + cls.tmp_save_data[cls.key] = modifier_parameter + + @classmethod + def change_modifier_parameter(cls) -> bool: + mod_data = cls.get_modifiers_parameter(gizmo.modifier) + return cls.key in cls.tmp_save_data and cls.tmp_save_data[cls.key] == mod_data + + @classmethod + def is_change_active_simple_parameter(cls): + parameter = cls.get_modifiers_parameter(gizmo.modifier) + if ChangeActiveObject.update_poll(): + cls.update_modifier_parameter(parameter) + elif ChangeActiveSimpleDeformModifier.update_poll(): + cls.update_modifier_parameter(parameter) + elif cls.key not in cls.tmp_save_data: + cls.update_modifier_parameter(parameter) + elif cls.tmp_save_data[cls.key] != parameter: + cls.update_modifier_parameter(parameter) + return True + return False + + +def register(): + simple_update.register() + + def p(): + gizmo.update_multiple_modifiers_data() + + ChangeActiveObject.append(p) + ChangeActiveModifierParameter.append(p) + ChangeActiveSimpleDeformModifier.append(p) + + +def unregister(): + simple_update.unregister() diff --git a/simple_deform_helper/utils.py b/simple_deform_helper/utils.py new file mode 100644 index 000000000..0b64a115c --- /dev/null +++ b/simple_deform_helper/utils.py @@ -0,0 +1,964 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +import math +import uuid +from functools import cache +from os.path import dirname, basename, realpath + +import bpy +import numpy as np +from bpy.types import AddonPreferences +from mathutils import Vector, Matrix, Euler + + +class PublicData: + """Public data class, where all fixed data will be placed +Classify each different type of data separately and cache it to avoid getting stuck due to excessive update frequency + """ + G_CustomShape = {} # + + G_DeformDrawData = {} # Save Deform Vertex And Indices,Update data only when updating deformation boxes + + G_MultipleModifiersBoundData = {} + + G_INDICES = ( + (0, 1), (0, 2), (1, 3), (2, 3), + (4, 5), (4, 6), (5, 7), (6, 7), + (0, 4), (1, 5), (2, 6), (3, 7)) # The order in which the 8 points of the bounding box are drawn + G_NAME = 'ViewSimpleDeformGizmo_' # Temporary use files prefix + + G_DEFORM_MESH_NAME = G_NAME + 'DeformMesh' + G_TMP_MULTIPLE_MODIFIERS_MESH = 'TMP_' + G_NAME + 'MultipleModifiersMesh' + G_SUB_LEVELS = 7 + + G_NAME_EMPTY_AXIS = G_NAME + '_Empty_' + G_NAME_CON_LIMIT = G_NAME + 'ConstraintsLimitRotation' # constraints name + G_NAME_CON_COPY_ROTATION = G_NAME + 'ConstraintsCopyRotation' + G_ADDON_NAME = basename(dirname(realpath(__file__))) # "simple_deform_helper" + + G_MODIFIERS_PROPERTY = [ # Copy modifier data + 'angle', + 'deform_axis', + 'deform_method', + 'factor', + 'invert_vertex_group', + 'limits', # bpy.types.bpy_prop_array + 'lock_x', + 'lock_y', + 'lock_z', + 'origin', + # 'show_expanded', + # 'show_in_editmode', + 'vertex_group', + ] + + @classmethod + def load_gizmo_data(cls) -> None: + import json + import os + json_path = os.path.join(os.path.dirname(__file__), "gizmo.json") + with open(json_path, "r") as file: + cls.G_CustomShape = json.load(file) + + @staticmethod + def from_mesh_get_triangle_face_co(mesh: 'bpy.types.Mesh') -> list: + """ + :param mesh: input mesh read vertex + :type mesh: bpy.data.meshes + :return list: vertex coordinate list[[cox,coy,coz],[cox,coy,coz]...] + """ + import bmesh + bm = bmesh.new() + bm.from_mesh(mesh) + bm.faces.ensure_lookup_table() + bm.verts.ensure_lookup_table() + bmesh.ops.triangulate(bm, faces=bm.faces) + co_list = [list(float(format(j, ".3f")) for j in vert.co) for face in bm.faces for vert in face.verts] + bm.free() + return co_list + + @classmethod + def from_selected_obj_generate_json(cls): + """Export selected object vertex data as gizmo custom paint data + The output file should be in the blender folder + gizmo.json + """ + import json + data = {} + for obj in bpy.context.selected_objects: + data[obj.name] = cls.from_mesh_get_triangle_face_co(obj.data) + print(data) + with open('gizmo.json', 'w+') as f: + f.write(json.dumps(data)) + + +class PublicClass(PublicData): + @staticmethod + def pref_() -> "AddonPreferences": + return bpy.context.preferences.addons[PublicData.G_ADDON_NAME].preferences + + @property + def pref(self=None) -> 'AddonPreferences': + """ + :return: AddonPreferences + """ + return PublicClass.pref_() + + +class PublicPoll(PublicClass): + @classmethod + def poll_context_mode_is_object(cls) -> bool: + return bpy.context.mode == 'OBJECT' + + @classmethod + def poll_modifier_type_is_simple(cls, context): + """ + Active Object in ('MESH', 'LATTICE') + Active Modifier Type Is 'SIMPLE_DEFORM' and show_viewport + :param context:bpy.types.Object + :return: + """ + + obj = context.object + if not obj: + return False + mod = obj.modifiers.active + if not mod: + return False + + available_obj_type = cls.obj_type_is_mesh_or_lattice(obj) + is_available_obj = cls.mod_is_simple_deform_type(mod) and available_obj_type + is_obj_mode = cls.poll_context_mode_is_object() + show_mod = mod.show_viewport + not_is_self_mesh = obj.name != cls.G_NAME + return is_available_obj and is_obj_mode and show_mod and not_is_self_mesh + + @classmethod + def poll_object_is_show(cls, context: 'bpy.types.Context') -> bool: + """ + hava active object and object is show + :param context: + :return: + """ + obj = context.object + return obj and (not obj.hide_viewport) and (not obj.hide_get()) + + @classmethod + def poll_simple_deform_public(cls, context: 'bpy.types.context') -> bool: + """Public poll + In 3D View + return True + """ + space = context.space_data + if not space: + return False + show_gizmo = space.show_gizmo if space.type == 'VIEW_3D' else True + is_simple = cls.poll_modifier_type_is_simple(context) + is_show = cls.poll_object_is_show(context) + return is_simple and show_gizmo and is_show + + @classmethod + def poll_simple_deform_modifier_is_bend(cls, context): + """ + Public poll + active modifier deform_method =='BEND' + """ + simple = cls.poll_simple_deform_public(context) + is_bend = simple and (context.object.modifiers.active.deform_method == 'BEND') + return simple and is_bend + + @classmethod + def poll_simple_deform_show_bend_axis_witch(cls, context): + """ + Show D + """ + switch_axis = cls.pref_().display_bend_axis_switch_gizmo + bend = cls.poll_simple_deform_modifier_is_bend(context) + return switch_axis and bend + + @classmethod + def simple_deform_show_gizmo_poll(cls, context): + poll = cls.poll_simple_deform_public(context) + not_switch = (not cls.poll_simple_deform_show_bend_axis_witch(context)) + return poll and not_switch + + +class PublicTranslate(PublicPoll): + @classmethod + def translate_text(cls, text): + return bpy.app.translations.pgettext(text) + + @classmethod + def translate_header_text(cls, mode, value): + return cls.translate_text(mode) + ':{}'.format(value) + + +class GizmoClassMethod(PublicTranslate): + + @classmethod + def get_depsgraph(cls, obj: 'bpy.types.Object'): + """ + @param obj: dep obj + @return: If there is no input obj, reverse the active object evaluated + """ + context = bpy.context + if obj is None: + obj = context.object + dep = context.evaluated_depsgraph_get() + return obj.evaluated_get(dep) + + @classmethod + def get_vector_axis(cls, mod): + axis = mod.deform_axis + if 'BEND' == mod.deform_method: + vector_axis = Vector((0, 0, 1)) if axis in ( + 'Y', 'X') else Vector((1, 0, 0)) + else: + vector = (Vector((1, 0, 0)) if ( + axis == 'X') else Vector((0, 1, 0))) + vector_axis = Vector((0, 0, 1)) if ( + axis == 'Z') else vector + return vector_axis + + @classmethod + def get_modifiers_parameter(cls, modifier): + prop = bpy.types.bpy_prop_array + return list( + getattr(modifier, i)[:] if type(getattr(modifier, i)) == prop else getattr(modifier, i) + for i in cls.G_MODIFIERS_PROPERTY + ) + + @classmethod + def value_limit(cls, value, max_value=1, min_value=0): + """ + @param value: limit value + @param max_value: Maximum allowed + @param min_value: Minimum allowed + @return: If the input value is greater than the maximum value or less than the minimum value + it will be limited to the maximum or minimum value + """ + if value > max_value: + return max_value + elif value < min_value: + return min_value + else: + return value + + @classmethod + def is_positive(cls, number: 'int') -> bool: + """return bool value + if number is positive return True else return False + """ + return number == abs(number) + + @classmethod + def _link_obj(cls, obj, link): + context = bpy.context + objects = context.view_layer.active_layer_collection.collection.objects + if obj.name not in objects: + if link: + objects.link( + obj) + else: + objects.unlink( + obj) + + @classmethod + def link_obj_to_active_collection(cls, obj: 'bpy.types.Object'): + cls._link_obj(obj, True) + + @classmethod + def unlink_obj_to_active_collection(cls, obj: 'bpy.types.Object'): + cls._link_obj(obj, False) + + @classmethod + def get_mesh_max_min_co(cls, obj: 'bpy.context.object') -> '[Vector,Vector]': + if obj.type == 'MESH': + ver_len = obj.data.vertices.__len__() + list_vertices = np.zeros(ver_len * 3, dtype=np.float32) + obj.data.vertices.foreach_get('co', list_vertices) + list_vertices = list_vertices.reshape(ver_len, 3) + elif obj.type == 'LATTICE': + ver_len = obj.data.points.__len__() + list_vertices = np.zeros(ver_len * 3, dtype=np.float32) + obj.data.points.foreach_get('co_deform', list_vertices) + list_vertices = list_vertices.reshape(ver_len, 3) + else: + list_vertices = np.zeros((3, 3), dtype=np.float32) + return Vector(list_vertices.min(axis=0)).freeze(), Vector(list_vertices.max(axis=0)).freeze() + + @classmethod + def matrix_calculation(cls, mat: 'Matrix', calculation_list: 'list') -> list: + return [mat @ Vector(i) for i in calculation_list] + + @classmethod + def point_to_angle(cls, i, j, f, axis_): + if i == j: + if f == 0: + i[0] += 0.1 + j[0] -= 0.1 + elif f == 1: + i[1] -= 0.1 + j[1] += 0.1 + else: + i[2] -= 0.1 + j[2] += 0.1 + vector_value = i - j + angle = (180 * vector_value.angle(axis_) / math.pi) + return angle + + @classmethod + def co_to_direction(cls, mat, data): + (min_x, min_y, min_z), (max_x, max_y, + max_z) = data + a = mat @ Vector((max_x, max_y, max_z)) + b = mat @ Vector((max_x, min_y, min_z)) + c = mat @ Vector((min_x, max_y, min_z)) + d = mat @ Vector((min_x, min_y, max_z)) + point_list = ((a, d), + (c, b), + (c, d), + (a, b), + (d, b), + (c, a),) + + return list((aa + bb) / 2 for (aa, bb) in point_list) + + @classmethod + def tow_co_to_coordinate(cls, data): + ((min_x, min_y, min_z), (max_x, max_y, max_z)) = data + return ( + Vector((max_x, min_y, min_z)), + Vector((min_x, min_y, min_z)), + Vector((max_x, max_y, min_z)), + Vector((min_x, max_y, min_z)), + Vector((max_x, min_y, max_z)), + Vector((min_x, min_y, max_z)), + Vector((max_x, max_y, max_z)), + Vector((min_x, max_y, max_z)) + ) + + @classmethod + def mod_is_simple_deform_type(cls, mod): + return mod and mod.type == 'SIMPLE_DEFORM' + + @classmethod + def obj_type_is_mesh_or_lattice(cls, obj: 'bpy.types.Object'): + return obj and (obj.type in ('MESH', 'LATTICE')) + + @classmethod + def from_vertices_new_mesh(cls, name, vertices): + new_mesh = bpy.data.meshes.new(name) + new_mesh.from_pydata(vertices, cls.G_INDICES, []) + new_mesh.update() + return new_mesh + + @classmethod + def copy_modifier_parameter(cls, old_mod, new_mod): + for prop_name in cls.G_MODIFIERS_PROPERTY: + origin_value = getattr(old_mod, prop_name, None) + is_array_prop = type(origin_value) == bpy.types.bpy_prop_array + value = origin_value[:] if is_array_prop else origin_value + setattr(new_mod, prop_name, value) + + +class PublicProperty(GizmoClassMethod): + + def __from_up_down_point_get_limits_point(self, up_point, down_point): + + def ex(a): + return down_point + ((up_point - down_point) * Vector((a, a, a))) + + up_limits = ex(self.modifier_up_limits) + down_limits = ex(self.modifier_down_limits) + return up_limits, down_limits + + @cache + def _get_limits_point_and_bound_box_co(self): + top, bottom, left, right, front, back = self.modifier_bound_box_pos + mod = self.modifier + g_l = self.__from_up_down_point_get_limits_point + origin = self.modifier.origin + if origin: + vector_axis = self.get_vector_axis(mod) + matrix = self.modifier.origin.matrix_local + origin_mat = matrix.to_3x3() + axis = origin_mat @ vector_axis + point_lit = [[top, bottom], [left, right], [front, back]] + for f in range(point_lit.__len__()): + i = point_lit[f][0] + j = point_lit[f][1] + angle = self.point_to_angle(i, j, f, axis) + if abs(angle - 180) < 0.00001: + up_point, down_point = j, i + up_limits, down_limits = g_l(j, i) + point_lit[f][1], point_lit[f][0] = up_limits, down_limits + elif abs(angle) < 0.00001: + up_point, down_point = i, j + up_limits, down_limits = g_l(i, j) + point_lit[f][0], point_lit[f][1] = up_limits, down_limits + [[top, bottom], [left, right], [front, back]] = point_lit + else: + axis = self.modifier_deform_axis + if 'BEND' == self.modifier.deform_method: + if axis in ('X', 'Y'): + up_point, down_point = top, bottom + top, bottom = up_limits, down_limits = g_l(top, bottom) + elif axis == 'Z': + up_point, down_point = right, left + right, left = up_limits, down_limits = g_l(right, left) + else: + if axis == 'X': + up_point, down_point = right, left + right, left = up_limits, down_limits = g_l(right, left) + elif axis == 'Y': + up_point, down_point = back, front + back, front = up_limits, down_limits = g_l(back, front) + + elif axis == 'Z': + up_point, down_point = top, bottom + top, bottom = up_limits, down_limits = g_l(top, bottom) + + points = (up_point, down_point, up_limits, down_limits) + each_point = ((right[0], back[1], top[2]), (left[0], front[1], bottom[2],)) + return points, self.tow_co_to_coordinate(each_point) + + # ---------------------- + @cache + def _each_face_pos(self, mat, co): + return self.co_to_direction(mat, co) + + @classmethod + def clear_cache(cls): + cls.clear_point_cache() + cls.clear_modifiers_data() + + @classmethod + def clear_point_cache(cls): + cls._get_limits_point_and_bound_box_co.cache_clear() + + @classmethod + def clear_modifiers_data(cls): + cls.G_MultipleModifiersBoundData.clear() + + @classmethod + def clear_deform_data(cls): + cls.G_DeformDrawData.clear() + + # --------------- Cache Data ---------------------- + @property + def modifier_bound_co(self): + def get_bound_co_data(): + key = 'self.modifier.name' + if key not in self.G_MultipleModifiersBoundData: + self.G_MultipleModifiersBoundData[key] = self.get_mesh_max_min_co(self.obj) + return self.G_MultipleModifiersBoundData[key] + + return self.G_MultipleModifiersBoundData.get(self.modifier.name, get_bound_co_data()) + + @property + def modifier_bound_box_pos(self): + matrix = Matrix() + matrix.freeze() + return self.co_to_direction(matrix, self.modifier_bound_co) + + @property + def modifier_limits_point(self): + points, _ = self._get_limits_point_and_bound_box_co() + return self.matrix_calculation(self.obj_matrix_world, points) + + @property + def modifier_limits_bound_box(self): + _, bound = self._get_limits_point_and_bound_box_co() + return self.matrix_calculation(self.obj_matrix_world, bound) + + @property + def modifier_origin_is_available(self): + try: + self._get_limits_point_and_bound_box_co() + return True + except UnboundLocalError: + self.clear_point_cache() + return False + + # --------------- Compute Data ---------------------- + @property + def obj(self): + return bpy.context.object + + @property + def obj_matrix_world(self): + if self.obj: + mat = self.obj.matrix_world.copy() + mat.freeze() + return mat + mat = Matrix() + mat.freeze() + return mat + + @property + def modifier(self): + obj = self.obj + if not obj: + return + return obj.modifiers.active + + @property + def modifier_deform_axis(self): + mod = self.modifier + if mod: + return mod.deform_axis + + @property + def modifier_angle(self): + mod = self.modifier + if mod: + return mod.angle + + @property + def modifier_is_use_angle_value(self): + if self.active_modifier_is_simple_deform: + return self.modifier.deform_method in ('TWIST', 'BEND') + + @property + def modifier_deform_method_is_bend(self): + if self.active_modifier_is_simple_deform: + return self.modifier.deform_method == 'BEND' + + @property + def modifier_up_limits(self): + if self.modifier: + return self.modifier.limits[1] + + @property + def modifier_down_limits(self): + if self.modifier: + return self.modifier.limits[0] + + @property + def active_modifier_is_simple_deform(self): + return self.mod_is_simple_deform_type(self.modifier) + + # ----- point + @property + def point_up(self): + return self.modifier_limits_point[0] + + @property + def point_down(self): + return self.modifier_limits_point[1] + + @property + def point_limits_up(self): + return self.modifier_limits_point[2] + + @property + def point_limits_down(self): + return self.modifier_limits_point[3] + + # ------ + + @property + def obj_origin_property_group(self): + mod = self.modifier + if mod.origin: + return mod.origin.SimpleDeformGizmo_PropertyGroup + else: + return self.obj.SimpleDeformGizmo_PropertyGroup + + @property + def origin_mode(self): + return self.obj_origin_property_group.origin_mode + + @property + def is_limits_middle_mode(self): + return self.origin_mode == 'LIMITS_MIDDLE' + + @property + def is_middle_mode(self): + return self.origin_mode in ('LIMITS_MIDDLE', 'MIDDLE') + + @property + def modifier_is_use_origin_axis(self): + return self.obj_origin_property_group.origin_mode != 'NOT' + + @property + def modifier_is_have_origin(self): + return self.modifier_is_use_origin_axis and self.modifier.origin + + +class GizmoUpdate(PublicProperty): + def fix_origin_parent_and_angle(self): + obj = self.obj + mod = self.modifier + if not obj or not mod or not getattr(mod, 'origin', False): + return + + origin = mod.origin + if not origin: + return + + if origin.parent != obj: + origin.parent = obj + origin.rotation_euler.zero() + if not self.modifier_origin_is_available: + origin.location.zero() + origin.scale = 1, 1, 1 + + def new_origin_empty_object(self): + mod = self.modifier + obj = self.obj + origin = mod.origin + if not origin: + new_name = self.G_NAME_EMPTY_AXIS + str(uuid.uuid4()) + origin_object = bpy.data.objects.new(new_name, None) + self.link_obj_to_active_collection(origin_object) + origin_object.hide_set(True) + origin_object.empty_display_size = min(obj.dimensions) + mod.origin = origin_object + else: + origin_object = mod.origin + origin_object.hide_viewport = False + if origin_object == obj: + return + # add constraints + name = self.G_NAME_CON_LIMIT + if origin_object.constraints.keys().__len__() > 2: + origin_object.constraints.clear() + if name in origin_object.constraints.keys(): + limit_constraints = origin.constraints.get(name) + else: + limit_constraints = origin_object.constraints.new( + 'LIMIT_ROTATION') + limit_constraints.name = name + limit_constraints.owner_space = 'WORLD' + limit_constraints.space_object = obj + limit_constraints.use_transform_limit = True + limit_constraints.use_limit_x = True + limit_constraints.use_limit_y = True + limit_constraints.use_limit_z = True + con_copy_name = self.G_NAME_CON_COPY_ROTATION + if con_copy_name in origin_object.constraints.keys(): + copy_constraints = origin.constraints.get(con_copy_name) + else: + copy_constraints = origin_object.constraints.new( + 'COPY_ROTATION') + copy_constraints.name = con_copy_name + copy_constraints.target = obj + copy_constraints.mix_mode = 'BEFORE' + copy_constraints.target_space = 'WORLD' + copy_constraints.owner_space = 'WORLD' + origin_mode = self.obj.SimpleDeformGizmo_PropertyGroup.origin_mode + origin_object.SimpleDeformGizmo_PropertyGroup.origin_mode = origin_mode + self.fix_origin_parent_and_angle() + return origin_object + + def update_object_origin_matrix(self): + if self.modifier_is_have_origin: + origin_mode = self.origin_mode + origin_object = self.modifier.origin + if origin_mode == 'UP_LIMITS': + origin_object.matrix_world.translation = Vector(self.point_limits_up) + elif origin_mode == 'DOWN_LIMITS': + origin_object.matrix_world.translation = Vector(self.point_limits_down) + elif origin_mode == 'LIMITS_MIDDLE': + translation = (self.point_limits_up + self.point_limits_down) / 2 + origin_object.matrix_world.translation = translation + elif origin_mode == 'MIDDLE': + translation = (self.point_up + self.point_down) / 2 + origin_object.matrix_world.translation = translation + + def update_multiple_modifiers_data(self): + obj = self.obj + context = bpy.context + if not self.obj_type_is_mesh_or_lattice(obj) or not self.poll_modifier_type_is_simple(context): + return + self.clear_point_cache() + self.clear_modifiers_data() + data = bpy.data + name = self.G_TMP_MULTIPLE_MODIFIERS_MESH + + # del old tmp object + old_object = data.objects.get(name) + if old_object: + data.objects.remove(old_object) + + if data.meshes.get(name): + data.meshes.remove(data.meshes.get(name)) + + """get origin mesh bound box as multiple basic mesh + add multiple modifiers and get depsgraph obj bound box + """ + vertices = self.tow_co_to_coordinate(self.get_mesh_max_min_co(self.obj)) + new_mesh = self.from_vertices_new_mesh(name, vertices) + modifiers_obj = data.objects.new(name, new_mesh) + + self.link_obj_to_active_collection(modifiers_obj) + if modifiers_obj == obj: # is cycles + return + if modifiers_obj.parent != obj: + modifiers_obj.parent = obj + + modifiers_obj.modifiers.clear() + subdivision = modifiers_obj.modifiers.new('1', 'SUBSURF') + subdivision.levels = self.G_SUB_LEVELS + + for mod in context.object.modifiers: + if self.mod_is_simple_deform_type(mod): + dep_bound_tow_co = self.get_mesh_max_min_co(self.get_depsgraph(modifiers_obj)) + self.G_MultipleModifiersBoundData[mod.name] = dep_bound_tow_co + new_mod = modifiers_obj.modifiers.new(mod.name, 'SIMPLE_DEFORM') + self.copy_modifier_parameter(mod, new_mod) + data.objects.remove(modifiers_obj) + + def update_deform_wireframe(self): + if not self.pref.update_deform_wireframe: + return + # obj = self.obj + name = self.modifier.name + deform_name = self.G_DEFORM_MESH_NAME + + co = self.G_MultipleModifiersBoundData[name] + + deform_obj = bpy.data.objects.get(deform_name, None) + + if not deform_obj: + a, b = 0.5, -0.5 + vertices = self.tow_co_to_coordinate(((b, b, b), (a, a, a))) + new_mesh = self.from_vertices_new_mesh(name, vertices) + deform_obj = bpy.data.objects.new(deform_name, new_mesh) + deform_obj.hide_select = True + # deform_obj.hide_set(True) + deform_obj.hide_render = True + deform_obj.hide_viewport = True + + self.link_obj_to_active_collection(deform_obj) + + deform_obj.parent = self.obj + + tmv = deform_obj.hide_viewport + tmh = deform_obj.hide_get() + deform_obj.hide_viewport = False + deform_obj.hide_set(False) + + # Update Matrix + deform_obj.matrix_world = Matrix() + center = (co[0] + co[1]) / 2 + scale = co[1] - co[0] + deform_obj.matrix_world = self.obj_matrix_world @ deform_obj.matrix_world + deform_obj.location = center + deform_obj.scale = scale + + # Update Modifier data + mods = deform_obj.modifiers + mods.clear() + subdivision = mods.new('1', 'SUBSURF') + subdivision.levels = self.G_SUB_LEVELS + + new_mod = mods.new(name, 'SIMPLE_DEFORM') + self.copy_modifier_parameter(self.modifier, new_mod) + + # Get vertices data + context = bpy.context + obj = self.get_depsgraph(deform_obj) + matrix = deform_obj.matrix_world.copy() + ver_len = obj.data.vertices.__len__() + edge_len = obj.data.edges.__len__() + if 'numpy_data' not in self.G_DeformDrawData: + self.G_DeformDrawData['numpy_data'] = {} + numpy_data = self.G_DeformDrawData['numpy_data'] + key = (ver_len, edge_len) + if key in numpy_data: + list_edges, list_vertices = numpy_data[key] + else: + list_edges = np.zeros(edge_len * 2, dtype=np.int32) + list_vertices = np.zeros(ver_len * 3, dtype=np.float32) + numpy_data[key] = (list_edges, list_vertices) + obj.data.vertices.foreach_get('co', list_vertices) + ver = list_vertices.reshape((ver_len, 3)) + ver = np.insert(ver, 3, 1, axis=1).T + ver[:] = np.dot(matrix, ver) + + ver /= ver[3, :] + ver = ver.T + ver = ver[:, :3] + obj.data.edges.foreach_get('vertices', list_edges) + indices = list_edges.reshape((edge_len, 2)) + + modifiers = self.get_modifiers_parameter(self.modifier) + limits = context.object.modifiers.active.limits[:] + + deform_obj.hide_viewport = tmv + deform_obj.hide_set(tmh) + + self.G_DeformDrawData['simple_deform_bound_data'] = (ver, indices, self.obj_matrix_world, modifiers, limits[:]) + + +class GizmoUtils(GizmoUpdate): + custom_shape: dict + init_mouse_region_y: float + init_mouse_region_x: float + mouse_dpi: int + matrix_basis: Matrix + draw_type: str + + def generate_gizmo(self, gizmo_data): + """Generate Gizmo From Input Data + Args: + gizmo_data (_type_): _description_ + """ + for i, j, k in gizmo_data: + setattr(self, i, self.gizmos.new(j)) + gizmo = getattr(self, i) + for f in k: + if f == 'target_set_operator': + gizmo.target_set_operator(k[f]) + elif f == 'target_set_prop': + gizmo.target_set_prop(*k[f]) + else: + setattr(gizmo, f, k[f]) + + def init_shape(self): + if not hasattr(self, 'custom_shape'): + self.custom_shape = {} + for i in self.G_CustomShape: + item = self.G_CustomShape[i] + self.custom_shape[i] = self.new_custom_shape('TRIS', item) + + def init_setup(self): + self.init_shape() + + def init_invoke(self, context, event): + self.init_mouse_region_y = event.mouse_region_y + self.init_mouse_region_x = event.mouse_region_x + + def __update_matrix_func(self, context): + func = getattr(self, 'update_gizmo_matrix', None) + if func and self.modifier_origin_is_available: + func(context) + + def draw(self, context): + if self.modifier_origin_is_available: + self.draw_custom_shape(self.custom_shape[self.draw_type]) + self.__update_matrix_func(context) + + def draw_select(self, context, select_id): + if self.modifier_origin_is_available: + self.draw_custom_shape( + self.custom_shape[self.draw_type], select_id=select_id) + self.__update_matrix_func(context) + + def get_delta(self, event): + delta = (self.init_mouse_region_x - event.mouse_region_x) / self.mouse_dpi + return delta + + def update_gizmo_matrix(self): + ... + + def event_handle(self, event): + """General event triggering""" + data_path = ('object.SimpleDeformGizmo_PropertyGroup.origin_mode', + 'object.modifiers.active.origin.SimpleDeformGizmo_PropertyGroup.origin_mode') + + if event.type in ('WHEELUPMOUSE', 'WHEELDOWNMOUSE'): + reverse = (event.type == 'WHEELUPMOUSE') + for path in data_path: + bpy.ops.wm.context_cycle_enum( + data_path=path, reverse=reverse, wrap=True) + elif event.type in ('X', 'Y', 'Z'): + self.obj.modifiers.active.deform_axis = event.type + elif event.type == 'A' and 'BEND' == self.modifier.deform_method: + self.pref.display_bend_axis_switch_gizmo = True + return {'FINISHED'} + elif event.type == 'W' and event.value == 'RELEASE': + self.pref.update_deform_wireframe = self.pref.update_deform_wireframe ^ True + return {'RUNNING_MODAL'} + + @staticmethod + def tag_redraw(context): + if context.area: + context.area.tag_redraw() + + +class GizmoGroupUtils(GizmoUtils): + bl_space_type = 'VIEW_3D' + bl_region_type = 'WINDOW' + bl_options = {'3D', + 'PERSISTENT', + # 'SCALE', + # 'DEPTH_3D', + # 'SELECT', + # 'SHOW_MODAL_ALL', + # 'EXCLUDE_MODAL', + # 'TOOL_INIT', # not show + # 'TOOL_FALLBACK_KEYMAP', + # 'VR_REDRAWS' + } + + +class Tmp: + @classmethod + def get_origin_bounds(cls, obj: 'bpy.types.Object') -> list: + modifiers_dict = {} + for mod in obj.modifiers: + if (mod == obj.modifiers.active) or (modifiers_dict != {}): + modifiers_dict[mod] = (mod.show_render, mod.show_viewport) + mod.show_viewport = False + mod.show_render = False + matrix_obj = obj.matrix_world.copy() + obj.matrix_world.zero() + obj.scale = (1, 1, 1) + bound = cls.bound_box_to_list(obj) + obj.matrix_world = matrix_obj + for mod in modifiers_dict: + show_render, show_viewport = modifiers_dict[mod] + mod.show_render = show_render + mod.show_viewport = show_viewport + return list(bound) + + def update_gizmo_rotate(self): + mod = self.modifier + axis = self.modifier_deform_axis + if self.rotate_follow_modifier: + rot = Euler() + if axis == 'X' and (not self.is_positive(mod.angle)): + rot.z = math.pi + + elif axis == 'Y': + if self.is_positive(mod.angle): + rot.z = -(math.pi / 2) + else: + rot.z = math.pi / 2 + elif axis == 'Z': + if self.is_positive(mod.angle): + rot.x = rot.z = rot.y = math.pi / 2 + else: + rot.z = rot.y = math.pi / 2 + rot.x = -(math.pi / 2) + + rot = rot.to_matrix() + self.matrix_basis = self.matrix_basis @ rot.to_4x4() + + @classmethod + def bound_box_to_list(cls, obj: 'bpy.types.Object'): + return tuple(i[:] for i in obj.bound_box) + + @classmethod + def properties_is_modifier(cls) -> bool: + """Returns whether there is a modifier property panel open in the active window. + If it is open, it returns to True else False + """ + for area in bpy.context.screen.areas: + if area.type == 'PROPERTIES': + for space in area.spaces: + if space.type == 'PROPERTIES' and space.context == 'MODIFIER': + return True + return False + + +def register(): + PublicData.load_gizmo_data() + + +def unregister(): + ...