├── .gitignore ├── README.md ├── __init__.py ├── license ├── operators ├── align_between_two.py ├── align_origin_to_normal.py ├── assign_vertex_color.py ├── cursor_origin │ └── mesh.py ├── cursor_rotate.py ├── curve_spline_type.py ├── curve_subdivide.py ├── drag_snap.py ├── drag_snap_cursor.py ├── drag_snap_uv.py ├── easy_mod_array.py ├── easy_mod_curve.py ├── easy_mod_shwarp.py ├── executor.py ├── grid_from_active.py ├── hotkeys │ ├── load_hotkeys.py │ └── save_hotkeys.py ├── image_reload.py ├── iops.py ├── library_reload.py ├── materials_from_textures.py ├── maya_isolate.py ├── mesh_convert_selection.py ├── mesh_copy_edges_angle.py ├── mesh_copy_edges_length.py ├── mesh_cursor_bisect.py ├── mesh_quick_snap.py ├── mesh_to_grid.py ├── mesh_uv_channel_hop.py ├── modes.py ├── mouseover_fill_select.py ├── object_align_to_face.py ├── object_auto_smooth.py ├── object_drop_it.py ├── object_kitbash_grid.py ├── object_match_transform_active.py ├── object_name_from_active.py ├── object_normalize.py ├── object_replace.py ├── object_rotate.py ├── object_three_point_rotation.py ├── object_uvmaps_add_remove.py ├── object_uvmaps_cleaner.py ├── object_visual_origin.py ├── open_asset_in_current_blender.py ├── outliner_collection_ops.py ├── preferences │ └── io_addon_preferences.py ├── render_asset_thumbnail.py ├── run_text.py ├── save_load_space_data.py ├── snap_combos.py ├── split_screen_area.py ├── split_screen_area_new.py ├── ui_prop_switch.py └── z_ops.py ├── prefs ├── addon_preferences.py ├── addon_properties.py ├── hotkeys_default.py ├── iops_prefs.py └── iops_prefs_list.py ├── ui ├── iops_data_panel.py ├── iops_pie_edit.py ├── iops_pie_menu.py ├── iops_pie_split.py └── iops_tm_panel.py └── utils ├── draw_stats.py ├── functions.py ├── iops_dict.py └── split_areas_dict.py /.gitignore: -------------------------------------------------------------------------------- 1 | # User Hotkeys 2 | prefs/hotkeys_user.py 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # Installer logs 9 | pip-log.txt 10 | pip-delete-this-directory.txt 11 | 12 | # Bookmarks file and recents 13 | config/bookmarks.txt 14 | config/recent-files.txt 15 | 16 | # VSCode 17 | .vscode/* 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # InteractionOps or iOps 4 | It is a set of operators which boost Blender interactivity between user and application by using only five functional buttons. 5 | 6 | ## Documentation: 7 | https://interactionops-docs.readthedocs.io/en/latest/index.html 8 | 9 | ## Ideas and support: 10 | BlenderArtists forum page 11 | https://blenderartists.org/t/interactionops-iops/ 12 | 13 | ## Authors: 14 | Cyrill Vitkovskiy: 15 | https://www.artstation.com/furash 16 | 17 | Titus Lavrov: 18 | https://www.artstation.com/tituslvr 19 | Also check my page on gumroad: 20 | https://gumroad.com/titus 21 | 22 | ## Special thanks: 23 | For author of qBlocker 24 | https://gumroad.com/sanislovart 25 | 26 | jayanamgames youtube channel: 27 | https://www.youtube.com/user/jayanamgames 28 | 29 | Many thanks! 30 | 31 | 32 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017-2020 Tit Lavrov, Cyrill Vitkovskiy, Titus.mailbox@gmail.com 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /operators/align_between_two.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from bpy.props import ( 4 | BoolProperty, 5 | EnumProperty, 6 | IntProperty 7 | ) 8 | 9 | from ..utils.functions import get_active_and_selected, ShowMessageBox 10 | 11 | 12 | class IOPS_OT_Align_between_two(bpy.types.Operator): 13 | """Align active object between two selected objects. Works for two objects also.""" 14 | 15 | bl_idname = "iops.object_align_between_two" 16 | bl_label = "Align Between Two" 17 | bl_options = {"REGISTER", "UNDO"} 18 | 19 | track_axis: EnumProperty( 20 | name="Track", 21 | description="Track axis", 22 | items=[ 23 | ("X", "X", "", "", 0), 24 | ("Y", "Y", "", "", 1), 25 | ("Z", "Z", "", "", 2), 26 | ("-X", "-X", "", "", 3), 27 | ("-Y", "-Y", "", "", 4), 28 | ("-Z", "-Z", "", "", 5), 29 | ], 30 | default="Y", 31 | ) 32 | up_axis: EnumProperty( 33 | name="Up", 34 | description="Up axis", 35 | items=[ 36 | ("X", "X", "", "", 0), 37 | ("Y", "Y", "", "", 1), 38 | ("Z", "Z", "", "", 2), 39 | ], 40 | default="Z", 41 | ) 42 | align: BoolProperty( 43 | name="Align", 44 | description="Align Duplicates Between Selected Objects", 45 | default=False, 46 | ) 47 | 48 | count: IntProperty( 49 | name="Count", 50 | description="Number of Duplicates", 51 | default=1, 52 | soft_min=0, 53 | soft_max=100000000, 54 | ) 55 | 56 | select_duplicated: BoolProperty( 57 | name="Select Duplicated", 58 | description="Enabled = Select Duplicated Objects, Disabled = Keep Selection", 59 | default=True, 60 | ) 61 | 62 | def align_between(self): 63 | sequence = [] 64 | active, objects = get_active_and_selected() 65 | if len(bpy.context.selected_objects) == 2: 66 | axis = active.location - objects[-1].location 67 | 68 | A = active.location 69 | B = objects[-1].location 70 | 71 | for ip in range(self.count): 72 | p = 1 / (self.count + 1) * (ip + 1) 73 | point = (1 - p) * A + p * B 74 | sequence.append(point) 75 | elif len(bpy.context.selected_objects) == 3: 76 | posA, posB = [ob.location for ob in objects] 77 | axis = posA - posB 78 | 79 | for idx in range(len(objects) - 1): 80 | A = objects[idx].location 81 | B = objects[idx + 1].location 82 | 83 | for ip in range(self.count): 84 | p = 1 / (self.count + 1) * (ip + 1) 85 | point = (1 - p) * A + p * B 86 | sequence.append(point) 87 | else: 88 | ShowMessageBox("Must be 2 or 3 Objects Selected.") 89 | return 90 | 91 | collection = bpy.data.collections.new("Objects Between") 92 | bpy.context.scene.collection.children.link(collection) 93 | new_objects = [] 94 | for p in sequence: 95 | new_ob = active.copy() 96 | new_ob.data = active.data.copy() 97 | # position 98 | new_ob.location = p 99 | # rotation 100 | if self.align: 101 | new_ob.rotation_mode = "QUATERNION" 102 | new_ob.rotation_quaternion = axis.to_track_quat( 103 | self.track_axis, self.up_axis 104 | ) 105 | new_ob.rotation_mode = "XYZ" 106 | 107 | collection.objects.link(new_ob) 108 | new_ob.select_set(False) 109 | new_objects.append(new_ob) 110 | 111 | if self.select_duplicated: 112 | active.select_set(False) 113 | for ob in objects: 114 | ob.select_set(False) 115 | for ob in new_objects: 116 | ob.select_set(True) 117 | bpy.context.view_layer.objects.active = new_objects[-1] 118 | 119 | def execute(self, context): 120 | if self.track_axis != self.up_axis: 121 | self.align_between() 122 | self.report({"INFO"}, "Aligned!") 123 | else: 124 | self.report({"WARNING"}, "SAME AXIS") 125 | return {"FINISHED"} 126 | 127 | def draw(self, context): 128 | layout = self.layout 129 | col = layout.column(align=True) 130 | # 131 | col.prop(self, "mode") 132 | col.separator() 133 | col.prop(self, "track_axis") 134 | col.prop(self, "up_axis") 135 | col.separator() 136 | col.prop(self, "align") 137 | col.prop(self, "count") 138 | col.separator() 139 | col.prop(self, "select_duplicated") 140 | -------------------------------------------------------------------------------- /operators/align_origin_to_normal.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import ( 3 | FloatVectorProperty 4 | ) 5 | import bmesh 6 | from mathutils import Matrix 7 | 8 | 9 | class IOPS_OT_AlignOriginToNormal(bpy.types.Operator): 10 | """Align object to selected face""" 11 | 12 | bl_idname = "iops.mesh_align_origin_to_normal" 13 | bl_label = "MESH: Align origin to face normal" 14 | bl_options = {"REGISTER", "UNDO"} 15 | 16 | loc: FloatVectorProperty() 17 | orig_mx = [] 18 | 19 | @classmethod 20 | def poll(self, context): 21 | return ( 22 | context.area.type == "VIEW_3D" 23 | and context.mode == "EDIT_MESH" 24 | and len(context.view_layer.objects.selected) != 0 25 | and context.view_layer.objects.active.type == "MESH" 26 | ) 27 | 28 | def align_origin_to_normal(self): 29 | 30 | bpy.ops.view3d.snap_cursor_to_selected() 31 | bpy.ops.object.mode_set(mode="OBJECT") 32 | bpy.ops.object.origin_set(type="ORIGIN_CURSOR") 33 | bpy.ops.object.mode_set(mode="EDIT") 34 | 35 | obj = bpy.context.view_layer.objects.active 36 | mx = obj.matrix_world.copy() 37 | loc = mx.to_translation() # Store location 38 | scale = mx.to_scale() # Store scale 39 | polymesh = obj.data 40 | bm = bmesh.from_edit_mesh(polymesh) 41 | face = bm.faces.active 42 | 43 | # Return face tangent based on longest edge. 44 | tangent = face.calc_tangent_edge() 45 | 46 | # Build vectors for new matrix 47 | n = face.normal 48 | t = tangent 49 | c = n.cross(t) 50 | 51 | # Assemble new matrix 52 | mx_new = Matrix((t * -1.0, c * -1.0, n)).transposed().to_4x4() 53 | 54 | # New matrix rotation part 55 | new_rot = mx_new.to_euler() 56 | 57 | # Apply new matrix 58 | obj.matrix_world = mx_new.inverted() 59 | obj.location = loc 60 | obj.scale = scale 61 | 62 | bpy.ops.object.mode_set(mode="OBJECT") 63 | bpy.ops.object.transform_apply( 64 | location=False, rotation=True, scale=False, properties=False 65 | ) 66 | obj.rotation_euler = new_rot 67 | 68 | def execute(self, context): 69 | if context.object and context.area.type == "VIEW_3D": 70 | # Apply transform so it works multiple times 71 | bpy.ops.object.mode_set(mode="OBJECT") 72 | bpy.ops.object.transform_apply( 73 | location=False, rotation=True, scale=False, properties=False 74 | ) 75 | bpy.ops.object.mode_set(mode="EDIT") 76 | 77 | # Initialize axis and assign starting values for object's location 78 | self.axis_rotate = "Z" 79 | self.flip = True 80 | self.edge_idx = 0 81 | self.counter = 0 82 | self.align_origin_to_normal() 83 | 84 | return {"FINISHED"} 85 | 86 | else: 87 | self.report({"WARNING"}, "No active object, could not finish") 88 | return {"FINISHED"} 89 | -------------------------------------------------------------------------------- /operators/assign_vertex_color.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from bpy.props import FloatProperty, BoolProperty 4 | 5 | class IOPS_OT_VertexColorAssign(bpy.types.Operator): 6 | """Assign Vertex color in editr mode to selected vertecies""" 7 | 8 | bl_idname = "iops.mesh_assign_vertex_color" 9 | bl_label = "Assign Vertex color in editr mode to selected vertecies" 10 | bl_options = {"REGISTER", "UNDO"} 11 | 12 | use_active_color: BoolProperty( 13 | name="Use Active Color Attribute", 14 | default=True 15 | ) 16 | 17 | col_attr_name: bpy.props.StringProperty( 18 | name="Color Attribute Name", 19 | default="Color" 20 | ) 21 | fill_color_black: BoolProperty( 22 | name="Fill Black", 23 | #description="Fill selected vertecies with black color", 24 | default=False 25 | ) 26 | fill_color_white: BoolProperty( 27 | name="Fill White", 28 | #descritpion="Fill selected vertecies with white color", 29 | default=False 30 | ) 31 | fill_color_grey: BoolProperty( 32 | name="Fill Grey", 33 | #description="Fill selected vertecies with grey color", 34 | default=False 35 | ) 36 | 37 | domain: bpy.props.EnumProperty( 38 | name="Domain", 39 | description="Domain of the color attribute", 40 | items=[("POINT", "Point", "Point"), ("CORNER", "Corner", "Corner")], 41 | default="POINT" 42 | ) 43 | 44 | attr_type: bpy.props.EnumProperty( 45 | name="Attribute Type", 46 | description="Type of the color attribute", 47 | items=[("FLOAT_COLOR", "Float Color", "Float Color"), ("BYTE_COLOR", "Byte Color", "Byte Color")], 48 | default="FLOAT_COLOR" 49 | ) 50 | 51 | @classmethod 52 | def poll(cls, context): 53 | return context.object and context.object.type == "MESH" 54 | 55 | def execute(self, context): 56 | color_black = (0.0, 0.0, 0.0, 1.0) 57 | color_white = (1.0, 1.0, 1.0, 1.0) 58 | color_picker = context.scene.IOPS.iops_vertex_color 59 | 60 | color = color_picker 61 | 62 | if self.fill_color_black: 63 | self.fill_color_black = False 64 | color = color_black 65 | 66 | if self.fill_color_grey: 67 | self.fill_color_grey = False 68 | color = (0.5, 0.5, 0.5, 1.0) 69 | 70 | if self.fill_color_white: 71 | self.fill_color_white = False 72 | color = color_white 73 | 74 | sel = [obj for obj in context.selected_objects] 75 | # Create color attribute if not exists 76 | if not self.use_active_color: 77 | for obj in sel: 78 | if self.col_attr_name not in obj.data.color_attributes: 79 | obj.data.color_attributes.new( 80 | self.col_attr_name, self.attr_type, self.domain 81 | ) 82 | # Set color attribute as active 83 | attr_name = obj.data.color_attributes[self.col_attr_name] 84 | obj.data.color_attributes.active_color = attr_name 85 | else: 86 | try: 87 | attr_name = context.object.data.color_attributes.active_color.name 88 | except AttributeError: 89 | # Create new color attribute if none exists 90 | context.object.data.color_attributes.new( 91 | self.col_attr_name, self.attr_type, self.domain 92 | ) 93 | attr_name = self.col_attr_name 94 | context.object.data.color_attributes.active_color = context.object.data.color_attributes[attr_name] 95 | 96 | attr_type = context.object.data.color_attributes.active_color.data_type 97 | 98 | if sel and context.mode == "EDIT_MESH": 99 | # IF EDIT MODE 100 | attr_domain = context.object.data.color_attributes.active_color.domain # POINT or CORNER 101 | col_layer = None 102 | if attr_domain == "POINT": 103 | for obj in sel: 104 | bm = bmesh.new() 105 | bm = bmesh.from_edit_mesh(obj.data) 106 | if attr_type == "FLOAT_COLOR": 107 | col_layer = bm.verts.layers.float_color[attr_name] 108 | elif attr_type == "BYTE_COLOR": 109 | col_layer = bm.verts.layers.color[attr_name] 110 | verts = [v for v in bm.verts if v.select] 111 | if col_layer: 112 | for vert in verts: 113 | vert[col_layer] = color 114 | elif attr_domain == "CORNER": 115 | for obj in sel: 116 | bm = bmesh.new() 117 | bm = bmesh.from_edit_mesh(obj.data) 118 | if attr_type == "FLOAT_COLOR": 119 | col_layer = bm.loops.layers.float_color[attr_name] 120 | elif attr_type == "BYTE_COLOR": 121 | col_layer = bm.loops.layers.color[attr_name] 122 | if col_layer: 123 | for f in bm.faces: 124 | if any(v.select for v in f.verts): 125 | for loop in f.loops: 126 | loop[col_layer] = color 127 | 128 | bmesh.update_edit_mesh(context.object.data) 129 | bm.free() 130 | 131 | elif context.mode == "OBJECT": 132 | self.report( 133 | {"WARNING"}, "OBJECT MODE. Will be implemented soon. Crashing for now." 134 | ) 135 | # IF OBJECT MODE 136 | # # BOTH ARE CRASHING ON POST OPERATOR COLOR CHANGE 137 | # for obj in sel: 138 | # # NON-Bmesh way 139 | # for poly in obj.data.polygons: 140 | # for i in poly.loop_indices: 141 | # obj.data.color_attributes[self.col_attr_name].data[i].color = color 142 | 143 | # # Bmesh way 144 | # bm = bmesh.new() 145 | # bm.from_mesh(obj.data) 146 | # verts = [v for v in bm.verts] 147 | # col_layer = bm.verts.layers.float_color[self.col_attr_name] 148 | # for v in verts: 149 | # v[col_layer] = color 150 | 151 | # bm.to_mesh(obj.data) 152 | # bm.free() 153 | else: 154 | self.report({"WARNING"}, context.object.name + " is not a MESH.") 155 | 156 | return {"FINISHED"} 157 | 158 | def draw(self, context): 159 | layout = self.layout 160 | col = layout.column(align=True) 161 | col.prop( 162 | context.scene.IOPS, 163 | "iops_vertex_color", 164 | text="", 165 | ) 166 | # TODO: Add use active color 167 | # col.prop(self, "use_active_color", text="Use Active Color") 168 | col.prop(self, "col_attr_name", text="Color Attribute Name") 169 | col.prop(self, "attr_type", text="Attribute Type") 170 | col.prop(self, "domain", text="Domain") 171 | col.prop(self, "fill_color_black", text="Fill Black") 172 | col.prop(self, "fill_color_grey", text="Fill Grey") 173 | col.prop(self, "fill_color_white", text="Fill White") 174 | 175 | 176 | class IOPS_OT_VertexColorAlphaAssign(bpy.types.Operator): 177 | """Assign Vertex Color Alpha to selected vertecies""" 178 | 179 | bl_idname = "iops.mesh_assign_vertex_color_alpha" 180 | bl_label = "Assign Vertex Color Alpha to selected vertecies" 181 | bl_options = {"REGISTER", "UNDO"} 182 | 183 | vertex_color_alpha: FloatProperty( 184 | name="Alpha", 185 | description="Alpha channel value. 0 - Transparent, 1 - Solid", 186 | default=1.0, 187 | min=0.0, 188 | max=1.0, 189 | ) 190 | 191 | @classmethod 192 | def poll(cls, context): 193 | return ( 194 | context.object 195 | and context.object.type == "MESH" 196 | and context.object.mode == "EDIT" 197 | ) 198 | 199 | def execute(self, context): 200 | if context.object.mode == "EDIT": 201 | bpy.ops.object.editmode_toggle() 202 | 203 | mesh = bpy.context.active_object.data 204 | if mesh.vertex_colors[:] == []: 205 | mesh.vertex_colors.new() 206 | vertices = mesh.vertices 207 | vcol = mesh.vertex_colors.active 208 | 209 | for loop_index, loop in enumerate(mesh.loops): 210 | # If vertex selected 211 | if vertices[loop.vertex_index].select: 212 | vertex_color = vcol.data[loop_index].color 213 | vertex_color[3] = self.vertex_color_alpha 214 | mesh.update() 215 | bpy.ops.object.editmode_toggle() 216 | return {"FINISHED"} 217 | 218 | def draw(self, context): 219 | layout = self.layout 220 | row = layout.row() 221 | row.prop(self, "vertex_color_alpha", slider=True, text="Alpha value") 222 | -------------------------------------------------------------------------------- /operators/cursor_origin/mesh.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import blf 3 | import gpu 4 | from gpu_extras.batch import batch_for_shader 5 | from mathutils import Vector 6 | from ..iops import IOPS_OT_Main 7 | 8 | 9 | # ---------------------------- UI --------------------------------------- 10 | def draw_line_cursor(self, context): 11 | coords = self.gpu_verts 12 | shader = gpu.shader.from_builtin("UNIFORM_COLOR") 13 | batch = batch_for_shader(shader, "LINES", {"pos": coords}) 14 | shader.bind() 15 | shader.uniform_float("color", (0.1, 0.6, 0.4, 1)) 16 | batch.draw(shader) 17 | # pass 18 | 19 | 20 | def draw_ui(self, context, _uidpi, _uifactor): 21 | 22 | def get_target(): 23 | if self.target == context.scene.cursor: 24 | return "3D Cursor" 25 | elif self.target == context.view_layer.objects.active: 26 | return "Active object" 27 | 28 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences 29 | tColor = prefs.text_color 30 | tKColor = prefs.text_color_key 31 | tCSize = prefs.text_size 32 | tCPosX = prefs.text_pos_x 33 | tCPosY = prefs.text_pos_y 34 | tShadow = prefs.text_shadow_toggle 35 | tSColor = prefs.text_shadow_color 36 | tSBlur = prefs.text_shadow_blur 37 | tSPosX = prefs.text_shadow_pos_x 38 | tSPosY = prefs.text_shadow_pos_y 39 | 40 | _target = get_target() 41 | iops_text = ( 42 | ("Look at", str(_target)), 43 | ("Look at axis", str(self.look_axis[0])), 44 | ("Match cursor's rotation", str(self.rotate)), 45 | ("Align to cursor's pos", "F3"), 46 | ("Visual origin helper", "F4"), 47 | ) 48 | 49 | # FontID 50 | font = 0 51 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3]) 52 | blf.size(font, tCSize) 53 | if tShadow: 54 | blf.enable(font, blf.SHADOW) 55 | blf.shadow(font, int(tSBlur), tSColor[0], tSColor[1], tSColor[2], tSColor[3]) 56 | blf.shadow_offset(font, tSPosX, tSPosY) 57 | else: 58 | blf.disable(0, blf.SHADOW) 59 | 60 | textsize = tCSize 61 | # get leftbottom corner 62 | offset = tCPosY 63 | columnoffs = (textsize * 15) * _uifactor 64 | for line in reversed(iops_text): 65 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3]) 66 | blf.position(font, tCPosX * _uifactor, offset, 0) 67 | blf.draw(font, line[0]) 68 | 69 | blf.color(font, tKColor[0], tKColor[1], tKColor[2], tKColor[3]) 70 | textdim = blf.dimensions(0, line[1]) 71 | coloffset = columnoffs - textdim[0] + tCPosX 72 | blf.position(0, coloffset, offset, 0) 73 | blf.draw(font, line[1]) 74 | offset += (tCSize + 5) * _uifactor 75 | 76 | 77 | class IOPS_OT_CursorOrigin_Mesh(IOPS_OT_Main): 78 | bl_idname = "iops.cursor_origin_mesh" 79 | bl_label = "MESH: Object mode - Align to cursor" 80 | orig_mxs = [] 81 | rotate = False 82 | flip = False 83 | target = None 84 | look_axis = [] 85 | gpu_verts = [] 86 | 87 | @classmethod 88 | def poll(self, context): 89 | return ( 90 | context.area.type == "VIEW_3D" 91 | and context.mode == "OBJECT" 92 | and context.view_layer.objects.active.type in {"MESH", "LIGHT"} 93 | and context.view_layer.objects.selected[:] != [] 94 | ) 95 | 96 | def move_to_cursor(self, rotate): 97 | scene = bpy.context.scene 98 | objs = bpy.context.selected_objects 99 | for ob in objs: 100 | ob.location = scene.cursor.location 101 | if rotate: 102 | ob.rotation_euler = scene.cursor.rotation_euler 103 | 104 | def look_at(self, context, target, axis, flip): 105 | objs = bpy.context.selected_objects 106 | self.gpu_verts = [] 107 | 108 | for o in objs: 109 | # Reset matrix 110 | q = o.matrix_world.to_quaternion() 111 | m = q.to_matrix() 112 | m = m.to_4x4() 113 | o.matrix_world @= m.inverted() 114 | 115 | self.gpu_verts.append(o.location) 116 | self.gpu_verts.append(target.location) 117 | 118 | v = Vector(o.location - target.location) 119 | if flip: 120 | rot_mx = v.to_track_quat("-" + axis[0], axis[1]).to_matrix().to_4x4() 121 | else: 122 | rot_mx = v.to_track_quat(axis[0], axis[1]).to_matrix().to_4x4() 123 | o.matrix_world @= rot_mx 124 | 125 | def modal(self, context, event): 126 | context.area.tag_redraw() 127 | objs = context.selected_objects 128 | 129 | if event.type in {"MIDDLEMOUSE"}: 130 | # Allow navigation 131 | return {"PASS_THROUGH"} 132 | 133 | elif event.type == "F4" and event.value == "PRESS": 134 | bpy.ops.iops.object_visual_origin("INVOKE_DEFAULT") 135 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_cursor, "WINDOW") 136 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_ui, "WINDOW") 137 | return {"FINISHED"} 138 | 139 | elif event.type == "F1" and event.value == "PRESS": 140 | self.flip = not self.flip 141 | self.target = context.scene.cursor 142 | self.look_at(context, self.target, self.look_axis, self.flip) 143 | self.report({"INFO"}, event.type) 144 | 145 | elif event.type == "F2" and event.value == "PRESS": 146 | self.flip = not self.flip 147 | self.target = context.view_layer.objects.active 148 | self.look_at(context, self.target, self.look_axis, self.flip) 149 | self.report({"INFO"}, event.type) 150 | 151 | elif event.type == "X" and event.value == "PRESS": 152 | self.flip = not self.flip 153 | self.look_axis = [("X"), ("Z")] 154 | self.look_at(context, self.target, self.look_axis, self.flip) 155 | self.report({"INFO"}, event.type) 156 | 157 | elif event.type == "Y" and event.value == "PRESS": 158 | self.flip = not self.flip 159 | self.look_axis = [("Y"), ("X")] 160 | self.look_at(context, self.target, self.look_axis, self.flip) 161 | self.report({"INFO"}, event.type) 162 | 163 | elif event.type == "Z" and event.value == "PRESS": 164 | self.flip = not self.flip 165 | self.look_axis = [("Z"), ("Y")] 166 | self.look_at(context, self.target, self.look_axis, self.flip) 167 | self.report({"INFO"}, event.type) 168 | 169 | elif event.type == "F3" and event.value == "PRESS": 170 | for o, m in zip(objs, self.orig_mxs): 171 | o.matrix_world = m 172 | self.rotate = not self.rotate 173 | self.move_to_cursor(self.rotate) 174 | self.report({"INFO"}, event.type) 175 | 176 | elif event.type in {"LEFTMOUSE", "SPACE"} and event.value == "PRESS": 177 | # self.execute(context) 178 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_cursor, "WINDOW") 179 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_ui, "WINDOW") 180 | return {"FINISHED"} 181 | 182 | elif event.type in {"RIGHTMOUSE", "ESC"}: 183 | for o, m in zip(objs, self.orig_mxs): 184 | o.matrix_world = m 185 | # clean up 186 | self.orig_mxs = [] 187 | 188 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_cursor, "WINDOW") 189 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_ui, "WINDOW") 190 | return {"CANCELLED"} 191 | 192 | return {"RUNNING_MODAL"} 193 | 194 | def invoke(self, context, event): 195 | self.orig_mxs = [] 196 | self.gpu_verts = [] 197 | self.look_axis = [("Z"), ("Y")] 198 | self.target = context.scene.cursor 199 | objs = context.selected_objects 200 | preferences = context.preferences 201 | # Store matricies for undo 202 | for o in objs: 203 | self.orig_mxs.append(o.matrix_world.copy()) 204 | 205 | if context.object and context.area.type == "VIEW_3D": 206 | # Add drawing handler for text overlay rendering 207 | uidpi = int((72 * preferences.system.ui_scale)) 208 | args = (self, context, uidpi, preferences.system.ui_scale) 209 | self._handle_ui = bpy.types.SpaceView3D.draw_handler_add( 210 | draw_ui, args, "WINDOW", "POST_PIXEL" 211 | ) 212 | args_line = (self, context) 213 | self._handle_cursor = bpy.types.SpaceView3D.draw_handler_add( 214 | draw_line_cursor, args_line, "WINDOW", "POST_VIEW" 215 | ) 216 | 217 | # Add modal handler to enter modal mode 218 | context.window_manager.modal_handler_add(self) 219 | return {"RUNNING_MODAL"} 220 | else: 221 | self.report({"WARNING"}, "No active object, could not finish") 222 | return {"CANCELLED"} 223 | -------------------------------------------------------------------------------- /operators/cursor_rotate.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import BoolProperty, FloatProperty, EnumProperty 3 | import math 4 | import mathutils 5 | 6 | # Cursor Rotate Operator 7 | class IOPS_OT_Cursor_Rotate(bpy.types.Operator): 8 | """Rotate cursor around X or Y or Z axis""" 9 | bl_idname = "iops.cursor_rotate" 10 | bl_label = "Cursor Rotate" 11 | bl_options = {"REGISTER", "UNDO"} 12 | 13 | reverse: BoolProperty( 14 | name="reverse", description="Reverse rotation", default=False 15 | ) 16 | angle: FloatProperty( 17 | name="angle", description="Rotation angle", default=90 18 | ) 19 | rotation_axis: EnumProperty( 20 | name="Axis", 21 | description="Rotation axis", 22 | items=[ 23 | ("X", "X", "", "", 0), 24 | ("Y", "Y", "", "", 1), 25 | ("Z", "Z", "", "", 2), 26 | ], 27 | default="X", 28 | ) 29 | 30 | @classmethod 31 | def poll(cls, context): 32 | return context.area.type == "VIEW_3D" 33 | 34 | def execute(self, context): 35 | cursor = bpy.context.scene.cursor 36 | mx = cursor.matrix 37 | # rotate cursor using maxtrix 38 | match self.rotation_axis: 39 | case 'X': 40 | if self.reverse: 41 | cursor.matrix = mx @ mathutils.Matrix.Rotation(math.radians(-self.angle), 4, 'X') 42 | else: 43 | cursor.matrix = mx @ mathutils.Matrix.Rotation(math.radians(self.angle), 4, 'X') 44 | case 'Y': 45 | if self.reverse: 46 | cursor.matrix = mx @ mathutils.Matrix.Rotation(math.radians(-self.angle), 4, 'Y') 47 | else: 48 | cursor.matrix = mx @ mathutils.Matrix.Rotation(math.radians(self.angle), 4, 'Y') 49 | case 'Z': 50 | if self.reverse: 51 | cursor.matrix = mx @ mathutils.Matrix.Rotation(math.radians(-self.angle), 4, 'Z') 52 | else: 53 | cursor.matrix = mx @ mathutils.Matrix.Rotation(math.radians(self.angle), 4, 'Z') 54 | # Report 55 | self.report({'INFO'}, f'Cursor rotated {self.angle} around {self.rotation_axis} axis') 56 | return {'FINISHED'} 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /operators/curve_spline_type.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import blf 3 | from bpy.props import BoolProperty 4 | 5 | 6 | def draw_iops_curve_spline_types_text_px(self, context, _uidpi, _uifactor): 7 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences 8 | tColor = prefs.text_color 9 | tKColor = prefs.text_color_key 10 | tCSize = prefs.text_size 11 | tCPosX = prefs.text_pos_x 12 | tCPosY = prefs.text_pos_y 13 | tShadow = prefs.text_shadow_toggle 14 | tSColor = prefs.text_shadow_color 15 | tSBlur = prefs.text_shadow_blur 16 | tSPosX = prefs.text_shadow_pos_x 17 | tSPosY = prefs.text_shadow_pos_y 18 | 19 | iops_text = ( 20 | ("Present type is", str(self.curv_spline_type)), 21 | ("Handles state", str(self.handles)), 22 | ("Enable/Disable handles", "H"), 23 | ("Spline type POLY", "F1"), 24 | ("Spline type BEZIER", "F2"), 25 | ("Spline type NURBS", "F3"), 26 | ) 27 | 28 | # FontID 29 | font = 0 30 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3]) 31 | blf.size(font, tCSize) 32 | if tShadow: 33 | blf.enable(font, blf.SHADOW) 34 | blf.shadow(font, int(tSBlur), tSColor[0], tSColor[1], tSColor[2], tSColor[3]) 35 | blf.shadow_offset(font, tSPosX, tSPosY) 36 | else: 37 | blf.disable(0, blf.SHADOW) 38 | 39 | textsize = tCSize 40 | # get leftbottom corner 41 | offset = tCPosY 42 | columnoffs = (textsize * 13) * _uifactor 43 | for line in reversed(iops_text): 44 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3]) 45 | blf.position(font, tCPosX * _uifactor, offset, 0) 46 | blf.draw(font, line[0]) 47 | 48 | blf.color(font, tKColor[0], tKColor[1], tKColor[2], tKColor[3]) 49 | textdim = blf.dimensions(0, line[1]) 50 | coloffset = columnoffs - textdim[0] + tCPosX 51 | blf.position(0, coloffset, offset, 0) 52 | blf.draw(font, line[1]) 53 | offset += (tCSize + 5) * _uifactor 54 | 55 | 56 | class IOPS_OT_CurveSplineType(bpy.types.Operator): 57 | """Curve select spline type""" 58 | 59 | bl_idname = "iops.curve_spline_type" 60 | bl_label = "CURVE: Spline type" 61 | bl_options = {"REGISTER", "UNDO"} 62 | 63 | handles: BoolProperty(name="Use handles", description="Use handles", default=False) 64 | 65 | spl_type = [] 66 | curv_spline_type = [] 67 | 68 | @classmethod 69 | def poll(self, context): 70 | return ( 71 | len(context.view_layer.objects.selected) != 0 72 | and context.view_layer.objects.active.type == "CURVE" 73 | and context.view_layer.objects.active.mode == "EDIT" 74 | ) 75 | 76 | def get_curve_active_spline_type(self, context): 77 | curve = context.view_layer.objects.active.data 78 | active_spline_type = curve.splines.active.type 79 | return active_spline_type 80 | 81 | def execute(self, context): 82 | bpy.ops.curve.spline_type_set(type=self.spl_type, use_handles=self.handles) 83 | return {"FINISHED"} 84 | 85 | def modal(self, context, event): 86 | context.area.tag_redraw() 87 | 88 | if event.type in {"MIDDLEMOUSE", "WHEELDOWNMOUSE", "WHEELUPMOUSE"}: 89 | # Allow navigation 90 | return {"PASS_THROUGH"} 91 | 92 | elif event.type in {"F1"} and event.value == "PRESS": 93 | self.spl_type = "POLY" 94 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_text, "WINDOW") 95 | self.execute(context) 96 | return {"FINISHED"} 97 | 98 | elif event.type in {"F2"} and event.value == "PRESS": 99 | self.spl_type = "BEZIER" 100 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_text, "WINDOW") 101 | self.execute(context) 102 | return {"FINISHED"} 103 | 104 | elif event.type in {"F3"} and event.value == "PRESS": 105 | self.spl_type = "NURBS" 106 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_text, "WINDOW") 107 | self.execute(context) 108 | return {"FINISHED"} 109 | 110 | elif event.type in {"H"} and event.value == "PRESS": 111 | hnd = self.handles 112 | if hnd: 113 | self.handles = False 114 | else: 115 | self.handles = True 116 | 117 | elif event.type in {"RIGHTMOUSE", "ESC"}: 118 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_text, "WINDOW") 119 | return {"CANCELLED"} 120 | return {"RUNNING_MODAL"} 121 | 122 | def invoke(self, context, event): 123 | preferences = context.preferences 124 | if context.object and context.area.type == "VIEW_3D": 125 | self.handles = False 126 | self.spl_type = "POLY" 127 | self.curv_spline_type = self.get_curve_active_spline_type(context) 128 | # Add drawing handler for text overlay rendering 129 | uidpi = int((72 * preferences.system.ui_scale)) 130 | args = (self, context, uidpi, preferences.system.ui_scale) 131 | self._handle_text = bpy.types.SpaceView3D.draw_handler_add( 132 | draw_iops_curve_spline_types_text_px, args, "WINDOW", "POST_PIXEL" 133 | ) 134 | # Add modal handler to enter modal mode 135 | context.window_manager.modal_handler_add(self) 136 | return {"RUNNING_MODAL"} 137 | else: 138 | self.report({"WARNING"}, "No active object, could not finish") 139 | return {"CANCELLED"} 140 | 141 | 142 | def register(): 143 | bpy.utils.register_class(IOPS_OT_CurveSplineType) 144 | 145 | 146 | def unregister(): 147 | bpy.utils.unregister_class(IOPS_OT_CurveSplineType) 148 | 149 | 150 | if __name__ == "__main__": 151 | register() 152 | -------------------------------------------------------------------------------- /operators/curve_subdivide.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import gpu 3 | from gpu_extras.batch import batch_for_shader 4 | import blf 5 | from bpy.props import ( 6 | IntProperty 7 | ) 8 | 9 | def draw_curve_pts(self, context): 10 | coords = self.get_curve_pts() # <- sequence 11 | shader = gpu.shader.from_builtin("UNIFORM_COLOR") 12 | batch = batch_for_shader(shader, "POINTS", {"pos": coords}) 13 | shader.bind() 14 | shader.uniform_float("color", (1, 0, 0, 1)) 15 | batch.draw(shader) 16 | # pass 17 | 18 | 19 | def draw_ui(self, context, _uidpi, _uifactor): 20 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences 21 | tColor = prefs.text_color 22 | tKColor = prefs.text_color_key 23 | tCSize = prefs.text_size 24 | tCPosX = prefs.text_pos_x 25 | tCPosY = prefs.text_pos_y 26 | tShadow = prefs.text_shadow_toggle 27 | tSColor = prefs.text_shadow_color 28 | tSBlur = prefs.text_shadow_blur 29 | tSPosX = prefs.text_shadow_pos_x 30 | tSPosY = prefs.text_shadow_pos_y 31 | 32 | iops_text = (("Number of cuts", str(self.points_num)),) 33 | # FontID 34 | font = 0 35 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3]) 36 | blf.size(font, tCSize) 37 | if tShadow: 38 | blf.enable(font, blf.SHADOW) 39 | blf.shadow(font, int(tSBlur), tSColor[0], tSColor[1], tSColor[2], tSColor[3]) 40 | blf.shadow_offset(font, tSPosX, tSPosY) 41 | else: 42 | blf.disable(0, blf.SHADOW) 43 | 44 | textsize = tCSize 45 | # get leftbottom corner 46 | offset = tCPosY 47 | columnoffs = (textsize * 9) * _uifactor 48 | for line in reversed(iops_text): 49 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3]) 50 | blf.position(font, tCPosX * _uifactor, offset, 0) 51 | blf.draw(font, line[0]) 52 | 53 | blf.color(font, tKColor[0], tKColor[1], tKColor[2], tKColor[3]) 54 | textdim = blf.dimensions(0, line[1]) 55 | coloffset = columnoffs - textdim[0] + tCPosX 56 | blf.position(0, coloffset, offset, 0) 57 | blf.draw(font, line[1]) 58 | offset += (tCSize + 5) * _uifactor 59 | 60 | 61 | class IOPS_OT_CurveSubdivide(bpy.types.Operator): 62 | """Subdivide Curve""" 63 | 64 | bl_idname = "iops.curve_subdivide" 65 | bl_label = "CURVE: Subdivide" 66 | bl_options = {"REGISTER", "UNDO"} 67 | 68 | pairs = [] 69 | 70 | points_num: IntProperty(name="Number of cuts", description="", default=1) 71 | 72 | @classmethod 73 | def poll(self, context): 74 | return ( 75 | context.view_layer.objects.active.type == "CURVE" 76 | and context.view_layer.objects.active.mode == "EDIT" 77 | ) 78 | 79 | def execute(self, context): 80 | self.subdivide(self.points_num) 81 | return {"FINISHED"} 82 | 83 | def subdivide(self, points): 84 | self.points_num = points 85 | bpy.ops.curve.subdivide(number_cuts=self.points_num) 86 | 87 | def get_curve_pts(self): 88 | obj = bpy.context.view_layer.objects.active 89 | pts = [] 90 | sequence = [] 91 | 92 | # Store selected curve points 93 | if obj.type == "CURVE": 94 | for s in obj.data.splines: 95 | for b in s.bezier_points: 96 | if b.select_control_point: 97 | pts.append(b) 98 | 99 | # P = (0.5)**3 * A + 3(0.5)**3 * Ha + 3(0.5) ** 3 * Hb + 0.5**3 * B 100 | 101 | for idx in range(len(pts) - 1): 102 | A = pts[idx].co @ obj.matrix_world + obj.location 103 | # Ahl = pts[idx].handle_left @ obj.matrix_world + obj.location 104 | Ahr = pts[idx].handle_right @ obj.matrix_world + obj.location # Ha 105 | 106 | B = pts[idx + 1].co @ obj.matrix_world + obj.location 107 | Bhl = pts[idx + 1].handle_left @ obj.matrix_world + obj.location # Hb 108 | # Bhr = pts[idx + 1].handle_right @ obj.matrix_world + obj.location 109 | 110 | for ip in range(self.points_num): 111 | p = 1 / (self.points_num + 1) * (ip + 1) 112 | point = ( 113 | (1 - p) ** 3 * A 114 | + 3 * (1 - p) ** 2 * p * Ahr 115 | + 3 * (1 - p) * p**2 * Bhl 116 | + p**3 * B 117 | ) 118 | sequence.append(point) 119 | return sequence 120 | 121 | def modal(self, context, event): 122 | context.area.tag_redraw() 123 | 124 | if event.type in {"MIDDLEMOUSE"}: 125 | # Allow navigation 126 | return {"PASS_THROUGH"} 127 | 128 | elif event.type == "WHEELDOWNMOUSE": 129 | if self.points_num > 1: 130 | self.points_num -= 1 131 | self.report({"INFO"}, event.type) 132 | 133 | elif event.type == "WHEELUPMOUSE": 134 | self.points_num += 1 135 | self.report({"INFO"}, event.type) 136 | 137 | elif event.type in {"LEFTMOUSE", "SPACE"} and event.value == "PRESS": 138 | self.execute(context) 139 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_curve, "WINDOW") 140 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_ui, "WINDOW") 141 | return {"FINISHED"} 142 | 143 | elif event.type in {"RIGHTMOUSE", "ESC"}: 144 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_ui, "WINDOW") 145 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_curve, "WINDOW") 146 | return {"CANCELLED"} 147 | 148 | return {"RUNNING_MODAL"} 149 | 150 | def invoke(self, context, event): 151 | preferences = context.preferences 152 | if context.object and context.area.type == "VIEW_3D": 153 | self.points_num = 1 154 | 155 | # Add drawing handler for text overlay rendering 156 | uidpi = int((72 * preferences.system.ui_scale)) 157 | args = (self, context, uidpi, preferences.system.ui_scale) 158 | self._handle_ui = bpy.types.SpaceView3D.draw_handler_add( 159 | draw_ui, args, "WINDOW", "POST_PIXEL" 160 | ) 161 | args_line = (self, context) 162 | self._handle_curve = bpy.types.SpaceView3D.draw_handler_add( 163 | draw_curve_pts, args_line, "WINDOW", "POST_VIEW" 164 | ) 165 | self.pairs = self.get_curve_pts() 166 | # Add modal handler to enter modal mode 167 | context.window_manager.modal_handler_add(self) 168 | return {"RUNNING_MODAL"} 169 | else: 170 | self.report({"WARNING"}, "No active object, could not finish") 171 | return {"CANCELLED"} 172 | -------------------------------------------------------------------------------- /operators/drag_snap_cursor.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import blf 3 | 4 | 5 | def draw_iops_text(self, context, _uidpi, _uifactor): 6 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences 7 | tColor = prefs.text_color 8 | tKColor = prefs.text_color_key 9 | tCSize = prefs.text_size 10 | tCPosX = prefs.text_pos_x 11 | tCPosY = prefs.text_pos_y 12 | tShadow = prefs.text_shadow_toggle 13 | tSColor = prefs.text_shadow_color 14 | tSBlur = prefs.text_shadow_blur 15 | tSPosX = prefs.text_shadow_pos_x 16 | tSPosY = prefs.text_shadow_pos_y 17 | 18 | iops_text = ( 19 | ("Press and Hold", "Q"), 20 | ("Pick snapping points", "Left Mouse Button Click"), 21 | ) 22 | 23 | # FontID 24 | font = 0 25 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3]) 26 | blf.size(font, tCSize) 27 | if tShadow: 28 | blf.enable(font, blf.SHADOW) 29 | blf.shadow(font, int(tSBlur), tSColor[0], tSColor[1], tSColor[2], tSColor[3]) 30 | blf.shadow_offset(font, tSPosX, tSPosY) 31 | else: 32 | blf.disable(0, blf.SHADOW) 33 | 34 | textsize = tCSize 35 | # get leftbottom corner 36 | offset = tCPosY 37 | columnoffs = (textsize * 21) * _uifactor 38 | for line in reversed(iops_text): 39 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3]) 40 | blf.position(font, tCPosX * _uifactor, offset, 0) 41 | blf.draw(font, line[0]) 42 | 43 | blf.color(font, tKColor[0], tKColor[1], tKColor[2], tKColor[3]) 44 | textdim = blf.dimensions(0, line[1]) 45 | coloffset = columnoffs - textdim[0] + tCPosX 46 | blf.position(0, coloffset, offset, 0) 47 | blf.draw(font, line[1]) 48 | offset += (tCSize + 5) * _uifactor 49 | 50 | 51 | class IOPS_OT_DragSnapCursor(bpy.types.Operator): 52 | """Quick drag & snap using 3D Cursor""" 53 | 54 | bl_idname = "iops.object_drag_snap_cursor" 55 | bl_label = "IOPS Drag Snap Cursor" 56 | bl_description = ( 57 | "Hold Q and LMB Click to quickly snap point to point using 3D Cursor" 58 | ) 59 | bl_options = {"REGISTER", "UNDO"} 60 | 61 | step = 1 62 | count = 0 63 | old_type = None 64 | old_value = None 65 | 66 | def clear_draw_handlers(self): 67 | for handler in self.vp_handlers: 68 | bpy.types.SpaceView3D.draw_handler_remove(handler, "WINDOW") 69 | 70 | @classmethod 71 | def poll(cls, context): 72 | return ( 73 | context.area.type == "VIEW_3D" 74 | and context.mode == "OBJECT" 75 | and len(context.view_layer.objects.selected) != 0 76 | ) 77 | 78 | def modal(self, context, event): 79 | context.area.tag_redraw() 80 | # prevent spamming 81 | # new_type = event.type 82 | # new_value = event.value 83 | # if new_type != self.old_type and new_value != self.old_value: 84 | # print(event.type, event.value) 85 | # self.old_type = new_type 86 | # self.old_value = new_value 87 | 88 | if ( 89 | event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"} 90 | and event.value == "PRESS" 91 | ): 92 | return {"PASS_THROUGH"} 93 | elif event.type in {"ESC", "RIGHMOUSE"} and event.value == "PRESS": 94 | try: 95 | self.clear_draw_handlers() 96 | except ValueError: 97 | pass 98 | return {"CANCELLED"} 99 | 100 | elif event.type == "Q" and event.value == "PRESS": 101 | print("Count:", self.count) 102 | bpy.ops.transform.translate( 103 | "INVOKE_DEFAULT", 104 | cursor_transform=True, 105 | use_snap_self=True, 106 | snap_target="CLOSEST", 107 | use_snap_nonedit=True, 108 | snap_elements={"VERTEX"}, 109 | snap=True, 110 | release_confirm=True, 111 | ) 112 | self.count += 1 113 | if self.count == 1: 114 | # print("Count:", 1) 115 | self.report({"INFO"}, "Step 2: Q to place cursor at point B") 116 | elif self.count == 2: 117 | bpy.context.scene.IOPS.dragsnap_point_a = ( 118 | bpy.context.scene.cursor.location 119 | ) 120 | # print("Count:", 2) 121 | self.report({"INFO"}, "Step 3: press Q") 122 | elif self.count == 3: 123 | # print("Count:", 3) 124 | bpy.context.scene.IOPS.dragsnap_point_b = ( 125 | bpy.context.scene.cursor.location 126 | ) 127 | vector = ( 128 | bpy.context.scene.IOPS.dragsnap_point_b 129 | - bpy.context.scene.IOPS.dragsnap_point_a 130 | ) 131 | bpy.ops.transform.translate(value=vector, orient_type="GLOBAL") 132 | try: 133 | self.clear_draw_handlers() 134 | except ValueError: 135 | pass 136 | return {"FINISHED"} 137 | 138 | return {"RUNNING_MODAL"} 139 | 140 | def invoke(self, context, event): 141 | preferences = context.preferences 142 | if context.space_data.type == "VIEW_3D": 143 | uidpi = int((72 * preferences.system.ui_scale)) 144 | args_text = (self, context, uidpi, preferences.system.ui_scale) 145 | # Add draw handlers 146 | self._handle_iops_text = bpy.types.SpaceView3D.draw_handler_add( 147 | draw_iops_text, args_text, "WINDOW", "POST_PIXEL" 148 | ) 149 | self.report({"INFO"}, "Step 1: Q to place cursor at point A") 150 | self.vp_handlers = [self._handle_iops_text] 151 | # Add modal handler to enter modal mode 152 | context.window_manager.modal_handler_add(self) 153 | return {"RUNNING_MODAL"} 154 | else: 155 | self.report({"WARNING"}, "Active space must be a View3d") 156 | return {"CANCELLED"} 157 | -------------------------------------------------------------------------------- /operators/easy_mod_curve.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import ( 3 | BoolProperty, 4 | EnumProperty, 5 | ) 6 | 7 | 8 | class IOPS_OT_Easy_Mod_Curve(bpy.types.Operator): 9 | """Select picked curve in curve modifier""" 10 | 11 | bl_idname = "iops.modifier_easy_curve" 12 | bl_label = "Easy Modifier - Curve" 13 | bl_options = {"REGISTER", "UNDO"} 14 | 15 | use_curve_radius: BoolProperty( 16 | name="Use Curve Radius", 17 | description="Causes the deformed object to be scaled by the set curve radius.", 18 | default=True, 19 | ) 20 | use_curve_stretch: BoolProperty( 21 | name="Use Curve Length", 22 | description="The Stretch curve option allows you to let the mesh object stretch, or squeeze, over the entire curve.", 23 | default=True, 24 | ) 25 | use_curve_bounds_clamp: BoolProperty( 26 | name="Use Curve Bounds", 27 | description="When this option is enabled, the object and mesh offset along the deformation axis is ignored.", 28 | default=True, 29 | ) 30 | 31 | curve_modifier_axis: EnumProperty( 32 | name="Deformation Axis", 33 | description="Deformation along selected axis", 34 | items=[ 35 | ("POS_X", "X", "", "", 0), 36 | ("POS_Y", "Y", "", "", 1), 37 | ("POS_Z", "Z", "", "", 2), 38 | ("NEG_X", "-X", "", "", 3), 39 | ("NEG_Y", "-Y", "", "", 4), 40 | ("NEG_Z", "-Z", "", "", 5), 41 | ], 42 | default="POS_X", 43 | ) 44 | find_array_and_set_curve_fit: BoolProperty( 45 | name="Array - Fit Curve", 46 | description="Find Array modifier in the active object modifier list and set Array Fit type to - Fit Curve and pick selected spline there", 47 | default=False, 48 | ) 49 | Replace_Curve_Modifier: BoolProperty( 50 | name="Find and Replace Curve Modifier", 51 | description="Find and replace iOps curve modifier", 52 | default=True, 53 | ) 54 | 55 | @classmethod 56 | def poll(cls, context): 57 | return context.mode == "OBJECT" and context.area.type == "VIEW_3D" 58 | 59 | def execute(self, context): 60 | if ( 61 | len(context.view_layer.objects.selected) == 1 62 | and context.active_object.type == "MESH" 63 | ): 64 | for mod in bpy.context.active_object.modifiers: 65 | if mod.type == "CURVE": 66 | bpy.ops.object.select_all(action="DESELECT") 67 | mod.object.select_set(True) 68 | context.view_layer.objects.active = mod.object 69 | self.report({"INFO"}, "Curve Modifer - Object selected.") 70 | 71 | if len(context.view_layer.objects.selected) == 2: 72 | obj = None 73 | curve = None 74 | 75 | for ob in context.view_layer.objects.selected: 76 | if ob.type == "MESH": 77 | obj = ob 78 | if ob.type == "CURVE": 79 | curve = ob 80 | 81 | if obj and curve: 82 | cur = context.scene.cursor 83 | curve.data.use_radius = self.use_curve_radius 84 | curve.data.use_stretch = self.use_curve_stretch 85 | curve.data.use_deform_bounds = self.use_curve_bounds_clamp 86 | 87 | if obj.location != curve.location: 88 | bpy.ops.object.select_all(action="DESELECT") 89 | curve.select_set(True) 90 | context.view_layer.objects.active = curve 91 | 92 | if curve.data.splines.active.type == "POLY": 93 | cur.location = ( 94 | curve.data.splines.active.points[0].co.xyz 95 | @ curve.matrix_world.transposed() 96 | ) 97 | bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN") 98 | 99 | if curve.data.splines.active.type == "BEZIER": 100 | cur.location = ( 101 | curve.data.splines.active.bezier_points[0].co 102 | @ curve.matrix_world.transposed() 103 | ) 104 | bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN") 105 | 106 | bpy.ops.object.select_all(action="DESELECT") 107 | obj.location = curve.location 108 | 109 | if obj.modifiers: 110 | if self.Replace_Curve_Modifier: 111 | for mod in obj.modifiers: 112 | if mod.type == "CURVE": 113 | mod.object = curve 114 | mod.deform_axis = self.curve_modifier_axis 115 | curve.select_set(True) 116 | obj.select_set(True) 117 | context.view_layer.objects.active = obj 118 | self.report({"INFO"}, "Curve Modifier Replaced") 119 | else: 120 | mod = obj.modifiers.new("iOps Curve", type="CURVE") 121 | mod.object = curve 122 | mod.deform_axis = self.curve_modifier_axis 123 | curve.select_set(True) 124 | obj.select_set(True) 125 | context.view_layer.objects.active = obj 126 | self.report( 127 | {"INFO"}, "New Curve Modifier added and curve object picked" 128 | ) 129 | 130 | if self.find_array_and_set_curve_fit: 131 | for mod in obj.modifiers: 132 | if mod.type == "ARRAY": 133 | mod.fit_type = "FIT_CURVE" 134 | mod.curve = curve 135 | self.report( 136 | {"INFO"}, "Array modifier found and curve setted" 137 | ) 138 | else: 139 | mod = obj.modifiers.new("iOps Curve", type="CURVE") 140 | mod.object = curve 141 | mod.deform_axis = self.curve_modifier_axis 142 | curve.select_set(True) 143 | obj.select_set(True) 144 | context.view_layer.objects.active = obj 145 | self.report( 146 | {"INFO"}, "New Curve Modifier added and curve object picked" 147 | ) 148 | else: 149 | self.report({"WARNING"}, "Mesh or Curve missing!!!") 150 | return {"FINISHED"} 151 | -------------------------------------------------------------------------------- /operators/easy_mod_shwarp.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import ( 3 | BoolProperty, 4 | EnumProperty, 5 | FloatProperty, 6 | ) 7 | 8 | 9 | class IOPS_OT_Easy_Mod_Shwarp(bpy.types.Operator): 10 | """Select picked curve in curve modifier""" 11 | 12 | bl_idname = "iops.modifier_easy_shwarp" 13 | bl_label = "Easy Modifier - Shrinkwarp" 14 | bl_options = {"REGISTER", "UNDO"} 15 | 16 | shwarp_offset: FloatProperty( 17 | name="Offset", 18 | description="Offset factor", 19 | default=0.0, 20 | min=0.00, 21 | max=9999.0, 22 | ) 23 | shwarp_method: EnumProperty( 24 | name="Mode", 25 | description="Mod", 26 | items=[ 27 | ("NEAREST_SURFACEPOINT", "NEAREST_SURFACEPOINT", "", "", 0), 28 | ("PROJECT", "PROJECT", "", "", 1), 29 | ("NEAREST_VERTEX", "NEAREST_VERTEX", "", "", 2), 30 | ("TARGET_PROJECT", "TARGET_PROJECT", "", "", 3), 31 | ], 32 | default="PROJECT", 33 | ) 34 | shwarp_use_vg: BoolProperty( 35 | name="Use vertex groups", description="Takes last one", default=False 36 | ) 37 | 38 | transfer_normals: BoolProperty( 39 | name="Transfer Normals", 40 | description="Add mod to transfer normals from target object", 41 | default=False, 42 | ) 43 | 44 | stack_location: EnumProperty( 45 | name="Mod location in stack", 46 | description="Where to put SWARP modifier?", 47 | items=[ 48 | ("First", "First", "", "", 0), 49 | ("Last", "Last", "", "", 1), 50 | ("Before Active", "Before Active", "", "", 2), 51 | ("After Active", "After Active", "", "", 3), 52 | ], 53 | default="Last", 54 | ) 55 | 56 | @classmethod 57 | def poll(cls, context): 58 | return ( 59 | context.object 60 | and context.object.type == "MESH" 61 | and context.area.type == "VIEW_3D" 62 | and len(context.view_layer.objects.selected) >= 2 63 | ) 64 | 65 | def execute(self, context): 66 | target = context.active_object 67 | ctx = bpy.context.copy() 68 | objs = [] 69 | 70 | for ob in context.view_layer.objects.selected: 71 | if ob.name != target.name and ob.type == "MESH": 72 | objs.append(ob) 73 | 74 | if objs and target: 75 | # print(objs) 76 | for ob in objs: 77 | if self.shwarp_use_vg and ob.vertex_groups[:] == []: 78 | self.shwarp_use_vg = False 79 | self.transfer_normals = False 80 | 81 | if ob.modifiers: 82 | mod_list = ob.modifiers.keys() 83 | active_idx = mod_list.index(ob.modifiers.active.name) 84 | print("Active mod:", ob.modifiers.active.name) 85 | 86 | # Context copy 87 | ctx["object"] = ob 88 | ctx["active_object"] = ob 89 | ctx["selected_objects"] = [ob] 90 | ctx["selected_editable_objects"] = [ob] 91 | 92 | if "iOps Shwarp" in ob.modifiers.keys(): 93 | continue 94 | # mod = ob.modifiers['iOps Shwarp'] 95 | # mod.show_in_editmode = True 96 | # mod.show_on_cage = True 97 | # mod.target = target 98 | # mod.offset = self.shwarp_offset 99 | # mod.wrap_method = self.shwarp_method 100 | # if self.shwarp_use_vg: 101 | # mod.vertex_group = ob.vertex_groups[0].name 102 | else: 103 | mod = ob.modifiers.new("iOps Shwarp", type="SHRINKWRAP") 104 | mod.show_in_editmode = True 105 | mod.show_on_cage = True 106 | mod.target = target 107 | mod.offset = self.shwarp_offset 108 | mod.wrap_method = self.shwarp_method 109 | if self.shwarp_use_vg and ob.vertex_groups: 110 | mod.vertex_group = ob.vertex_groups[0].name 111 | 112 | # Logic to move modifier around 113 | if self.stack_location in {"First", "Last"}: 114 | count = len(ob.modifiers) - 1 115 | elif self.stack_location == "Before Active": 116 | count = len(ob.modifiers) - 1 - active_idx 117 | elif self.stack_location == "After Active": 118 | count = len(ob.modifiers) - 2 - active_idx 119 | 120 | print("Starting moves:", count) 121 | 122 | while count > 0: 123 | if self.stack_location in { 124 | "First", 125 | "Before Active", 126 | "After Active", 127 | }: 128 | with context.temp_override(**ctx): 129 | bpy.ops.object.modifier_move_down( 130 | modifier="iOps Shwarp" 131 | ) 132 | else: 133 | break 134 | count -= 1 135 | print("Active mod:", ob.modifiers.active.name, count) 136 | 137 | # Best settings for default Z projection, will use first group for snapping 138 | if self.shwarp_method == "PROJECT": 139 | mod.use_project_z = True 140 | mod.use_negative_direction = True 141 | mod.use_positive_direction = True 142 | if self.shwarp_use_vg and ob.vertex_groups: 143 | mod.vertex_group = ob.vertex_groups[0].name 144 | 145 | if self.transfer_normals: 146 | if "iOps Transfer Normals" not in ob.modifiers.keys(): 147 | mod = ob.modifiers.new( 148 | "iOps Transfer Normals", type="DATA_TRANSFER" 149 | ) 150 | mod.object = target 151 | mod.use_loop_data = True 152 | mod.data_types_loops = {"CUSTOM_NORMAL"} 153 | mod.loop_mapping = "POLYINTERP_NEAREST" 154 | if self.shwarp_use_vg and ob.vertex_groups: 155 | mod.vertex_group = ob.vertex_groups[0].name 156 | else: 157 | continue 158 | 159 | return {"FINISHED"} 160 | -------------------------------------------------------------------------------- /operators/executor.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os import listdir 3 | from os.path import isfile, join 4 | import bpy 5 | from bpy.props import StringProperty 6 | 7 | 8 | def get_prefix(name): 9 | if "_" in name: 10 | split = name.split("_") 11 | prefix = split[0] 12 | if prefix == prefix.upper(): 13 | split.pop(0) 14 | new_name = "_".join(split) 15 | return prefix, new_name 16 | else: 17 | return prefix[0].upper(), name 18 | else: 19 | return name[0].upper(), name 20 | 21 | 22 | def get_executor_column_width(scripts): 23 | max_lenght = 0 24 | executor_name_lenght = bpy.context.preferences.addons["InteractionOps"].preferences.executor_name_lenght 25 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences 26 | 27 | for script in scripts: 28 | full_name = os.path.split(script) 29 | name = os.path.splitext(full_name[1])[0] 30 | if len(name) > max_lenght: 31 | max_lenght = len(name) + executor_name_lenght 32 | 33 | min_width = executor_name_lenght 34 | column_count = max(1, int(len(scripts) / prefs.executor_column_count)) 35 | return max(min_width, max_lenght) 36 | 37 | 38 | class IOPS_OT_Executor(bpy.types.Operator): 39 | """Execute python scripts from folder""" 40 | 41 | bl_idname = "iops.executor" 42 | bl_label = "IOPS Executor" 43 | bl_options = {"REGISTER"} 44 | 45 | script: StringProperty( 46 | name="Script path", 47 | default="", 48 | ) 49 | 50 | def execute(self, context): 51 | filename = self.script 52 | exec(compile(open(filename).read(), filename, "exec")) 53 | return {"FINISHED"} 54 | 55 | 56 | class IOPS_PT_ExecuteList(bpy.types.Panel): 57 | bl_idname = "IOPS_PT_ExecuteList" 58 | bl_label = "Executor list" 59 | bl_space_type = "VIEW_3D" 60 | bl_region_type = "WINDOW" 61 | 62 | def draw(self, context): 63 | prefs = context.preferences.addons["InteractionOps"].preferences 64 | props = context.window_manager.IOPS_AddonProperties 65 | letter = "" 66 | scripts = bpy.context.scene.IOPS["executor_scripts"] 67 | global_column_amount = max(1, int(len(scripts) / prefs.executor_column_count)) 68 | 69 | layout = self.layout 70 | if getattr(props, "iops_exec_filter", None): 71 | filtered_scripts = bpy.context.scene.IOPS.get("filtered_executor_scripts", []) 72 | scripts = filtered_scripts if props.iops_exec_filter and filtered_scripts else bpy.context.scene.IOPS["executor_scripts"] 73 | else: 74 | scripts = bpy.context.scene.IOPS["executor_scripts"] 75 | column_amount = max(1, int(len(scripts) / prefs.executor_column_count)) 76 | layout.ui_units_x = get_executor_column_width(scripts) / (global_column_amount / column_amount) 77 | column_flow = layout.column_flow(columns=column_amount, align=False) 78 | column_flow.prop(props, "iops_exec_filter", text="", icon="VIEWZOOM") 79 | if scripts: 80 | for script in scripts: # Start counting from 1 81 | full_name = os.path.split(script) 82 | name = os.path.splitext(full_name[1])[0] 83 | list_name = name[0].upper() 84 | if str(list_name) != letter: 85 | column_flow.label(text=str(list_name)) 86 | letter = str(list_name) 87 | column_flow.operator( 88 | "iops.executor", text=name, icon="FILE_SCRIPT" 89 | ).script = script 90 | 91 | 92 | class IOPS_OT_Call_MT_Executor(bpy.types.Operator): 93 | """Active object data(mesh) information""" 94 | 95 | bl_idname = "iops.scripts_call_mt_executor" 96 | bl_label = "IOPS Call Executor" 97 | 98 | def execute(self, context): 99 | addon_prop = context.window_manager.IOPS_AddonProperties 100 | addon_prop.iops_exec_filter = "" 101 | if "filtered_executor_scripts" in bpy.context.scene.IOPS.keys(): 102 | del bpy.context.scene.IOPS["filtered_executor_scripts"] 103 | prefs = context.preferences.addons["InteractionOps"].preferences 104 | executor_scripts_folder = prefs.executor_scripts_folder 105 | scripts_folder = executor_scripts_folder # TODO: Add user scripts folder 106 | # scripts_folder = os.path.join(scripts_folder, "custom") 107 | _files = [f for f in listdir(scripts_folder) if isfile(join(scripts_folder, f))] 108 | files = [os.path.join(scripts_folder, f) for f in _files] 109 | scripts = [script for script in files if script[-2:] == "py"] 110 | bpy.context.scene["IOPS"]["executor_scripts"] = scripts 111 | bpy.ops.wm.call_panel(name="IOPS_PT_ExecuteList") 112 | return {"FINISHED"} 113 | -------------------------------------------------------------------------------- /operators/grid_from_active.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import numpy as np 3 | from mathutils import Vector 4 | 5 | 6 | class IOPS_OT_ToGridFromActive(bpy.types.Operator): 7 | """Locations to grid from active""" 8 | 9 | bl_idname = "iops.object_to_grid_from_active" 10 | bl_label = "To Grid From Active" 11 | bl_options = {"REGISTER", "UNDO"} 12 | 13 | @classmethod 14 | def poll(self, context): 15 | return ( 16 | context.area.type == "VIEW_3D" 17 | and context.mode == "OBJECT" 18 | and len(context.view_layer.objects.selected) != 0 19 | ) 20 | 21 | def execute(self, context): 22 | C = bpy.context 23 | active = C.view_layer.objects.active 24 | origin = active.location 25 | objects = C.selected_objects 26 | mx_orig = active.matrix_world.copy() 27 | 28 | # Reset transforms of all 29 | bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) 30 | active.matrix_world @= active.matrix_world.inverted() 31 | bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM") 32 | 33 | # Sizes 34 | size_x = active.dimensions[0] 35 | size_y = active.dimensions[1] 36 | size_z = active.dimensions[2] 37 | 38 | # Move objects to grid 39 | o_xyz = np.array([origin[0], origin[1], origin[2]], dtype=np.float32) 40 | 41 | for o in objects: 42 | print("Location difference before:", o.location - active.location) 43 | 44 | # Extracting 3D coordinates 45 | p = np.array([o.location[0], o.location[1], o.location[2]]) 46 | 47 | # Calculations for X, Y, and Z axes 48 | p_prime = ( 49 | np.around((p - o_xyz) / (size_x, size_y, size_z)) 50 | * (size_x, size_y, size_z) 51 | + o_xyz 52 | ) 53 | 54 | # Checking for close match 55 | if np.allclose(p, p_prime, rtol=1e-21, atol=1e-24): 56 | continue 57 | 58 | # Setting new location 59 | location = Vector((p_prime[0], p_prime[1], p_prime[2])) 60 | o.matrix_world @= o.matrix_world.inverted() 61 | o.location = location 62 | 63 | print("----") 64 | print("p", p) 65 | print("p_prime", p_prime) 66 | 67 | # Restore matrix for all and clear parent 68 | bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) 69 | active.matrix_world = mx_orig 70 | bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM") 71 | 72 | self.report({"INFO"}, "Aligned to grid from active") 73 | 74 | return {"FINISHED"} 75 | -------------------------------------------------------------------------------- /operators/hotkeys/load_hotkeys.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | import json 4 | from ...utils.functions import register_keymaps, unregister_keymaps 5 | from ...prefs.hotkeys_default import keys_default 6 | 7 | 8 | class IOPS_OT_LoadUserHotkeys(bpy.types.Operator): 9 | bl_idname = "iops.load_user_hotkeys" 10 | bl_label = "Load User's Hotkeys" 11 | bl_options = {"REGISTER", "UNDO"} 12 | 13 | def execute(self, context): 14 | unregister_keymaps() 15 | bpy.context.window_manager.keyconfigs.update() 16 | 17 | keys_user = [] 18 | 19 | path = bpy.utils.script_path_user() 20 | user_hotkeys_file = os.path.join( 21 | path, "presets", "IOPS", "iops_hotkeys_user.py" 22 | ) 23 | user_hotkeys_path = os.path.join(path, "presets", "IOPS") 24 | 25 | if os.path.exists(user_hotkeys_file): 26 | with open(user_hotkeys_file) as f: 27 | keys_user = json.load(f) 28 | else: 29 | if not os.path.exists(user_hotkeys_path): 30 | os.makedirs(user_hotkeys_path) 31 | with open(user_hotkeys_file, "w") as f: 32 | f.write("[]") 33 | 34 | register_keymaps(keys_user) 35 | print("Loaded user's hotkeys") 36 | return {"FINISHED"} 37 | 38 | 39 | class IOPS_OT_LoadDefaultHotkeys(bpy.types.Operator): 40 | bl_idname = "iops.load_default_hotkeys" 41 | bl_label = "Load Default Hotkeys" 42 | bl_options = {"REGISTER", "UNDO"} 43 | 44 | def execute(self, context): 45 | unregister_keymaps() 46 | bpy.context.window_manager.keyconfigs.update() 47 | register_keymaps(keys_default) 48 | print("Loaded default hotkeys") 49 | return {"FINISHED"} 50 | -------------------------------------------------------------------------------- /operators/hotkeys/save_hotkeys.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | import json 4 | 5 | 6 | def save_hotkeys(): 7 | path = bpy.utils.script_path_user() 8 | folder = os.path.join(path, "presets", "IOPS") 9 | user_hotkeys_file = os.path.join(path, "presets", "IOPS", "iops_hotkeys_user.py") 10 | if not os.path.exists(folder): 11 | os.makedirs(folder) 12 | with open(user_hotkeys_file, "w") as f: 13 | data = get_iops_keys() 14 | f.write("[" + ",\n".join(json.dumps(i) for i in data) + "]\n") 15 | 16 | 17 | def get_iops_keys(): 18 | keys = [] 19 | 20 | if bpy.app.version[0] == 2 and bpy.app.version[1] <= 92: 21 | keyconfig = bpy.context.window_manager.keyconfigs["blender addon"] 22 | elif bpy.app.version[0] == 2 and bpy.app.version[1] >= 92: 23 | keyconfig = bpy.context.window_manager.keyconfigs["Blender addon"] 24 | elif bpy.app.version[0] == 3 and bpy.app.version[1] >= 0: 25 | keyconfig = bpy.context.window_manager.keyconfigs["Blender addon"] 26 | elif bpy.app.version[0] == 4 and bpy.app.version[1] >= 0: 27 | keyconfig = bpy.context.window_manager.keyconfigs["Blender user"] 28 | 29 | for keymap in keyconfig.keymaps: 30 | if keymap: 31 | keymapItems = keymap.keymap_items 32 | toSave = tuple( 33 | item for item in keymapItems if item.idname.startswith("iops.") 34 | ) 35 | for item in toSave: 36 | entry = ( 37 | item.idname, 38 | item.type, 39 | item.value, 40 | item.ctrl, 41 | item.alt, 42 | item.shift, 43 | item.oskey, 44 | ) 45 | keys.append(entry) 46 | for k in keys: 47 | print(k) 48 | return keys 49 | 50 | 51 | class IOPS_OT_SaveUserHotkeys(bpy.types.Operator): 52 | bl_idname = "iops.save_user_hotkeys" 53 | bl_label = "Save User's Hotkeys" 54 | bl_options = {"REGISTER", "UNDO"} 55 | 56 | def execute(self, context): 57 | save_hotkeys() 58 | print("Saved user's hotkeys") 59 | return {"FINISHED"} 60 | -------------------------------------------------------------------------------- /operators/image_reload.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | class IOPS_OT_Reload_Images(bpy.types.Operator): 4 | """Reload images in the current blend file if they are existing.""" 5 | bl_idname = "iops.reload_images" 6 | bl_label = "Reload Images" 7 | bl_description = "Reload Images in the current blend file if they are existing." 8 | 9 | @classmethod 10 | def poll(cls, context): 11 | return (bpy.data.images) 12 | 13 | def execute(self, context): 14 | for image in bpy.data.images: 15 | image.reload() 16 | self.report({'INFO'}, f"Image '{image.name}' reloaded.") 17 | return {'FINISHED'} -------------------------------------------------------------------------------- /operators/iops.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from ..utils.iops_dict import IOPS_Dict 3 | from ..utils.functions import get_iop 4 | 5 | 6 | class IOPS_OT_Main(bpy.types.Operator): 7 | bl_idname = "iops.main" 8 | bl_label = "IOPS" 9 | bl_options = {"REGISTER", "UNDO"} 10 | 11 | # modes_3d = {0: "VERT", 1: "EDGE", 2: "FACE"} 12 | # modes_uv = {0: "VERTEX", 1: "EDGE", 2: "FACE", 3: "ISLAND"} 13 | # modes_gpen = {0: "EDIT_GPENCIL", 1: "PAINT_GPENCIL", 2: "SCULPT_GPENCIL"} 14 | # modes_curve = {0: "EDIT_CURVE"} 15 | # modes_text = {0: "EDIT_TEXT"} 16 | # modes_meta = {0: "EDIT_META"} 17 | # modes_lattice = {0: "EDIT_LATTICE"} 18 | # modes_armature = {0: "EDIT", 1: "POSE"} 19 | # supported_types = {"MESH", "CURVE", "GPENCIL", "EMPTY", "TEXT", "META", "ARMATURE", "LATTICE"} 20 | 21 | # @classmethod 22 | # def poll(cls, context): 23 | # return (bpy.context.object is not None and 24 | # bpy.context.active_object is not None) 25 | 26 | alt = False 27 | ctrl = False 28 | shift = False 29 | 30 | def get_mode_3d(self, tool_mesh): 31 | mode = "" 32 | if tool_mesh[0]: 33 | mode = "VERT" 34 | elif tool_mesh[1]: 35 | mode = "EDGE" 36 | elif tool_mesh[2]: 37 | mode = "FACE" 38 | return mode 39 | 40 | def get_modifier_state(self): 41 | modifiers = [] 42 | if self.alt: 43 | modifiers.append("ALT") 44 | if self.ctrl: 45 | modifiers.append("CTRL") 46 | if self.shift: 47 | modifiers.append("SHIFT") 48 | return "_".join(modifiers) if modifiers else "NONE" 49 | 50 | def invoke(self, context, event): 51 | # Set modifier flags 52 | self.alt = event.alt 53 | self.ctrl = event.ctrl 54 | self.shift = event.shift 55 | return self.execute(context) 56 | 57 | def execute(self, context): 58 | op = self.operator 59 | event = self.get_modifier_state() 60 | if bpy.context.area: 61 | type_area = bpy.context.area.type 62 | if bpy.context.view_layer.objects.active: 63 | tool_mesh = bpy.context.scene.tool_settings.mesh_select_mode 64 | type_object = bpy.context.view_layer.objects.active.type 65 | mode_object = bpy.context.view_layer.objects.active.mode 66 | mode_mesh = self.get_mode_3d(tool_mesh) 67 | mode_uv = 'UV_' + bpy.context.tool_settings.uv_select_mode 68 | flag_uv = bpy.context.tool_settings.use_uv_select_sync 69 | if flag_uv: 70 | mode_uv = mode_mesh 71 | 72 | # Build query with current state 73 | query = ( 74 | type_area, 75 | flag_uv, 76 | type_object, 77 | mode_object, 78 | mode_uv, 79 | mode_mesh, 80 | op, 81 | event, 82 | ) 83 | else: 84 | query = (type_area, None, None, None, None, None, None, None) 85 | 86 | # Get and execute the function 87 | function = get_iop(IOPS_Dict.iops_dict, query) 88 | if function: 89 | function() 90 | else: 91 | self.report({"WARNING"}, "No operation defined for this context") 92 | else: 93 | self.report({"INFO"}, "Focus your mouse pointer on corresponding window.") 94 | return {"FINISHED"} 95 | -------------------------------------------------------------------------------- /operators/library_reload.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | class IOPS_OT_Reload_Libraries(bpy.types.Operator): 4 | """Reload Libraries in the current blend file if they are existing.""" 5 | bl_idname = "iops.reload_libraries" 6 | bl_label = "Reload Libraries" 7 | bl_description = "Reload Libraries in the current blend file if they are existing." 8 | 9 | @classmethod 10 | def poll(cls, context): 11 | return (bpy.data.libraries) 12 | 13 | def execute(self, context): 14 | for lib in bpy.data.libraries: 15 | lib.reload() 16 | self.report({'INFO'}, f"Library '{lib.name}' reloaded.") 17 | return {'FINISHED'} 18 | -------------------------------------------------------------------------------- /operators/materials_from_textures.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | from bpy.props import CollectionProperty, BoolProperty 4 | from bpy_extras.io_utils import ImportHelper 5 | from bpy.types import Operator 6 | 7 | 8 | def material_name_generator(name): 9 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences 10 | prefixes = prefs.texture_to_material_prefixes.split(",") 11 | suffixes = prefs.texture_to_material_suffixes.split(",") 12 | extension = name.find(".") 13 | name = name[:extension] 14 | for pref in prefixes: 15 | if name.startswith(pref): 16 | name = name[len(pref) :] 17 | for suf in suffixes: 18 | if name.endswith(suf): 19 | name = name[: -len(suf)] 20 | return name 21 | 22 | 23 | class IOPS_OT_MaterialsFromTextures(Operator, ImportHelper): 24 | """Create Materials from Selected Textures in the File Browser""" 25 | 26 | bl_idname = "iops.materials_from_textures" 27 | bl_label = "IOPS Materials from Textures" 28 | 29 | files: CollectionProperty(type=bpy.types.PropertyGroup) 30 | import_all: BoolProperty( 31 | name="Import all textures", 32 | description="Import all related textures", 33 | default=True, 34 | ) 35 | 36 | def execute(self, context): 37 | dirname = os.path.dirname(self.filepath) 38 | print("DIRNAME", dirname) 39 | for f in self.files: 40 | path = os.path.join(dirname, f.name) 41 | # Create Shader/Matrial 42 | mat_name = material_name_generator(f.name) 43 | mat = bpy.data.materials.new(name=mat_name) 44 | mat.use_nodes = True 45 | bsdf = mat.node_tree.nodes["Principled BSDF"] 46 | bsdf.location = (0, 0) 47 | mat.node_tree.nodes["Material Output"].location = (350, 0) 48 | # Create Image 49 | texImage = mat.node_tree.nodes.new("ShaderNodeTexImage") 50 | texImage.image = bpy.data.images.load(path) 51 | texImage.image.use_fake_user = True 52 | texImage.location = (-600, 120) 53 | mat.node_tree.links.new( 54 | bsdf.inputs["Base Color"], texImage.outputs["Color"] 55 | ) 56 | mat.use_fake_user = True 57 | offset = -350 58 | if self.import_all: 59 | directory = os.listdir(dirname) 60 | for file in directory: 61 | if mat_name in file and file != f.name: 62 | normal_map = mat_name + "_nm" 63 | mask_map = mat_name + "_mk" 64 | if normal_map in file: 65 | path = os.path.join(dirname, file) 66 | NormalImage = mat.node_tree.nodes.new("ShaderNodeTexImage") 67 | NormalImage.name = normal_map 68 | NormalImage.image = bpy.data.images.load(path) 69 | NormalImage.image.colorspace_settings.name = "Non-Color" 70 | NormalImage.image.use_fake_user = True 71 | NormalImage.location = (-600, -720) 72 | 73 | NormalMap = mat.node_tree.nodes.new("ShaderNodeNormalMap") 74 | NormalMap.location = (-250, -600) 75 | 76 | mat.node_tree.links.new( 77 | NormalMap.inputs["Color"], NormalImage.outputs["Color"] 78 | ) 79 | mat.node_tree.links.new( 80 | bsdf.inputs["Normal"], NormalMap.outputs["Normal"] 81 | ) 82 | elif mask_map in file: 83 | path = os.path.join(dirname, file) 84 | MaskImage = mat.node_tree.nodes.new("ShaderNodeTexImage") 85 | MaskImage.name = mask_map 86 | MaskImage.image = bpy.data.images.load(path) 87 | MaskImage.image.colorspace_settings.name = "Non-Color" 88 | MaskImage.image.use_fake_user = True 89 | MaskImage.location = (-600, -300) 90 | 91 | SeparateRGB = mat.node_tree.nodes.new( 92 | "ShaderNodeSeparateRGB" 93 | ) 94 | SeparateRGB.location = (-250, -250) 95 | 96 | mat.node_tree.links.new( 97 | SeparateRGB.inputs["Image"], MaskImage.outputs["Color"] 98 | ) 99 | mat.node_tree.links.new( 100 | bsdf.inputs["Metallic"], SeparateRGB.outputs["R"] 101 | ) 102 | mat.node_tree.links.new( 103 | bsdf.inputs["Roughness"], SeparateRGB.outputs["G"] 104 | ) 105 | else: 106 | path = os.path.join(dirname, file) 107 | texImage = mat.node_tree.nodes.new("ShaderNodeTexImage") 108 | texImage.name = file 109 | texImage.image = bpy.data.images.load(path) 110 | texImage.image.use_fake_user = True 111 | texImage.location = (350, offset) 112 | offset -= 450 113 | 114 | return {"FINISHED"} 115 | -------------------------------------------------------------------------------- /operators/maya_isolate.py: -------------------------------------------------------------------------------- 1 | # In edit mode hides unselected and enters local view 2 | # In object mode uses default blende behavior 3 | 4 | import bpy 5 | 6 | 7 | class IOPS_OT_MayaIsolate(bpy.types.Operator): 8 | """Maya-like isolate selection""" 9 | 10 | bl_idname = "iops.view3d_maya_isolate" 11 | bl_label = "IOPS Maya Isolate" 12 | bl_options = {"REGISTER", "UNDO"} 13 | 14 | @classmethod 15 | def poll(self, context): 16 | return ( 17 | context.area.type == "VIEW_3D" 18 | and (context.mode == "OBJECT" or context.mode == "EDIT_MESH") 19 | and context.view_layer.objects.active.type == "MESH" 20 | and len(context.view_layer.objects.selected) != 0 21 | ) 22 | 23 | def mesh_isolate(self, context): 24 | bpy.ops.mesh.hide(unselected=True) 25 | bpy.ops.view3d.localview(frame_selected=False) 26 | 27 | def object_isolate(self, context): 28 | bpy.ops.view3d.localview(frame_selected=False) 29 | 30 | def execute(self, context): 31 | if context.mode == "OBJECT": 32 | self.object_isolate(context) 33 | elif context.mode == "EDIT_MESH": 34 | self.mesh_isolate(context) 35 | 36 | return {"FINISHED"} 37 | -------------------------------------------------------------------------------- /operators/mesh_convert_selection.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class IOPS_OT_ToFaces(bpy.types.Operator): 5 | """Convert Vertex/Edge selection to face selection""" 6 | 7 | bl_idname = "iops.mesh_to_faces" 8 | bl_label = "Convert vertex/edge selection to face selection" 9 | bl_options = {"REGISTER", "UNDO"} 10 | 11 | @classmethod 12 | def poll(cls, context): 13 | return context.mode == "EDIT_MESH" 14 | 15 | def execute(self, context): 16 | bpy.ops.mesh.select_mode(use_expand=True, type="FACE") 17 | context.tool_settings.mesh_select_mode = (False, False, True) 18 | return {"FINISHED"} 19 | 20 | 21 | class IOPS_OT_ToEdges(bpy.types.Operator): 22 | """Convert Vertex/Face selection to edge selection""" 23 | 24 | bl_idname = "iops.mesh_to_edges" 25 | bl_label = "Convert vertex/face selection to edge selection" 26 | bl_options = {"REGISTER", "UNDO"} 27 | 28 | @classmethod 29 | def poll(cls, context): 30 | return context.mode == "EDIT_MESH" 31 | 32 | def execute(self, context): 33 | exp = False 34 | if context.tool_settings.mesh_select_mode[0]: 35 | exp = True 36 | bpy.ops.mesh.select_mode(use_expand=exp, type="EDGE") 37 | context.tool_settings.mesh_select_mode = (False, True, False) 38 | return {"FINISHED"} 39 | 40 | 41 | class IOPS_OT_ToVerts(bpy.types.Operator): 42 | """Convert Edge/Face selection to vertex selection""" 43 | 44 | bl_idname = "iops.mesh_to_verts" 45 | bl_label = "Convert edge/face selection to vertex selection" 46 | bl_options = {"REGISTER", "UNDO"} 47 | 48 | @classmethod 49 | def poll(cls, context): 50 | return context.mode == "EDIT_MESH" 51 | 52 | def execute(self, context): 53 | bpy.ops.mesh.select_mode(use_extend=True, type="VERT") 54 | context.tool_settings.mesh_select_mode = (True, False, False) 55 | return {"FINISHED"} 56 | -------------------------------------------------------------------------------- /operators/mesh_copy_edges_angle.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from math import degrees 4 | 5 | 6 | class IOPS_MESH_OT_CopyEdgesAngle(bpy.types.Operator): 7 | """Copy the angle between 2 selected edges""" 8 | 9 | bl_idname = "iops.mesh_copy_edges_angle" 10 | bl_label = "Copy Edges Angle" 11 | 12 | @classmethod 13 | def poll(cls, context): 14 | return ( 15 | context.mode == "EDIT_MESH" 16 | and context.active_object 17 | and context.active_object.type == "MESH" 18 | ) 19 | 20 | def edge_vector(self, edge): 21 | v1, v2 = edge.verts 22 | return (v2.co - v1.co).normalized() 23 | 24 | def execute(self, context): 25 | active_object = context.active_object 26 | bm = bmesh.from_edit_mesh(active_object.data) 27 | bm.edges.ensure_lookup_table() 28 | 29 | selected_edges = [e for e in bm.edges if e.select] 30 | 31 | if len(selected_edges) == 2: 32 | vec1 = self.edge_vector(selected_edges[0]) 33 | vec2 = self.edge_vector(selected_edges[1]) 34 | 35 | # Calculate angle in degrees 36 | angle_rad = vec1.angle(vec2) 37 | angle_deg = degrees(angle_rad) 38 | 39 | if angle_deg == 0: 40 | self.report({"WARNING"}, "Edges do not intersect") 41 | return {"FINISHED"} 42 | 43 | self.report({"INFO"}, f"Copied {angle_deg} to clipboard") 44 | 45 | bpy.context.window_manager.clipboard = str(angle_deg) 46 | bm.free() 47 | return {"FINISHED"} 48 | 49 | else: 50 | self.report({"ERROR"}, "Select only 2 edges.") 51 | return {"CANCELLED"} 52 | -------------------------------------------------------------------------------- /operators/mesh_copy_edges_length.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | 4 | 5 | def get_unit_scale(): 6 | """Get the unit scale based on the current scene units""" 7 | units = bpy.data.scenes["Scene"].unit_settings.length_unit 8 | if units == "MICROMETERS": 9 | return 0.000001 10 | elif units == "MILLIMETERS": 11 | return 0.001 12 | elif units == "CENTIMETERS": 13 | return 0.01 14 | elif units == "METERS": 15 | return 1.0 16 | elif units == "KILOMETERS": 17 | return 1000.0 18 | elif units == "ADAPTIVE": 19 | return 1.0 20 | else: 21 | return 1.0 22 | 23 | 24 | class IOPS_MESH_OT_CopyEdgesLength(bpy.types.Operator): 25 | """Copy the active edge length to the clipboard""" 26 | 27 | bl_idname = "iops.mesh_copy_edge_length" 28 | bl_label = "Copy Active Edge Length to Clipboard" 29 | 30 | @classmethod 31 | def poll(cls, context): 32 | return ( 33 | context.mode == "EDIT_MESH" 34 | and context.active_object 35 | and context.active_object.type == "MESH" 36 | ) 37 | 38 | def execute(self, context): 39 | active_object = context.active_object 40 | bm = bmesh.from_edit_mesh(active_object.data) 41 | bm.edges.ensure_lookup_table() 42 | selected_verts = [v for v in bm.verts if v.select] 43 | selected_edges = [e for e in bm.edges if e.select] 44 | 45 | if selected_edges: 46 | # Add the length of all selected edges and store it in the clipboard 47 | total_length = ( 48 | sum(e.calc_length() for e in selected_edges) / get_unit_scale() 49 | ) 50 | bpy.context.window_manager.clipboard = str(total_length) 51 | self.report({"INFO"}, "Copied the sum of edges' length to clipboard") 52 | bm.free() 53 | return {"FINISHED"} 54 | # if only 2 verts are selected, copy the distance between them 55 | elif len(selected_verts) == 2: 56 | bpy.context.window_manager.clipboard = str( 57 | (selected_verts[0].co - selected_verts[1].co).length 58 | ) 59 | self.report( 60 | {"INFO"}, 61 | "Copied the distance between the 2 selected verts to clipboard", 62 | ) 63 | bm.free() 64 | return {"FINISHED"} 65 | else: 66 | # Invalid Selection 67 | self.report({"ERROR"}, "Invalid Selection") 68 | return {"CANCELLED"} 69 | -------------------------------------------------------------------------------- /operators/mesh_cursor_bisect.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import mathutils 3 | import bmesh 4 | from mathutils import Vector 5 | import gpu 6 | from gpu_extras.batch import batch_for_shader 7 | # Removed heapq import as 'merge' wasn't used 8 | 9 | class IOPS_OT_Mesh_Cursor_Bisect(bpy.types.Operator): 10 | """Bisect mesh using 3D cursor position and orientation""" 11 | bl_idname = "iops.mesh_cursor_bisect" 12 | bl_label = "Cursor Bisect" 13 | bl_description = "Bisect the mesh using the 3D cursor position and orientation" 14 | bl_options = {'REGISTER', 'UNDO'} 15 | 16 | axis: bpy.props.EnumProperty( 17 | name="Bisect Axis", 18 | description="Choose the axis for bisecting", 19 | items=[ 20 | ('X', "X Axis", "Bisect along the X axis"), 21 | ('Y', "Y Axis", "Bisect along the Y axis"), 22 | ('Z', "Z Axis", "Bisect along the Z axis"), 23 | ], 24 | default='Y' 25 | ) 26 | merge_doubles: bpy.props.BoolProperty( 27 | name="Merge Doubles", 28 | description="Merge vertices after bisecting", 29 | default=True 30 | ) 31 | merge_distance: bpy.props.FloatProperty( 32 | name="Merge Distance", 33 | description="Distance for merging vertices after bisecting", 34 | default=0.005, 35 | min=0.0, 36 | max=10.0, 37 | ) 38 | 39 | _handle = None 40 | # _plane_size removed as it wasn't defined or used consistently 41 | 42 | def draw_callback(self, context): 43 | cursor = context.scene.cursor 44 | plane_co = cursor.location 45 | obj = context.object 46 | if obj is None: 47 | return 48 | 49 | cursor_mx = bpy.context.scene.cursor.matrix 50 | 51 | if self.axis == 'X': 52 | color = (1.0, 0.0, 0.0, 0.3) 53 | u = Vector((0.0, 1.0, 0.0)) 54 | v = Vector((0.0, 0.0, 1.0)) 55 | elif self.axis == 'Y': 56 | color = (0.0, 1.0, 0.0, 0.3) 57 | u = Vector((1.0, 0.0, 0.0)) 58 | v = Vector((0.0, 0.0, 1.0)) 59 | else: # self.axis == 'Z' 60 | color = (0.0, 0.0, 1.0, 0.3) 61 | u = Vector((1.0, 0.0, 0.0)) 62 | v = Vector((0.0, 1.0, 0.0)) 63 | 64 | u = cursor_mx.to_3x3() @ u 65 | v = cursor_mx.to_3x3() @ v 66 | 67 | size = 1.0 # Keep a fixed size for the visual aid 68 | corners = [ 69 | plane_co + (u + v) * size, 70 | plane_co + (u - v) * size, 71 | plane_co + (-u - v) * size, 72 | plane_co + (-u + v) * size, 73 | ] 74 | 75 | shader = gpu.shader.from_builtin('UNIFORM_COLOR') 76 | batch = batch_for_shader(shader, 'TRI_FAN', {"pos": corners}) 77 | shader.bind() 78 | shader.uniform_float("color", color) 79 | gpu.state.blend_set('ALPHA') 80 | gpu.state.depth_test_set('NONE') 81 | batch.draw(shader) 82 | gpu.state.depth_test_set('LESS') 83 | gpu.state.blend_set('NONE') 84 | 85 | def invoke(self, context, event): 86 | if context.area.type == 'VIEW_3D': 87 | self._handle = bpy.types.SpaceView3D.draw_handler_add( 88 | self.draw_callback, (context,), 'WINDOW', 'POST_VIEW' 89 | ) 90 | context.window_manager.modal_handler_add(self) 91 | context.area.tag_redraw() 92 | return {'RUNNING_MODAL'} 93 | else: 94 | self.report({'WARNING'}, "View3D not found, cannot run operator") 95 | return {'CANCELLED'} 96 | 97 | def modal(self, context, event): 98 | if event.type in {'MIDDLEMOUSE'}: 99 | return {'PASS_THROUGH'} 100 | elif event.type in {'ESC', 'RIGHTMOUSE'}: 101 | self.cancel(context) 102 | return {'CANCELLED'} 103 | elif event.type == 'LEFTMOUSE': 104 | self.execute(context) 105 | self.cancel(context) 106 | return {'FINISHED'} 107 | elif event.type == 'WHEELUPMOUSE': 108 | self.axis = {'X': 'Y', 'Y': 'Z', 'Z': 'X'}[self.axis] 109 | context.area.tag_redraw() 110 | return {'RUNNING_MODAL'} # Stay modal 111 | elif event.type == 'WHEELDOWNMOUSE': 112 | self.axis = {'X': 'Z', 'Z': 'Y', 'Y': 'X'}[self.axis] 113 | context.area.tag_redraw() 114 | return {'RUNNING_MODAL'} # Stay modal 115 | # Removed arrow key handling for plane size as it wasn't fully implemented 116 | 117 | # Redraw requested for axis changes or potentially other interactions 118 | if event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}: 119 | context.area.tag_redraw() 120 | 121 | return {'RUNNING_MODAL'} 122 | 123 | 124 | def execute(self, context): 125 | obj = context.object 126 | scene = context.scene 127 | cursor = scene.cursor 128 | 129 | if not obj: 130 | self.report({'ERROR'}, "No active object selected.") 131 | return {'CANCELLED'} 132 | if obj.type != 'MESH': 133 | self.report({'ERROR'}, "Active object is not a Mesh.") 134 | return {'CANCELLED'} 135 | if obj.mode != 'EDIT': 136 | self.report({'ERROR'}, "Not in Edit Mode. Please switch to Edit Mode.") 137 | return {'CANCELLED'} 138 | 139 | obj_mat_world = obj.matrix_world 140 | obj_mat_world_inv = obj_mat_world.inverted() 141 | cursor_mat_world = cursor.matrix 142 | 143 | plane_co_local = obj_mat_world_inv @ cursor.location 144 | 145 | if self.axis == 'X': 146 | base_normal = mathutils.Vector((1, 0, 0)) 147 | elif self.axis == 'Y': 148 | base_normal = mathutils.Vector((0, 1, 0)) 149 | else: # self.axis == 'Z' 150 | base_normal = mathutils.Vector((0, 0, 1)) 151 | 152 | normal_world = cursor_mat_world.to_3x3() @ base_normal 153 | normal_world.normalize() 154 | 155 | plane_no_local = obj_mat_world_inv.to_3x3() @ normal_world 156 | plane_no_local.normalize() 157 | 158 | bm = bmesh.from_edit_mesh(obj.data) 159 | 160 | selected_faces = [f for f in bm.faces if f.select] 161 | if not selected_faces: 162 | # If no faces selected, operate on the whole mesh implicitly 163 | # by not restricting the 'geom' input (or providing all geometry) 164 | # For bisect_plane, providing None or all geom works 165 | geom = bm.verts[:] + bm.edges[:] + bm.faces[:] # Operate on everything 166 | else: 167 | # Operate only on selected faces and their constituent edges/verts 168 | geom_faces = set(selected_faces) 169 | geom_edges = set(e for f in geom_faces for e in f.edges) 170 | geom_verts = set(v for f in geom_faces for v in f.verts) 171 | geom = list(geom_verts) + list(geom_edges) + list(geom_faces) 172 | 173 | 174 | try: 175 | result = bmesh.ops.bisect_plane( 176 | bm, 177 | geom=geom, 178 | plane_co=plane_co_local, 179 | plane_no=plane_no_local, 180 | # clear_inner=False, # Default 181 | # clear_outer=False, # Default 182 | # use_fill=False # Default 183 | ) 184 | 185 | if self.merge_doubles: 186 | bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=self.merge_distance) 187 | else: 188 | if selected_faces: 189 | for f in selected_faces: 190 | if f.is_valid: 191 | f.select_set(False) 192 | 193 | if result and 'geom_cut' in result: 194 | for element in result['geom_cut']: 195 | if element.is_valid: 196 | element.select_set(True) 197 | bm.select_flush(True) 198 | 199 | bmesh.update_edit_mesh(obj.data) 200 | 201 | except Exception as e: 202 | self.report({'ERROR'}, f"BMesh bisect failed: {e}") 203 | # Avoid freeing BMesh here as 'from_edit_mesh' handles it 204 | return {'CANCELLED'} 205 | 206 | return {'FINISHED'} 207 | 208 | def cancel(self, context): 209 | if self._handle is not None: 210 | bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') 211 | self._handle = None 212 | # Ensure redraw happens on cancel to remove the visual aid 213 | if context.area: 214 | context.area.tag_redraw() 215 | -------------------------------------------------------------------------------- /operators/mesh_quick_snap.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from mathutils import Vector 4 | from bpy.props import BoolProperty 5 | 6 | 7 | class IOPS_OT_Mesh_QuickSnap(bpy.types.Operator): 8 | """Quick Snap point to point""" 9 | 10 | bl_idname = "iops.mesh_quick_snap" 11 | bl_label = "IOPS Quick Snap" 12 | bl_options = {"REGISTER", "UNDO"} 13 | 14 | quick_snap_surface: BoolProperty( 15 | name="Surface snap", description="ON/Off", default=False 16 | ) 17 | # @classmethod 18 | # def poll(cls, context): 19 | # return (and context.area.type == "VIEW_3D") 20 | 21 | def execute(self, context): 22 | if context.mode == "EDIT_MESH": 23 | scene = context.scene 24 | 25 | edit_obj = context.active_object 26 | me = edit_obj.data 27 | target_points = [] 28 | # GET INDEXES 29 | bm = bmesh.from_edit_mesh(me) 30 | selected_verts_index = [] 31 | for v in bm.verts: 32 | if v.select: 33 | selected_verts_index.append(v.index) 34 | bpy.ops.object.editmode_toggle() 35 | bm.free() 36 | bpy.ops.object.editmode_toggle() 37 | 38 | # GET SCENE OBJECTS 39 | mesh_objects = [ 40 | o 41 | for o in scene.objects 42 | if o.type == "MESH" 43 | and o.data.polygons[:] != [] 44 | and o.visible_get() 45 | and o.modifiers[:] == [] 46 | ] 47 | bm = bmesh.new() 48 | for ob in mesh_objects: 49 | if ob == edit_obj: 50 | continue 51 | ob_mw_i = ob.matrix_world.inverted() 52 | 53 | bm.from_mesh(me) 54 | bm.verts.ensure_lookup_table() 55 | for ind in selected_verts_index: 56 | vert = bm.verts[ind] 57 | vert_co = edit_obj.matrix_world @ vert.co 58 | local_pos = ob_mw_i @ vert_co 59 | (hit, loc, norm, face_index) = ob.closest_point_on_mesh(local_pos) 60 | if hit: 61 | bm.verts.ensure_lookup_table() 62 | bm.faces.ensure_lookup_table() 63 | v_dists = {} 64 | if self.quick_snap_surface: 65 | target_co = ob.matrix_world @ loc 66 | v_dist = (target_co - vert_co).length 67 | min_co = target_co 68 | min_len = v_dist 69 | else: 70 | for v in ob.data.polygons[face_index].vertices: 71 | target_co = ob.matrix_world @ ob.data.vertices[v].co 72 | v_dist = (target_co - vert_co).length 73 | v_dists[v] = {} 74 | v_dists[v]["co"] = (*target_co,) 75 | v_dists[v]["len"] = v_dist 76 | 77 | lens = [v_dists[idx]["len"] for idx in v_dists] 78 | for k in v_dists.values(): 79 | if k["len"] == min(lens): 80 | min_co = Vector((k["co"])) 81 | min_len = k["len"] 82 | 83 | if target_points: 84 | if len(target_points) != len(selected_verts_index): 85 | target_points.append([ind, min_co, min_len]) 86 | else: 87 | for p in target_points: 88 | if p[0] == ind and p[2] >= min_len: 89 | p[1] = min_co 90 | p[2] = min_len 91 | else: 92 | target_points.append([ind, min_co, min_len]) 93 | bm.clear() 94 | 95 | bm = bmesh.from_edit_mesh(me) 96 | bm.verts.ensure_lookup_table() 97 | for p in target_points: 98 | bm.verts[p[0]].co = edit_obj.matrix_world.inverted() @ p[1] 99 | bmesh.update_edit_mesh(me) 100 | 101 | self.report({"INFO"}, "POINTS ARE SNAPPED!!!") 102 | else: 103 | self.report({"WARNING"}, "ENTER TO MESH EDIT MODE!!!") 104 | return {"FINISHED"} 105 | -------------------------------------------------------------------------------- /operators/mesh_to_grid.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from bpy.props import FloatProperty 4 | 5 | 6 | class IOPS_OT_mesh_to_grid(bpy.types.Operator): 7 | """Gridify vertex position""" 8 | 9 | bl_idname = "iops.mesh_to_grid" 10 | bl_label = "IOPS mesh_to_grid" 11 | bl_options = {"REGISTER", "UNDO"} 12 | 13 | base: FloatProperty( 14 | name="Base", 15 | description="Nearest grid number in scene units (0.01 = 1cm, 10 = 10m)", 16 | default=0.01, 17 | soft_min=0.01, 18 | soft_max=10, 19 | ) 20 | 21 | @classmethod 22 | def poll(cls, context): 23 | return context.mode == "EDIT_MESH" and context.area.type == "VIEW_3D" 24 | 25 | def round_to_base(self, coord, base): 26 | return base * round(coord / base) 27 | 28 | def execute(self, context): 29 | dg = bpy.context.evaluated_depsgraph_get() 30 | ob = context.view_layer.objects.active 31 | bm = bmesh.from_edit_mesh(ob.data) 32 | 33 | for v in bm.verts: 34 | pos_x = self.round_to_base(v.co[0], self.base) 35 | pos_y = self.round_to_base(v.co[1], self.base) 36 | pos_z = self.round_to_base(v.co[2], self.base) 37 | 38 | v.co = (pos_x, pos_y, pos_z) 39 | 40 | bmesh.update_edit_mesh(ob.data) 41 | dg.update() 42 | self.report({"INFO"}, "Vertices snapped to grid") 43 | return {"FINISHED"} 44 | -------------------------------------------------------------------------------- /operators/mesh_uv_channel_hop.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | 4 | from bpy.props import ( 5 | BoolProperty, 6 | ) 7 | 8 | 9 | class IOPS_OT_Mesh_UV_Channel_Hop(bpy.types.Operator): 10 | """Cyclic switching uv channels and uv seams on active object""" 11 | 12 | bl_idname = "iops.mesh_uv_channel_hop" 13 | bl_label = "Object UV Channel Hop" 14 | bl_options = {"REGISTER", "UNDO"} 15 | 16 | mark_seam: BoolProperty( 17 | name="Mark Seams", description="Mark seams by UV Islands", default=True 18 | ) 19 | hop_previous: BoolProperty( 20 | name="Hop to Previous", description="Hop to previous UV Channel", default=False 21 | ) 22 | set_render: BoolProperty( 23 | name="Set Render Channel", description="Set Render Channel", default=True 24 | ) 25 | 26 | @classmethod 27 | def poll(cls, context): 28 | return ( 29 | context.area.type == "VIEW_3D" 30 | and context.active_object 31 | and context.active_object.type == "MESH" 32 | and context.active_object.mode == "EDIT" 33 | ) 34 | 35 | def execute(self, context): 36 | # Switch UV Channel by modulo of uv_layers length and set render channel 37 | ob = context.active_object 38 | me = ob.data 39 | # save curentrly selected faces, deselect all faces, select all faces 40 | bm = bmesh.from_edit_mesh(me) 41 | # list of selected faces 42 | selected_faces = [f for f in bm.faces if f.select] 43 | # Deselect all and select all faces 44 | bpy.ops.mesh.select_all(action="DESELECT") 45 | bpy.ops.mesh.select_all(action="SELECT") 46 | # switch uv channel 47 | if self.hop_previous: 48 | ob.data.uv_layers.active_index = (ob.data.uv_layers.active_index - 1) % len( 49 | ob.data.uv_layers 50 | ) 51 | else: 52 | ob.data.uv_layers.active_index = (ob.data.uv_layers.active_index + 1) % len( 53 | ob.data.uv_layers 54 | ) 55 | # clear seams and mark seams by UV Islands 56 | if self.mark_seam: 57 | bpy.ops.mesh.mark_seam(clear=True) 58 | bpy.ops.uv.select_all(action="SELECT") 59 | bpy.ops.uv.seams_from_islands(mark_seams=True) 60 | # set active render channel 61 | if self.set_render: 62 | ob.data.uv_layers[ob.data.uv_layers.active_index].active_render = True 63 | # Deselect all and select previously selected faces 64 | bpy.ops.mesh.select_all(action="DESELECT") 65 | # Restore selection from selected_faces list 66 | if selected_faces: 67 | for f in selected_faces: 68 | f.select = True 69 | bmesh.update_edit_mesh(me) 70 | # Get active render channel 71 | active_render = "" 72 | for uv_layer in ob.data.uv_layers: 73 | if uv_layer.active_render: 74 | active_render = uv_layer.name 75 | # Print report 76 | self.report( 77 | {"INFO"}, 78 | f"UV Active Channel: {ob.data.uv_layers.active.name}, UV Active Render: {active_render}", 79 | ) 80 | return {"FINISHED"} 81 | -------------------------------------------------------------------------------- /operators/modes.py: -------------------------------------------------------------------------------- 1 | from .iops import IOPS_OT_Main 2 | 3 | 4 | class IOPS_OT_F1(IOPS_OT_Main): 5 | bl_idname = "iops.function_f1" 6 | bl_label = "IOPS OPERATOR F1" 7 | operator = "F1" 8 | 9 | 10 | class IOPS_OT_F2(IOPS_OT_Main): 11 | bl_idname = "iops.function_f2" 12 | bl_label = "IOPS OPERATOR F2" 13 | operator = "F2" 14 | 15 | 16 | class IOPS_OT_F3(IOPS_OT_Main): 17 | bl_idname = "iops.function_f3" 18 | bl_label = "IOPS OPERATOR F3" 19 | operator = "F3" 20 | 21 | 22 | class IOPS_OT_F4(IOPS_OT_Main): 23 | bl_idname = "iops.function_f4" 24 | bl_label = "IOPS OPERATOR F4" 25 | operator = "F4" 26 | 27 | 28 | class IOPS_OT_F5(IOPS_OT_Main): 29 | bl_idname = "iops.function_f5" 30 | bl_label = "IOPS OPERATOR F5" 31 | operator = "F5" 32 | 33 | 34 | class IOPS_OT_ESC(IOPS_OT_Main): 35 | bl_idname = "iops.function_esc" 36 | bl_label = "IOPS OPERATOR ESC" 37 | operator = "ESC" 38 | -------------------------------------------------------------------------------- /operators/mouseover_fill_select.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class IOPS_MouseoverFillSelect(bpy.types.Operator): 5 | """Fill select faces at mouseover""" 6 | 7 | bl_idname = "iops.mesh_mouseover_fill_select" 8 | bl_label = "IOPS Mouseover Fill Select" 9 | bl_options = {"REGISTER", "UNDO"} 10 | 11 | @classmethod 12 | def poll(cls, context): 13 | return ( 14 | context.object is not None 15 | and context.object.type == "MESH" 16 | and context.object.data.is_editmode 17 | ) 18 | 19 | def invoke(self, context, event): 20 | loc = event.mouse_region_x, event.mouse_region_y 21 | bpy.ops.mesh.hide(unselected=False) 22 | bpy.ops.view3d.select(extend=True, location=loc) 23 | bpy.ops.mesh.select_linked(delimit={"NORMAL"}) 24 | bpy.ops.mesh.reveal(select=True) 25 | return {"FINISHED"} 26 | -------------------------------------------------------------------------------- /operators/object_auto_smooth.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | from math import radians 4 | 5 | # Check box to put the modifier at the top of the stack or bottom 6 | 7 | # def move_modifier_to_top(obj, mod): 8 | # if obj.modifiers: 9 | # while obj.modifiers[0] != mod: 10 | # bpy.ops.object.modifier_move_up(modifier=mod.name) 11 | 12 | 13 | class IOPS_OT_AutoSmooth(bpy.types.Operator): 14 | bl_idname = "iops.object_auto_smooth" 15 | bl_description = "Add Auto Smooth modifier to selected objects" 16 | bl_label = "Add Auto Smooth Modifier" 17 | bl_options = {"REGISTER", "UNDO"} 18 | 19 | angle: bpy.props.FloatProperty( 20 | name="Smooth Angle", 21 | description="Smooth Angle", 22 | default=30.0, 23 | min=0.0, 24 | max=180.0, 25 | ) 26 | 27 | ignore_sharp: bpy.props.BoolProperty( 28 | name="Ignore Sharp Edges", 29 | description="Ignore Sharp Edges", 30 | default=False, 31 | ) 32 | 33 | stack_top: bpy.props.BoolProperty( 34 | name="Top of Stack", 35 | description="Add modifier to top of stack", 36 | default=True, 37 | ) 38 | 39 | @classmethod 40 | def poll(self, context): 41 | # True if any of the selected objects are meshes 42 | return any(obj.type == "MESH" for obj in bpy.context.selected_objects) 43 | 44 | def invoke(self, context, event): 45 | # check if bpy.data.node_groups["Smooth by Angle"] exists, if not import it 46 | if "Smooth by Angle" not in bpy.data.node_groups.keys(): 47 | res_path = bpy.utils.resource_path("LOCAL") 48 | path = os.path.join( 49 | res_path, "datafiles\\assets\\geometry_nodes\\smooth_by_angle.blend" 50 | ) 51 | 52 | with bpy.data.libraries.load(path, link=True) as (data_from, data_to): 53 | data_to.node_groups = data_from.node_groups 54 | # print(f"Loaded {path}") 55 | return self.execute(context) 56 | 57 | def execute(self, context): 58 | if context.object.mode == 'OBJECT': 59 | bpy.ops.object.shade_smooth() 60 | else: 61 | bpy.ops.object.mode_set(mode='OBJECT') 62 | bpy.ops.object.shade_smooth() 63 | bpy.ops.object.mode_set(mode='EDIT') 64 | count = 1 65 | meshes = [obj for obj in bpy.context.selected_objects if obj.type == "MESH"] 66 | for mesh in meshes: 67 | auto_smooth_exists = False 68 | # Only change parameters if existing Auto Smooth has node_groups["Smooth by Angle"] 69 | for mod in mesh.modifiers: 70 | if ( 71 | "Auto Smooth" in mod.name 72 | and mod.type == "NODES" 73 | and getattr(mod.node_group, "name", None) == "Smooth by Angle" 74 | ): 75 | print( 76 | f"Auto Smooth modifier exists on {mesh.name}. Changing parameters." 77 | ) 78 | mod["Input_1"] = radians(self.angle) 79 | mod["Socket_1"] = self.ignore_sharp 80 | # redraw ui 81 | areas = [ 82 | area 83 | for area in bpy.context.screen.areas 84 | if area.type == "PROPERTIES" 85 | ] 86 | if areas: 87 | for area in areas: 88 | with bpy.context.temp_override(): 89 | area.tag_redraw() 90 | auto_smooth_exists = True 91 | break 92 | 93 | if auto_smooth_exists: 94 | continue 95 | 96 | if getattr(mesh, "auto_smooth_modifier", None) == "Auto Smooth": 97 | mesh.auto_smooth_modifier = "Auto Smooth" 98 | print( 99 | f"Adding Auto Smooth modifier to {mesh.name}, {count} of {len(bpy.context.selected_objects)}" 100 | ) 101 | 102 | count += 1 103 | 104 | with bpy.context.temp_override(object=mesh, active_object=mesh, selected_objects=[mesh]): 105 | # Shade Smooth 106 | # bpy.ops.object.shade_smooth() 107 | # Delete existing Auto Smooth modifier 108 | for mod in mesh.modifiers: 109 | if ( 110 | "Auto Smooth" in mod.name 111 | and mod.type == "NODES" 112 | and getattr(mod.node_group, "name", None) == "Auto Smooth" 113 | ): 114 | try: 115 | bpy.ops.object.modifier_remove(modifier=mod.name) 116 | except Exception as e: 117 | print( 118 | f"Could not remove Auto Smooth modifier from {mesh.name} — {e}" 119 | ) 120 | break 121 | 122 | # Add Smooth by Angle modifier from Essentials library 123 | try: 124 | if "Auto Smooth" not in [mod.name for mod in mesh.modifiers]: 125 | bpy.ops.object.modifier_add_node_group( 126 | asset_library_type="ESSENTIALS", 127 | asset_library_identifier="", 128 | relative_asset_identifier="geometry_nodes/smooth_by_angle.blend/NodeTree/Smooth by Angle", 129 | ) 130 | for _mod in mesh.modifiers: 131 | if _mod.type == "NODES" and "Smooth by Angle" in _mod.name: 132 | _mod.name = "Auto Smooth" 133 | _mod["Input_1"] = radians(self.angle) 134 | _mod["Socket_1"] = self.ignore_sharp 135 | _mod.name = "Auto Smooth" 136 | mod = _mod 137 | 138 | # if mod.type == "NODES": 139 | # mod.node_group = bpy.data.node_groups["Smooth by Angle"] 140 | # mod["Input_1"] = radians(self.angle) 141 | # mod["Socket_1"] = self.ignore_sharp 142 | 143 | else: 144 | mod = mesh.modifiers["Auto Smooth"] 145 | except Exception as e: 146 | print(f"Could not add Auto Smooth modifier to {mesh.name} — {e}") 147 | continue 148 | mod.show_viewport = False 149 | mod.show_viewport = True 150 | if self.stack_top: 151 | bpy.ops.object.modifier_move_to_index(modifier=mod.name, index=0) 152 | 153 | else: 154 | bpy.ops.object.modifier_move_to_index( 155 | modifier=mod.name, index=len(mesh.modifiers) - 1 156 | ) 157 | 158 | return {"FINISHED"} 159 | 160 | 161 | class IOPS_OT_ClearCustomNormals(bpy.types.Operator): 162 | bl_idname = "iops.object_clear_normals" 163 | bl_description = "Remove custom normals from selected objects" 164 | bl_label = "Clear Custom Normals" 165 | bl_options = {"REGISTER", "UNDO"} 166 | 167 | @classmethod 168 | def poll(self, context): 169 | # True if any of the selected objects are meshes and have custom normals (obj.data.has_custom_normals) 170 | return any( 171 | obj.type == "MESH" and getattr(obj.data, "has_custom_normals", False) 172 | for obj in bpy.context.selected_objects 173 | ) 174 | 175 | def execute(self, context): 176 | count = 1 177 | for obj in bpy.context.selected_objects: 178 | print( 179 | f"Clearing custom normals from {obj.name}, {count} of {len(bpy.context.selected_objects)}" 180 | ) 181 | count += 1 182 | with bpy.context.temp_override(object=obj): 183 | if obj.type == "MESH": 184 | bpy.ops.mesh.customdata_custom_splitnormals_clear() 185 | return {"FINISHED"} 186 | -------------------------------------------------------------------------------- /operators/object_match_transform_active.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class IOPS_OT_MatchTransformActive(bpy.types.Operator): 5 | """Match dimensions of selected object to active""" 6 | 7 | bl_idname = "iops.object_match_transform_active" 8 | bl_label = "Match dimensions" 9 | bl_options = {"REGISTER", "UNDO"} 10 | 11 | @classmethod 12 | def poll(self, context): 13 | return ( 14 | context.area.type == "VIEW_3D" 15 | and context.mode == "OBJECT" 16 | and len(context.view_layer.objects.selected) != 0 17 | and context.view_layer.objects.active.type == "MESH" 18 | ) 19 | 20 | def execute(self, context): 21 | selection = context.view_layer.objects.selected 22 | active = context.view_layer.objects.active 23 | 24 | for ob in selection: 25 | ob.dimensions = active.dimensions 26 | 27 | self.report({"INFO"}, "Dimensions matched") 28 | 29 | return {"FINISHED"} 30 | -------------------------------------------------------------------------------- /operators/object_name_from_active.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import re 3 | from bpy.props import ( 4 | IntProperty, 5 | StringProperty, 6 | BoolProperty, 7 | ) 8 | from mathutils import Vector 9 | 10 | 11 | def distance_vec(point1: Vector, point2: Vector): 12 | """Calculate distance between two points.""" 13 | return (point2 - point1).length 14 | 15 | 16 | class IOPS_OT_Object_Name_From_Active(bpy.types.Operator): 17 | """Rename Object as Active ObjectName""" 18 | 19 | bl_idname = "iops.object_name_from_active" 20 | bl_label = "IOPS Object Name From Active" 21 | bl_options = {"REGISTER", "UNDO"} 22 | 23 | new_name: StringProperty( 24 | name="New Name", 25 | default="", 26 | ) 27 | 28 | active_name: StringProperty( 29 | name="Active Name", 30 | default="", 31 | ) 32 | 33 | pattern: StringProperty( 34 | name="Pattern", 35 | description="""Naming Syntaxis: 36 | [N] - Name 37 | [C] - Counter 38 | [T] - Object Type 39 | [COL] - Collection Name 40 | """, 41 | default="[N]_[C]", 42 | ) 43 | 44 | use_distance: BoolProperty( 45 | name="By Distance", 46 | description="Rename Selected Objects Based on Distance to Active Object", 47 | default=True, 48 | ) 49 | 50 | counter_digits: IntProperty( 51 | name="Counter Digits", 52 | description="Number Of Digits For Counter", 53 | default=2, 54 | min=2, 55 | max=10, 56 | ) 57 | 58 | counter_shift: BoolProperty( 59 | name="+1", 60 | description="+1 shift for counter, useful when we need to rename active object too", 61 | default=True, 62 | ) 63 | 64 | rename_active: BoolProperty( 65 | name="Rename Active", description="Rename active also", default=True 66 | ) 67 | 68 | rename_mesh_data: BoolProperty( 69 | name="Rename Mesh Data", description="Rename Mesh Data", default=True 70 | ) 71 | 72 | trim_prefix: IntProperty( 73 | name="Prefix", 74 | description="Number Of Digits for Prefix trim", 75 | default=0, 76 | min=0, 77 | max=100, 78 | ) 79 | 80 | trim_suffix: IntProperty( 81 | name="Suffix", 82 | description="Number Of Digits for Suffix trim", 83 | default=0, 84 | min=0, 85 | max=100, 86 | ) 87 | use_trim: BoolProperty( 88 | name="Trim", description="Trim Name Prefix/Suffix", default=False 89 | ) 90 | rename_linked: BoolProperty( 91 | name="Rename Linked", 92 | description="Rename Linked Objects", 93 | default=False, 94 | ) 95 | 96 | def invoke(self, context, event): 97 | self.active_name = context.view_layer.objects.active.name 98 | self.new_name = self.active_name 99 | return self.execute(context) 100 | 101 | def execute(self, context): 102 | if self.pattern: 103 | if self.active_name != context.view_layer.objects.active.name: 104 | context.view_layer.objects.active.name = self.active_name 105 | # Trim string 106 | if self.use_trim: 107 | name = self.active_name 108 | if self.trim_suffix == 0: 109 | self.new_name = name[(self.trim_prefix) :] 110 | else: 111 | self.new_name = name[(self.trim_prefix) : -(self.trim_suffix)] 112 | else: 113 | self.trim_suffix = self.trim_prefix = 0 114 | 115 | digit = "{0:0>" + str(self.counter_digits) + "}" 116 | # Combine objects 117 | active = bpy.context.view_layer.objects.active 118 | Objects = bpy.context.selected_objects 119 | if active: 120 | active_collection = active.users_collection[0] 121 | else: 122 | active_collection = "" 123 | to_rename = [] 124 | if self.use_distance: 125 | Objects.sort(key=lambda obj: (obj.location - active.location).length) 126 | # Check active 127 | if self.rename_active: 128 | to_rename = [ob.name for ob in Objects] 129 | else: 130 | to_rename = [ob.name for ob in Objects if ob is not active] 131 | # counter 132 | counter = 0 133 | if self.counter_shift: 134 | counter = 1 135 | 136 | if self.rename_linked: 137 | if active.children_recursive: 138 | to_rename.extend([child.name for child in active.children_recursive]) 139 | 140 | 141 | for name in to_rename: 142 | o = bpy.data.objects[name] 143 | pattern = re.split(r"(\[\w+\])", self.pattern) 144 | # i - index, p - pattern 145 | for i, p in enumerate(pattern): 146 | if p == "[N]": 147 | pattern[i] = self.new_name 148 | if p == "[C]": 149 | pattern[i] = digit.format(counter) 150 | if p == "[T]": 151 | pattern[i] = o.type.lower() 152 | if p == "[COL]": 153 | pattern[i] = active_collection.name 154 | o.name = "".join(pattern) 155 | # Rename object mesh data 156 | if self.rename_mesh_data: 157 | if o.type == "MESH": 158 | o.data.name = o.name 159 | counter += 1 160 | else: 161 | self.report({"ERROR"}, "Please fill the pattern field") 162 | return {"FINISHED"} 163 | 164 | def draw(self, context): 165 | layout = self.layout 166 | col_active_name = layout.column(align=True) 167 | col_active_name.enabled = False 168 | col_active_name.prop(self, "active_name", text="Old Name") 169 | 170 | col = layout.column(align=True) 171 | col.prop(self, "new_name", text="New Name") 172 | col.separator() 173 | col = layout.column(align=True) 174 | # Trim 175 | row = col.row(align=True) 176 | sub = row.row(align=True) 177 | sub.enabled = self.use_trim 178 | sub.prop(self, "trim_prefix") 179 | row.prop(self, "use_trim", toggle=True) 180 | sub = row.row(align=True) 181 | sub.enabled = self.use_trim 182 | sub.prop(self, "trim_suffix") 183 | # Pattern 184 | col = layout.column(align=True) 185 | col.separator() 186 | col.prop(self, "pattern") 187 | col.separator() 188 | row = col.row(align=True) 189 | row.label(text="Counter Digits:") 190 | row.alignment = "LEFT" 191 | row.prop(self, "counter_digits", text=" ") 192 | row.separator(factor=1.0) 193 | row.prop(self, "counter_shift") 194 | col = layout.column(align=True) 195 | col.label(text="Rename:") 196 | 197 | col.prop(self, "rename_active", text="Active Object") 198 | col.prop(self, "use_distance", text="By Distance") 199 | col.prop(self, "rename_mesh_data", text="Object's MeshData") 200 | col.prop(self, "rename_linked", text="Linked Objects") 201 | -------------------------------------------------------------------------------- /operators/object_normalize.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | from mathutils import Vector 4 | from bpy.props import BoolProperty, IntProperty 5 | 6 | 7 | class IOPS_OT_object_normalize(bpy.types.Operator): 8 | """Normalize location,Rotation,Scale,Dimensions values""" 9 | 10 | bl_idname = "iops.object_normalize" 11 | bl_label = "IOPS object normalize" 12 | bl_options = {"REGISTER", "UNDO"} 13 | 14 | precision: IntProperty( 15 | name="Precision", 16 | description="Digits after point", 17 | default=2, 18 | soft_min=0, 19 | soft_max=10, 20 | ) 21 | location: BoolProperty( 22 | name="Trim location", description="Trim location values", default=True 23 | ) 24 | rotation: BoolProperty( 25 | name="Trim rotation", description="Trim rotation values", default=True 26 | ) 27 | 28 | dimensions: BoolProperty( 29 | name="Trim dimentsion", description="Trim dimentsion values", default=True 30 | ) 31 | 32 | @classmethod 33 | def poll(self, context): 34 | return ( 35 | context.area.type == "VIEW_3D" 36 | and context.mode == "OBJECT" 37 | and len(context.view_layer.objects.selected) != 0 38 | and ( 39 | context.view_layer.objects.active.type == "MESH" 40 | or context.view_layer.objects.active.type == "EMPTY" 41 | ) 42 | ) 43 | 44 | def execute(self, context): 45 | selection = context.view_layer.objects.selected 46 | dg = bpy.context.evaluated_depsgraph_get() 47 | for ob in selection: 48 | if self.location: 49 | pos_x = round(ob.location.x, self.precision) 50 | pos_y = round(ob.location.y, self.precision) 51 | pos_z = round(ob.location.z, self.precision) 52 | ob.location.x = pos_x 53 | ob.location.y = pos_y 54 | ob.location.z = pos_z 55 | 56 | if self.rotation: 57 | rot_x = round(math.degrees(ob.rotation_euler.x), self.precision) 58 | rot_y = round(math.degrees(ob.rotation_euler.y), self.precision) 59 | rot_z = round(math.degrees(ob.rotation_euler.z), self.precision) 60 | ob.rotation_euler.x = math.radians(rot_x) 61 | ob.rotation_euler.y = math.radians(rot_y) 62 | ob.rotation_euler.z = math.radians(rot_z) 63 | 64 | if self.dimensions: 65 | dim_x = round(ob.dimensions.x, self.precision) 66 | dim_y = round(ob.dimensions.y, self.precision) 67 | dim_z = round(ob.dimensions.z, self.precision) 68 | ob.dimensions = Vector((dim_x, dim_y, dim_z)) 69 | dg.update() 70 | self.report({"INFO"}, "Object dimensions normalized") 71 | return {"FINISHED"} 72 | -------------------------------------------------------------------------------- /operators/object_replace.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import random 3 | from bpy.props import ( 4 | BoolProperty, 5 | ) 6 | from ..utils.functions import get_active_and_selected 7 | 8 | 9 | def get_random_obj_from_list(list): 10 | if len(list) != 0: 11 | obj = random.choice(list) 12 | return obj 13 | 14 | 15 | class IOPS_OT_Object_Replace(bpy.types.Operator): 16 | """Replace objects with active""" 17 | 18 | bl_idname = "iops.object_replace" 19 | bl_label = "IOPS Object Replace" 20 | bl_options = {"REGISTER", "UNDO"} 21 | 22 | # use_active_collection: BoolProperty( 23 | # name="Active Collection", 24 | # description="Use Active Collection", 25 | # default=False 26 | # ) 27 | replace: BoolProperty( 28 | name="Add/Replace", 29 | description="Enabled = Replace, Disabled = Add", 30 | default=True, 31 | ) 32 | select_replaced: BoolProperty( 33 | name="Select Replaced", 34 | description="Enabled = Select Replaced Objects, Disabled = Keep Selection", 35 | default=True, 36 | ) 37 | 38 | keep_rotation: BoolProperty( 39 | name="Keep Rotation", 40 | description="Enabled = Use Active Object Rotation, Disabled = Use Selected Object Rotation", 41 | default=False, 42 | ) 43 | 44 | keep_scale: BoolProperty( 45 | name="Keep Scale", 46 | description="Enabled = Use Active Object Scale, Disabled = Use Selected Object Scale", 47 | default=False, 48 | ) 49 | 50 | keep_active_object_collection: BoolProperty( 51 | name="Keep Active Object Collection", 52 | description="Enabled = Use Active Object Collection, Disabled = Object Replace Collection", 53 | default=True, 54 | ) 55 | 56 | keep_object_collection: BoolProperty( 57 | name="Keep Object Collection", 58 | description="Enabled = Use Selected Object Collection, Disabled = Object Replace Collection. Keep in mind that this option will override the Keep Active Object Collection option.", 59 | default=True, 60 | ) 61 | 62 | def execute(self, context): 63 | active, objects = get_active_and_selected() 64 | if active and objects: 65 | 66 | if self.keep_active_object_collection: 67 | collection = active.users_collection[0] 68 | else: 69 | collection = bpy.data.collections.new("Object Replace") 70 | bpy.context.scene.collection.children.link(collection) 71 | 72 | new_objects = [] 73 | for ob in objects: 74 | if self.keep_object_collection: 75 | collection = ob.users_collection[0] 76 | new_ob = active.copy() 77 | if active.type == "MESH": 78 | new_ob.data = active.data.copy() 79 | # position 80 | new_ob.location = ob.location 81 | # scale 82 | if self.keep_scale: 83 | new_ob.scale = active.scale 84 | else: 85 | new_ob.scale = ob.scale 86 | # rotation 87 | if self.keep_rotation: 88 | new_ob.rotation_euler = active.rotation_euler 89 | else: 90 | new_ob.rotation_euler = ob.rotation_euler 91 | 92 | collection.objects.link(new_ob) 93 | new_ob.select_set(False) 94 | new_objects.append(new_ob) 95 | 96 | if self.select_replaced: 97 | active.select_set(False) 98 | for ob in objects: 99 | ob.select_set(False) 100 | for ob in new_objects: 101 | ob.select_set(True) 102 | bpy.context.view_layer.objects.active = new_objects[-1] 103 | 104 | if self.replace: 105 | for ob in objects: 106 | bpy.data.objects.remove( 107 | ob, do_unlink=True, do_id_user=True, do_ui_user=True 108 | ) 109 | self.report({"INFO"}, "Objects Were Replaced") 110 | else: 111 | self.report({"ERROR"}, "Please Select Objects") 112 | return {"FINISHED"} 113 | 114 | def draw(self, context): 115 | layout = self.layout 116 | col = layout.column(align=True) 117 | col.prop(self, "keep_active_object_collection") 118 | col.prop(self, "keep_object_collection") 119 | # col.prop(self, "use_active_collection") 120 | col.prop(self, "replace") 121 | col.prop(self, "select_replaced") 122 | col.separator() 123 | col.prop(self, "keep_rotation") 124 | col.prop(self, "keep_scale") 125 | -------------------------------------------------------------------------------- /operators/object_uvmaps_add_remove.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | 5 | def tag_redraw(context, space_type="VIEW_3D", region_type="WINDOW"): 6 | for window in context.window_manager.windows: 7 | for area in window.screen.areas: 8 | if area.spaces[0].type == space_type: 9 | for region in area.regions: 10 | if region.type == region_type: 11 | region.tag_redraw() 12 | 13 | def uvmap_add(obj): 14 | obj_uvmaps = obj.data.uv_layers 15 | ch_num_text = "ch" + str(len(obj_uvmaps) + 1) 16 | obj_uvmaps.new(name=ch_num_text, do_init=True) 17 | 18 | 19 | def uvmap_clean_by_name(obj, name): 20 | obj_uvmaps = obj.data.uv_layers 21 | if obj_uvmaps and name in obj_uvmaps: 22 | obj_uvmaps.remove(obj_uvmaps[name]) 23 | 24 | 25 | 26 | def active_uvmap_by_active(obj, index): 27 | obj_uvmaps = obj.data.uv_layers 28 | if len(obj_uvmaps) >= index + 1: 29 | obj_uvmaps.active_index = index 30 | 31 | 32 | 33 | class IOPS_OT_Add_UVMap(bpy.types.Operator): 34 | """Add UVMap to selected objects""" 35 | 36 | bl_idname = "iops.uv_add_uvmap" 37 | bl_label = "Add UVMap to selected objects" 38 | bl_options = {"REGISTER", "UNDO"} 39 | 40 | def execute(self, context): 41 | selected_objs = [ 42 | o for o in context.view_layer.objects.selected 43 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get() 44 | ] 45 | if selected_objs: 46 | #check for instances 47 | unique_objects = {} 48 | filtered_list = [] 49 | for ob in selected_objs: 50 | # Check if the object is an instance (linked data) 51 | ob_data= ob.data 52 | if ob_data not in unique_objects: 53 | unique_objects[ob_data] = ob 54 | filtered_list.append(ob) 55 | for ob in filtered_list: 56 | uvmap_add(ob) 57 | self.report({"INFO"}, "UVMaps Were Added") 58 | else: 59 | self.report({"ERROR"}, "Select MESH objects.") 60 | return {"FINISHED"} 61 | 62 | 63 | class IOPS_OT_Remove_UVMap_by_Active_Name(bpy.types.Operator): 64 | """Remove UVMap by Name of Active UVMap on selected objects""" 65 | 66 | bl_idname = "iops.uv_remove_uvmap_by_active_name" 67 | bl_label = "Remove UVMap by Name of Active UVMap on selected objects" 68 | bl_options = {"REGISTER", "UNDO"} 69 | 70 | def execute(self, context): 71 | selected_objs = [ 72 | o 73 | for o in context.view_layer.objects.selected 74 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get() 75 | ] 76 | if selected_objs and context.active_object in selected_objs: 77 | if context.active_object.data.uv_layers: 78 | uvmap_name = context.active_object.data.uv_layers.active.name 79 | for ob in selected_objs: 80 | if ob.data.uv_layers: 81 | uvmap_clean_by_name(ob, uvmap_name) 82 | tag_redraw(context) 83 | self.report({"INFO"}, ("UVMap %s Was Deleted" % (uvmap_name))) 84 | else: 85 | self.report({"ERROR"}, "Select MESH objects.") 86 | return {"FINISHED"} 87 | 88 | 89 | class IOPS_OT_Active_UVMap_by_Active(bpy.types.Operator): 90 | """Remove UVMap by Name of Active UVMap on selected objects""" 91 | 92 | bl_idname = "iops.uv_active_uvmap_by_active_object" 93 | bl_label = "Set Active UVMap as Active object active UVMap" 94 | bl_options = {"REGISTER", "UNDO"} 95 | 96 | def execute(self, context): 97 | selected_objs = [ 98 | o 99 | for o in context.view_layer.objects.selected 100 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get() 101 | ] 102 | if selected_objs and context.active_object in selected_objs: 103 | if context.active_object.data.uv_layers: 104 | uvmap_name = context.active_object.data.uv_layers.active.name 105 | uvmap_index = context.active_object.data.uv_layers.active_index 106 | for ob in selected_objs: 107 | if ob.data.uv_layers: 108 | active_uvmap_by_active(ob, uvmap_index) 109 | tag_redraw(context) 110 | self.report({"INFO"}, ("UVMap %s Active" % (uvmap_name))) 111 | else: 112 | self.report({"ERROR"}, "Select MESH objects.") 113 | return {"FINISHED"} 114 | -------------------------------------------------------------------------------- /operators/object_uvmaps_cleaner.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | # from mathutils import Vector, Matrix 4 | # from bpy.props import (BoolProperty, 5 | # EnumProperty, 6 | # FloatProperty, 7 | # IntProperty, 8 | # PointerProperty, 9 | # StringProperty, 10 | # FloatVectorProperty, 11 | # ) 12 | 13 | 14 | def tag_redraw(context, space_type="VIEW_3D", region_type="WINDOW"): 15 | for window in context.window_manager.windows: 16 | for area in window.screen.areas: 17 | if area.spaces[0].type == space_type: 18 | for region in area.regions: 19 | if region.type == region_type: 20 | region.tag_redraw() 21 | 22 | def uvmap_clean_by_index(obj, index): 23 | obj_uvmaps = obj.data.uv_layers 24 | if obj_uvmaps: 25 | ch_num = len(obj_uvmaps) 26 | for i in range(ch_num, index, -1): 27 | if i != 1: 28 | obj_uvmaps.remove(obj_uvmaps[i - 1]) 29 | else: 30 | obj_uvmaps.remove(obj_uvmaps[0]) 31 | 32 | 33 | class IOPS_OT_Clean_UVMap_0(bpy.types.Operator): 34 | """Clean all UVMaps on selected objects""" 35 | 36 | bl_idname = "iops.object_clean_uvmap_0" 37 | bl_label = "Remove All UVMaps" 38 | bl_options = {"REGISTER", "UNDO"} 39 | 40 | # @classmethod 41 | # def poll(cls, context): 42 | # return (context.area.type == "VIEW_3D") 43 | 44 | def execute(self, context): 45 | selected_objs = [ 46 | o 47 | for o in context.view_layer.objects.selected 48 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get() 49 | ] 50 | if selected_objs: 51 | for ob in selected_objs: 52 | uvmap_clean_by_index(ob, 0) 53 | tag_redraw(context) 54 | self.report({"INFO"}, "All UVMaps Were Removed") 55 | 56 | else: 57 | self.report({"ERROR"}, "Select MESH objects.") 58 | return {"FINISHED"} 59 | 60 | 61 | class IOPS_OT_Clean_UVMap_1(bpy.types.Operator): 62 | """Clean from UVMap #2 and up - on selected objects""" 63 | 64 | bl_idname = "iops.object_clean_uvmap_1" 65 | bl_label = "Remove UVMap 1" 66 | bl_options = {"REGISTER", "UNDO"} 67 | 68 | # @classmethod 69 | # def poll(cls, context): 70 | # return (context.area.type == "VIEW_3D") 71 | 72 | def execute(self, context): 73 | selected_objs = [ 74 | o 75 | for o in context.view_layer.objects.selected 76 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get() 77 | ] 78 | if selected_objs: 79 | for ob in selected_objs: 80 | uvmap_clean_by_index(ob, 1) 81 | tag_redraw(context) 82 | self.report({"INFO"}, "UVMaps 2 to 8 Removed") 83 | 84 | else: 85 | self.report({"ERROR"}, "Select MESH objects.") 86 | return {"FINISHED"} 87 | 88 | 89 | class IOPS_OT_Clean_UVMap_2(bpy.types.Operator): 90 | """Clean from UVMap #3 and up - on selected objects""" 91 | 92 | bl_idname = "iops.object_clean_uvmap_2" 93 | bl_label = "Remove UVMap 2" 94 | bl_options = {"REGISTER", "UNDO"} 95 | 96 | # @classmethod 97 | # def poll(cls, context): 98 | # return (context.area.type == "VIEW_3D") 99 | 100 | def execute(self, context): 101 | selected_objs = [ 102 | o 103 | for o in context.view_layer.objects.selected 104 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get() 105 | ] 106 | if selected_objs: 107 | for ob in selected_objs: 108 | uvmap_clean_by_index(ob, 2) 109 | tag_redraw(context) 110 | self.report({"INFO"}, "UVMaps 3 to 8 Removed") 111 | 112 | else: 113 | self.report({"ERROR"}, "Select MESH objects.") 114 | return {"FINISHED"} 115 | 116 | 117 | class IOPS_OT_Clean_UVMap_3(bpy.types.Operator): 118 | """Clean from UVMap #4 and up - on selected objects""" 119 | 120 | bl_idname = "iops.object_clean_uvmap_3" 121 | bl_label = "Remove UVMap 3" 122 | bl_options = {"REGISTER", "UNDO"} 123 | 124 | # @classmethod 125 | # def poll(cls, context): 126 | # return (context.area.type == "VIEW_3D") 127 | 128 | def execute(self, context): 129 | selected_objs = [ 130 | o 131 | for o in context.view_layer.objects.selected 132 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get() 133 | ] 134 | if selected_objs: 135 | for ob in selected_objs: 136 | uvmap_clean_by_index(ob, 3) 137 | tag_redraw(context) 138 | self.report({"INFO"}, "UVMaps 4 to 8 Removed") 139 | 140 | else: 141 | self.report({"ERROR"}, "Select MESH objects.") 142 | return {"FINISHED"} 143 | 144 | 145 | class IOPS_OT_Clean_UVMap_4(bpy.types.Operator): 146 | """Clean from UVMap #5 and up - on selected objects""" 147 | 148 | bl_idname = "iops.object_clean_uvmap_4" 149 | bl_label = "Remove UVMap 4" 150 | bl_options = {"REGISTER", "UNDO"} 151 | 152 | # @classmethod 153 | # def poll(cls, context): 154 | # return (context.area.type == "VIEW_3D") 155 | 156 | def execute(self, context): 157 | selected_objs = [ 158 | o 159 | for o in context.view_layer.objects.selected 160 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get() 161 | ] 162 | if selected_objs: 163 | for ob in selected_objs: 164 | uvmap_clean_by_index(ob, 4) 165 | tag_redraw(context) 166 | self.report({"INFO"}, "UVMaps 5 to 8 Removed") 167 | 168 | else: 169 | self.report({"ERROR"}, "Select MESH objects.") 170 | return {"FINISHED"} 171 | 172 | 173 | class IOPS_OT_Clean_UVMap_5(bpy.types.Operator): 174 | """Clean from UVMap #6 and up - on selected objects""" 175 | 176 | bl_idname = "iops.object_clean_uvmap_5" 177 | bl_label = "Remove UVMap 5" 178 | bl_options = {"REGISTER", "UNDO"} 179 | 180 | # @classmethod 181 | # def poll(cls, context): 182 | # return (context.area.type == "VIEW_3D") 183 | 184 | def execute(self, context): 185 | selected_objs = [ 186 | o 187 | for o in context.view_layer.objects.selected 188 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get() 189 | ] 190 | if selected_objs: 191 | for ob in selected_objs: 192 | uvmap_clean_by_index(ob, 5) 193 | tag_redraw(context) 194 | self.report({"INFO"}, "UVMaps 6 to 8 Removed") 195 | 196 | else: 197 | self.report({"ERROR"}, "Select MESH objects.") 198 | return {"FINISHED"} 199 | 200 | 201 | class IOPS_OT_Clean_UVMap_6(bpy.types.Operator): 202 | """Clean from UVMap #7 and up - on selected objects""" 203 | 204 | bl_idname = "iops.object_clean_uvmap_6" 205 | bl_label = "Remove UVMap 6" 206 | bl_options = {"REGISTER", "UNDO"} 207 | 208 | # @classmethod 209 | # def poll(cls, context): 210 | # return (context.area.type == "VIEW_3D") 211 | 212 | def execute(self, context): 213 | selected_objs = [ 214 | o 215 | for o in context.view_layer.objects.selected 216 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get() 217 | ] 218 | if selected_objs: 219 | for ob in selected_objs: 220 | uvmap_clean_by_index(ob, 6) 221 | tag_redraw(context) 222 | self.report({"INFO"}, "UVMaps 7 to 8 Removed") 223 | 224 | else: 225 | self.report({"ERROR"}, "Select MESH objects.") 226 | return {"FINISHED"} 227 | 228 | 229 | class IOPS_OT_Clean_UVMap_7(bpy.types.Operator): 230 | """Clean UVMap #8 - on selected objects""" 231 | 232 | bl_idname = "iops.object_clean_uvmap_7" 233 | bl_label = "Remove UVMap 7" 234 | bl_options = {"REGISTER", "UNDO"} 235 | 236 | # @classmethod 237 | # def poll(cls, context): 238 | # return (context.area.type == "VIEW_3D") 239 | 240 | def execute(self, context): 241 | selected_objs = [ 242 | o 243 | for o in context.view_layer.objects.selected 244 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get() 245 | ] 246 | if selected_objs: 247 | for ob in selected_objs: 248 | uvmap_clean_by_index(ob, 7) 249 | tag_redraw(context) 250 | self.report({"INFO"}, "UVMap 8 Removed") 251 | 252 | else: 253 | self.report({"ERROR"}, "Select MESH objects.") 254 | return {"FINISHED"} 255 | -------------------------------------------------------------------------------- /operators/open_asset_in_current_blender.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class IOPS_OT_OpenAssetInCurrentBlender(bpy.types.Operator): 5 | """Open asset in current Blender instance""" 6 | 7 | bl_idname = "iops.open_asset_in_current_blender" 8 | bl_label = "Open in Current Blender" 9 | bl_options = {"REGISTER"} 10 | 11 | 12 | @classmethod 13 | def poll(cls, context): 14 | asset = getattr(context, "asset", None) 15 | 16 | if not asset: 17 | cls.poll_message_set("No asset selected") 18 | return False 19 | if asset.local_id: 20 | cls.poll_message_set("Selected asset is contained in the current file") 21 | return False 22 | # This could become a built-in query, for now this is good enough. 23 | if asset.full_library_path.endswith(".asset.blend"): 24 | cls.poll_message_set( 25 | "Selected asset is contained in a file managed by the asset system, manual edits should be avoided", 26 | ) 27 | return False 28 | return True 29 | 30 | def execute(self, context): 31 | asset = context.asset 32 | 33 | if asset.local_id: 34 | self.report({'WARNING'}, "This asset is stored in the current blend file") 35 | return {'CANCELLED'} 36 | 37 | asset_lib_path = asset.full_library_path 38 | bpy.ops.wm.open_mainfile(filepath=asset_lib_path) 39 | 40 | return {'FINISHED'} 41 | -------------------------------------------------------------------------------- /operators/outliner_collection_ops.py: -------------------------------------------------------------------------------- 1 | # Include/Exclude collections from view layer 2 | 3 | import bpy 4 | 5 | 6 | def exclude_layer_col_by_name(layerColl, collName, exclude): 7 | found = None 8 | if layerColl.name == collName: 9 | layerColl.exclude = exclude 10 | return layerColl 11 | for layer in layerColl.children: 12 | found = exclude_layer_col_by_name(layer, collName, exclude) 13 | if found: 14 | found.exclude = exclude 15 | return found 16 | 17 | 18 | class IOPS_OT_Collections_Include(bpy.types.Operator): 19 | """Include collection and children in view layer""" 20 | 21 | bl_idname = "iops.collections_include" 22 | bl_label = "Include All" 23 | bl_description = "Include collection and children in view layer" 24 | bl_options = {"REGISTER", "UNDO"} 25 | 26 | def execute(self, context): 27 | selected_cols = [ 28 | col 29 | for col in bpy.context.selected_ids 30 | if isinstance(col, bpy.types.Collection) 31 | ] 32 | selected_cols_children = [] 33 | 34 | # Get all children from selected_cols withoud duplicates 35 | for col in selected_cols: 36 | for child in col.children_recursive: 37 | if child not in selected_cols_children: 38 | selected_cols_children.append(child) 39 | 40 | all_colls = selected_cols + selected_cols_children 41 | 42 | master_col = bpy.context.view_layer.layer_collection 43 | # Get layer collection from selected_cols names 44 | for col in all_colls: 45 | exclude_layer_col_by_name(master_col, col.name, False) 46 | 47 | return {"FINISHED"} 48 | 49 | 50 | class IOPS_OT_Collections_Exclude(bpy.types.Operator): 51 | """Exclude collection and children from view layer""" 52 | 53 | bl_idname = "iops.collections_exclude" 54 | bl_label = "Exclude All" 55 | bl_description = "Exclude collection and children from view layer" 56 | bl_options = {"REGISTER", "UNDO"} 57 | 58 | def execute(self, context): 59 | selected_cols = [ 60 | col 61 | for col in bpy.context.selected_ids 62 | if isinstance(col, bpy.types.Collection) 63 | ] 64 | selected_cols_children = [] 65 | 66 | # Get all children from selected_cols withoud duplicates 67 | for col in selected_cols: 68 | for child in col.children_recursive: 69 | if child not in selected_cols_children: 70 | selected_cols_children.append(child) 71 | 72 | all_colls = selected_cols + selected_cols_children 73 | 74 | master_col = bpy.context.view_layer.layer_collection 75 | # Get layer collection from selected_cols names 76 | for col in all_colls: 77 | exclude_layer_col_by_name(master_col, col.name, True) 78 | 79 | return {"FINISHED"} 80 | -------------------------------------------------------------------------------- /operators/preferences/io_addon_preferences.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | import json 4 | from ...prefs.iops_prefs import get_iops_prefs 5 | from ...utils.split_areas_dict import split_areas_dict, split_areas_position_list 6 | 7 | # Save Addon Preferences 8 | def save_iops_preferences(): 9 | iops_prefs = get_iops_prefs() 10 | path = bpy.utils.script_path_user() 11 | folder = os.path.join(path, "presets", "IOPS") 12 | iops_prefs_file = os.path.join(path, "presets", "IOPS", "iops_prefs_user.json") 13 | 14 | if not os.path.exists(folder): 15 | os.makedirs(folder) 16 | 17 | with open(iops_prefs_file, "w") as f: 18 | json.dump(iops_prefs, f, indent=4) 19 | 20 | 21 | def get_split_pos_ui(pos, ui): 22 | for p in split_areas_position_list: 23 | if p[0] == pos: 24 | pos_num = p[4] 25 | for key, value in split_areas_dict.items(): 26 | if value["ui"] == ui: 27 | ui_num = value["num"] 28 | return pos_num, ui_num 29 | 30 | 31 | # Load Addon Preferences 32 | def load_iops_preferences(): 33 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences 34 | path = bpy.utils.script_path_user() 35 | try: 36 | iops_prefs_file = os.path.join(path, "presets", "IOPS", "iops_prefs_user.json") 37 | with open(iops_prefs_file, "r") as f: 38 | iops_prefs = json.load(f) 39 | for key, value in iops_prefs.items(): 40 | match key: 41 | case "IOPS_DEBUG": 42 | prefs.IOPS_DEBUG = value["IOPS_DEBUG"] 43 | case "ALIGN_TO_EDGE": 44 | prefs.align_edge_color = value["align_edge_color"] 45 | case "EXECUTOR": 46 | prefs.executor_scripts_folder = value["executor_scripts_folder"] 47 | prefs.executor_column_count = value["executor_column_count"] 48 | prefs.executor_name_lenght = value["executor_name_lenght"] 49 | prefs.executor_use_script_path_user = value["executor_use_script_path_user"] 50 | prefs.executor_scripts_subfolder = value["executor_scripts_subfolder"] 51 | case "SPLIT_AREA_PIES": 52 | for pie in value: 53 | pie_num = pie[-1] 54 | pos, ui = get_split_pos_ui( 55 | value[pie][f"split_area_pie_{pie_num}_pos"], 56 | value[pie][f"split_area_pie_{pie_num}_ui"], 57 | ) 58 | prefs[f"split_area_pie_{pie_num}_factor"] = value[pie][ 59 | f"split_area_pie_{pie_num}_factor" 60 | ] 61 | prefs[f"split_area_pie_{pie_num}_pos"] = pos 62 | prefs[f"split_area_pie_{pie_num}_ui"] = ui 63 | case "UI_TEXT": 64 | prefs.text_color = value["text_color"] 65 | prefs.text_color_key = value["text_color_key"] 66 | prefs.text_pos_x = value["text_pos_x"] 67 | prefs.text_pos_y = value["text_pos_y"] 68 | prefs.text_shadow_color = value["text_shadow_color"] 69 | prefs.text_shadow_pos_x = value["text_shadow_pos_x"] 70 | prefs.text_shadow_pos_y = value["text_shadow_pos_y"] 71 | prefs.text_shadow_toggle = value["text_shadow_toggle"] 72 | prefs.text_size = value["text_size"] 73 | case "UI_TEXT_STAT": 74 | prefs.iops_stat = value["iops_stat"] 75 | prefs.text_color_stat = value["text_color_stat"] 76 | prefs.text_color_key_stat = value["text_color_key_stat"] 77 | prefs.text_color_error_stat = value["text_color_error_stat"] 78 | prefs.text_pos_x_stat = value["text_pos_x_stat"] 79 | prefs.text_pos_y_stat = value["text_pos_y_stat"] 80 | prefs.text_shadow_color_stat = value["text_shadow_color_stat"] 81 | prefs.text_shadow_pos_x_stat = value["text_shadow_pos_x_stat"] 82 | prefs.text_shadow_pos_y_stat = value["text_shadow_pos_y_stat"] 83 | prefs.text_shadow_toggle_stat = value["text_shadow_toggle_stat"] 84 | prefs.text_size_stat = value["text_size_stat"] 85 | prefs.text_column_offset_stat = value["text_column_offset_stat"] 86 | prefs.text_column_width_stat = value["text_column_width_stat"] 87 | case "VISUAL_ORIGIN": 88 | prefs.vo_cage_ap_color = value["vo_cage_ap_color"] 89 | prefs.vo_cage_ap_size = value["vo_cage_ap_size"] 90 | prefs.vo_cage_color = value["vo_cage_color"] 91 | prefs.vo_cage_p_size = value["vo_cage_p_size"] 92 | prefs.vo_cage_points_color = value["vo_cage_points_color"] 93 | prefs.vo_cage_line_thickness = value["vo_cage_line_thickness"] 94 | case "TEXTURE_TO_MATERIAL": 95 | prefs.texture_to_material_prefixes = value[ 96 | "texture_to_material_prefixes" 97 | ] 98 | prefs.texture_to_material_suffixes = value[ 99 | "texture_to_material_suffixes" 100 | ] 101 | case "SNAP_COMBOS": 102 | for i in range(1, 9): 103 | prefs[f"snap_combo_{i}"] = value[f"snap_combo_{i}"] 104 | case "DRAG_SNAP": 105 | prefs.drag_snap_line_thickness = value["drag_snap_line_thickness"] 106 | case _: 107 | print( 108 | "IOPS Prefs: No entry for " + key, 109 | ) 110 | 111 | except FileNotFoundError: 112 | save_iops_preferences() 113 | print("IOPS Preferences file was not found. A new one was created at:", iops_prefs_file) 114 | return 115 | 116 | 117 | class IOPS_OT_SaveAddonPreferences(bpy.types.Operator): 118 | bl_idname = "iops.save_addon_preferences" 119 | bl_label = "Save Addon Preferences" 120 | bl_options = {"REGISTER", "UNDO"} 121 | 122 | def execute(self, context): 123 | save_iops_preferences() 124 | print("IOPS Preferences were Saved.") 125 | return {"FINISHED"} 126 | 127 | 128 | class IOPS_OT_LoadAddonPreferences(bpy.types.Operator): 129 | bl_idname = "iops.load_addon_preferences" 130 | bl_label = "Load Addon Preferences" 131 | bl_options = {"REGISTER", "UNDO"} 132 | 133 | def execute(self, context): 134 | load_iops_preferences() 135 | print("IOPS Preferences were Loaded") 136 | return {"FINISHED"} 137 | -------------------------------------------------------------------------------- /operators/render_asset_thumbnail.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import FloatProperty, BoolProperty, EnumProperty 3 | import os 4 | 5 | 6 | def get_path(): 7 | return os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 8 | 9 | 10 | class IOPS_OT_RenderAssetThumbnail(bpy.types.Operator): 11 | bl_idname = "iops.assets_render_asset_thumbnail" 12 | bl_label = "Render Active Asset Thumbnail" 13 | bl_description = "Render Active Asset Thumbnail: Collection, Object, Material, Geometry Nodes" 14 | bl_options = {"REGISTER", "UNDO"} 15 | 16 | thumbnail_lens: FloatProperty(name="Thumbnail Lens", default=100) 17 | toggle_overlays: BoolProperty(name="Toggle Overlays", default=True) 18 | 19 | render_for: EnumProperty( 20 | name="Render for:", 21 | items=[ 22 | ("OBJECT", "Object", "Render the selected object"), 23 | ("COLLECTION", "Collection", "Render the selected collection"), 24 | ("MATERIAL", "Material", "Render the selected material"), 25 | ("GEOMETRY", "Geometry Nodes", "Render the selected geometry node"), 26 | 27 | ], 28 | default="COLLECTION", 29 | ) 30 | 31 | def render_viewport(self, context, filepath): 32 | resolution = ( 33 | context.scene.render.resolution_x, 34 | context.scene.render.resolution_y, 35 | ) 36 | file_format = context.scene.render.image_settings.file_format 37 | lens = context.space_data.lens 38 | show_overlays = context.space_data.overlay.show_overlays 39 | 40 | context.scene.render.resolution_x = 500 41 | context.scene.render.resolution_y = 500 42 | context.scene.render.image_settings.file_format = "JPEG" 43 | 44 | context.space_data.lens = self.thumbnail_lens 45 | 46 | if show_overlays and self.toggle_overlays: 47 | context.space_data.overlay.show_overlays = False 48 | 49 | bpy.ops.render.opengl() 50 | 51 | thumb = bpy.data.images.get("Render Result") 52 | 53 | if thumb: 54 | thumb.save_render(filepath=filepath) 55 | 56 | context.scene.render.resolution_x = resolution[0] 57 | context.scene.render.resolution_y = resolution[1] 58 | context.space_data.lens = lens 59 | 60 | context.scene.render.image_settings.file_format = file_format 61 | 62 | if show_overlays and self.toggle_overlays: 63 | context.space_data.overlay.show_overlays = True 64 | 65 | def execute(self, context): 66 | if self.render_for == "COLLECTION": 67 | active_collection = bpy.context.collection 68 | 69 | thumbpath = os.path.join(get_path(), "resources", "thumb.png") 70 | self.render_viewport(context, thumbpath) 71 | if os.path.exists(thumbpath): 72 | with context.temp_override(id=active_collection): 73 | bpy.ops.ed.lib_id_load_custom_preview(filepath=thumbpath) 74 | os.unlink(thumbpath) 75 | elif self.render_for == "OBJECT": 76 | active_object = bpy.context.object 77 | 78 | thumbpath = os.path.join(get_path(), "resources", "thumb.png") 79 | self.render_viewport(context, thumbpath) 80 | if os.path.exists(thumbpath): 81 | with context.temp_override(id=active_object): 82 | bpy.ops.ed.lib_id_load_custom_preview(filepath=thumbpath) 83 | os.unlink(thumbpath) 84 | elif self.render_for == "MATERIAL": 85 | active_object = bpy.context.object 86 | if active_object.type == "MESH": 87 | active_material = active_object.active_material 88 | 89 | thumbpath = os.path.join(get_path(), "resources", "thumb.png") 90 | self.render_viewport(context, thumbpath) 91 | if os.path.exists(thumbpath): 92 | try: 93 | with context.temp_override(id=active_material): 94 | bpy.ops.ed.lib_id_load_custom_preview(filepath=thumbpath) 95 | os.unlink(thumbpath) 96 | except RuntimeError: 97 | self.report({"ERROR"}, "Current object does not have a material marked as asset") 98 | elif self.render_for == "GEOMETRY": 99 | active_object = bpy.context.object 100 | if active_object.type == "MESH" and active_object.modifiers.active.type == "NODES": 101 | active_node = active_object.modifiers.active.node_group 102 | 103 | thumbpath = os.path.join(get_path(), "resources", "thumb.png") 104 | self.render_viewport(context, thumbpath) 105 | if os.path.exists(thumbpath): 106 | try: 107 | with context.temp_override(id=active_node): 108 | bpy.ops.ed.lib_id_load_custom_preview(filepath=thumbpath) 109 | os.unlink(thumbpath) 110 | except RuntimeError: 111 | self.report({"ERROR"}, "Current object does not have a node groups marked as asset") 112 | else: 113 | self.report({"ERROR"}, "Active object is not a mesh") 114 | 115 | return {"FINISHED"} 116 | -------------------------------------------------------------------------------- /operators/run_text.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def ContextOverride(): 5 | for window in bpy.context.window_manager.windows: 6 | screen = window.screen 7 | for area in screen.areas: 8 | if area.type == "TEXT_EDITOR": 9 | for region in area.regions: 10 | if region.type == "WINDOW": 11 | context_override = { 12 | "window": window, 13 | "screen": screen, 14 | "area": area, 15 | "region": region, 16 | "scene": bpy.context.scene, 17 | "edit_object": bpy.context.edit_object, 18 | "active_object": bpy.context.active_object, 19 | "selected_objects": bpy.context.selected_objects, 20 | } 21 | return context_override 22 | raise Exception("ERROR: TEXT_EDITOR not found!") 23 | 24 | 25 | class IOPS_OT_RunText(bpy.types.Operator): 26 | """Run Current Script in Text Editor""" 27 | 28 | bl_idname = "iops.scripts_run_text" 29 | bl_label = "IOPS Run Text" 30 | bl_options = {"REGISTER", "UNDO"} 31 | 32 | def execute(self, context): 33 | context_override = ContextOverride() 34 | with context.temp_override(**context_override): 35 | bpy.ops.text.run_script() 36 | return {"FINISHED"} 37 | -------------------------------------------------------------------------------- /operators/save_load_space_data.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def save_space_data(context): 5 | area_type = context.area.type 6 | sd = context.space_data 7 | 8 | if "IOPS" not in context.scene: 9 | context.scene["IOPS"] = {} 10 | 11 | if area_type not in context.scene["IOPS"]: 12 | context.scene["IOPS"][area_type] = {} 13 | 14 | if area_type == "OUTLINER": 15 | outliner = context.scene["IOPS"]["OUTLINER"] 16 | display_mode = context.space_data.display_mode 17 | outliner["display_mode"] = display_mode 18 | 19 | outliner["show_header"] = sd.show_region_header 20 | outliner["show_menus"] = context.area.show_menus 21 | 22 | if display_mode == "VIEW_LAYER": 23 | outliner["show_restrict_column_enable"] = sd.show_restrict_column_enable 24 | outliner["show_restrict_column_select"] = sd.show_restrict_column_select 25 | outliner["show_restrict_column_hide"] = sd.show_restrict_column_hide 26 | outliner["show_restrict_column_viewport"] = sd.show_restrict_column_viewport 27 | outliner["show_restrict_column_render"] = sd.show_restrict_column_render 28 | outliner["show_restrict_column_holdout"] = sd.show_restrict_column_holdout 29 | 30 | elif display_mode == "SCENES": 31 | outliner["show_restrict_column_select"] = sd.show_restrict_column_select 32 | outliner["show_restrict_column_hide"] = sd.show_restrict_column_hide 33 | outliner["show_restrict_column_viewport"] = sd.show_restrict_column_viewport 34 | outliner["show_restrict_column_render"] = sd.show_restrict_column_render 35 | 36 | if display_mode != "DATA_API": 37 | outliner["use_sort_alpha"] = sd.use_sort_alpha 38 | else: 39 | context.scene["IOPS"][area_type]["show_header"] = sd.show_region_header 40 | context.scene["IOPS"][area_type]["show_menus"] = context.area.show_menus 41 | 42 | 43 | def load_space_data(context): 44 | area_type = context.area.type 45 | sd = context.space_data 46 | 47 | if "IOPS" not in context.scene: 48 | context.scene["IOPS"] = {} 49 | 50 | if context.scene["IOPS"].values() != []: 51 | if area_type in context.scene["IOPS"] and area_type == "OUTLINER": 52 | outliner = context.scene["IOPS"]["OUTLINER"] 53 | display_mode = context.space_data.display_mode 54 | sd.display_mode = outliner["display_mode"] 55 | context.area.show_menus = outliner["show_menus"] 56 | sd.show_region_header = outliner["show_header"] 57 | 58 | if display_mode == "VIEW_LAYER": 59 | sd.show_restrict_column_enable = outliner["show_restrict_column_enable"] 60 | sd.show_restrict_column_select = outliner["show_restrict_column_select"] 61 | sd.show_restrict_column_hide = outliner["show_restrict_column_hide"] 62 | sd.show_restrict_column_viewport = outliner[ 63 | "show_restrict_column_viewport" 64 | ] 65 | sd.show_restrict_column_render = outliner["show_restrict_column_render"] 66 | sd.show_restrict_column_holdout = outliner[ 67 | "show_restrict_column_holdout" 68 | ] 69 | 70 | elif display_mode == "SCENES": 71 | sd.show_restrict_column_select = outliner["show_restrict_column_select"] 72 | sd.show_restrict_column_hide = outliner["show_restrict_column_hide"] 73 | sd.show_restrict_column_viewport = outliner[ 74 | "show_restrict_column_viewport" 75 | ] 76 | sd.show_restrict_column_render = outliner["show_restrict_column_render"] 77 | 78 | if display_mode != "DATA_API": 79 | sd.use_sort_alpha = outliner["use_sort_alpha"] 80 | 81 | return f"{area_type} space data loaded" 82 | 83 | elif area_type in context.scene["IOPS"]: 84 | sd.show_region_header = context.scene["IOPS"][area_type]["show_header"] 85 | context.area.show_menus = context.scene["IOPS"][area_type]["show_menus"] 86 | return f"{area_type} space data loaded" 87 | else: 88 | return f"No space data for {area_type}" 89 | else: 90 | return "No space data to load!" 91 | 92 | 93 | class IOPS_OT_SaveSpaceData(bpy.types.Operator): 94 | """Save Space Data""" 95 | 96 | bl_idname = "iops.space_data_save" 97 | bl_label = "Save space_data" 98 | 99 | # @classmethod 100 | # def poll(cls, context): 101 | # return context.area.type == 'OUTLINER' 102 | 103 | def execute(self, context): 104 | save_space_data(context) 105 | self.report({"INFO"}, f"{bpy.context.area.type} Space Data Saved") 106 | return {"FINISHED"} 107 | 108 | 109 | class IOPS_OT_LoadSpaceData(bpy.types.Operator): 110 | """Save Space Data""" 111 | 112 | bl_idname = "iops.space_data_load" 113 | bl_label = "Load space_data" 114 | 115 | # @classmethod 116 | # def poll(cls, context): 117 | # return context.area.type == 'OUTLINER' 118 | 119 | def execute(self, context): 120 | event = load_space_data(context) 121 | if event: 122 | self.report({"INFO"}, event) 123 | else: 124 | self.report({"INFO"}, "load_space_data failed") 125 | return {"FINISHED"} 126 | -------------------------------------------------------------------------------- /operators/snap_combos.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | import json 4 | 5 | def ensure_snap_combos_prefs(): 6 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences 7 | 8 | for i in range(1,9): 9 | if f"snap_combo_{i}" not in prefs.keys(): 10 | prefs[f"snap_combo_{i}"] = { 11 | "SNAP_ELEMENTS": { 12 | "INCREMENT": False, 13 | "VERTEX": True, 14 | "EDGE": False, 15 | "FACE": False, 16 | "VOLUME": False, 17 | "EDGE_MIDPOINT": False, 18 | "EDGE_PERPENDICULAR": False, 19 | "FACE_PROJECT": False, 20 | "FACE_NEAREST": False 21 | }, 22 | "TOOL_SETTINGS": { 23 | "transform_pivot_point": "ACTIVE_ELEMENT", 24 | "snap_target": "ACTIVE", 25 | # "use_snap_grid_absolute": True, 26 | "use_snap_self": True, 27 | "use_snap_align_rotation": False, 28 | "use_snap_peel_object": True, 29 | "use_snap_backface_culling": False, 30 | "use_snap_selectable": False, 31 | "use_snap_translate": False, 32 | "use_snap_rotate": False, 33 | "use_snap_scale": False, 34 | "use_snap_to_same_target": False 35 | }, 36 | "TRANSFORMATION": "GLOBAL" 37 | } 38 | 39 | def save_snap_combo(idx): 40 | tool_settings = bpy.context.scene.tool_settings 41 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences 42 | path = bpy.utils.script_path_user() 43 | iops_prefs_file = os.path.join(path, "presets", "IOPS", "iops_prefs_user.json") 44 | 45 | snap_elements_list = ['VERTEX', 46 | 'EDGE', 47 | 'FACE', 48 | 'VOLUME', 49 | 'INCREMENT', 50 | 'EDGE_MIDPOINT', 51 | 'EDGE_PERPENDICULAR', 52 | 'FACE_PROJECT', 53 | 'FACE_NEAREST' 54 | ] 55 | 56 | with open(iops_prefs_file, "r") as f: 57 | iops_prefs = json.load(f) 58 | 59 | ensure_snap_combos_prefs() 60 | 61 | for snap_combo, snap_details in iops_prefs.get("SNAP_COMBOS", {}).items(): 62 | if snap_combo[-1] == str(idx): 63 | snap_elements = tool_settings.snap_elements 64 | snap_elements_dict = {k: k in snap_elements for k in snap_elements_list} 65 | tool_settings_dict = { 66 | "transform_pivot_point": tool_settings.transform_pivot_point, 67 | "snap_target": tool_settings.snap_target, 68 | # "use_snap_grid_absolute": tool_settings.use_snap_grid_absolute, 69 | "use_snap_self": tool_settings.use_snap_self, 70 | "use_snap_align_rotation": tool_settings.use_snap_align_rotation, 71 | "use_snap_peel_object": tool_settings.use_snap_peel_object, 72 | "use_snap_backface_culling": tool_settings.use_snap_backface_culling, 73 | "use_snap_selectable": tool_settings.use_snap_selectable, 74 | "use_snap_translate": tool_settings.use_snap_translate, 75 | "use_snap_rotate": tool_settings.use_snap_rotate, 76 | "use_snap_scale": tool_settings.use_snap_scale, 77 | "use_snap_to_same_target": tool_settings.use_snap_to_same_target 78 | } 79 | snap_details["SNAP_ELEMENTS"] = snap_elements_dict 80 | snap_details["TOOL_SETTINGS"] = tool_settings_dict 81 | snap_details["TRANSFORMATION"] = bpy.context.scene.transform_orientation_slots[0].type 82 | prefs[snap_combo] = snap_details 83 | break 84 | 85 | with open(iops_prefs_file, "w") as f: 86 | json.dump(iops_prefs, f, indent=4) 87 | 88 | 89 | 90 | class IOPS_OT_SetSnapCombo(bpy.types.Operator): 91 | ''' 92 | Click to set the Snap Combo 93 | Shift+Click to save the current Snap Combo 94 | ''' 95 | bl_idname = "iops.set_snap_combo" 96 | bl_label = "Set Snap Combo" 97 | bl_options = {"REGISTER", "UNDO"} 98 | 99 | idx: bpy.props.IntProperty() 100 | 101 | def invoke(self, context, event): 102 | prefs = context.preferences.addons["InteractionOps"].preferences 103 | save_combo_mod = prefs.snap_combo_mod 104 | 105 | if ((event.shift and save_combo_mod == "SHIFT") or 106 | (event.ctrl and save_combo_mod == "CTRL") or 107 | (event.alt and save_combo_mod == "ALT")): 108 | 109 | save_snap_combo(self.idx) 110 | self.report({"INFO"}, f"Snap Combo {self.idx} Saved.") 111 | 112 | else: 113 | tool_settings = context.scene.tool_settings 114 | snap_combos_prefs = [sc for sc in prefs.keys() if sc.startswith("snap_combo_")] 115 | for snap_combo in snap_combos_prefs: 116 | if snap_combo[-1] == str(self.idx): 117 | elements = {k for k, v in prefs[snap_combo]["SNAP_ELEMENTS"].items() if v} 118 | tool_settings.snap_elements = elements 119 | tool_settings.transform_pivot_point = prefs[snap_combo]["TOOL_SETTINGS"]["transform_pivot_point"] 120 | tool_settings.snap_target = prefs[snap_combo]["TOOL_SETTINGS"]["snap_target"] 121 | # tool_settings.use_snap_grid_absolute = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_grid_absolute"] 122 | tool_settings.use_snap_self = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_self"] 123 | tool_settings.use_snap_align_rotation = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_align_rotation"] 124 | tool_settings.use_snap_peel_object = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_peel_object"] 125 | tool_settings.use_snap_backface_culling = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_backface_culling"] 126 | tool_settings.use_snap_selectable = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_selectable"] 127 | tool_settings.use_snap_translate = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_translate"] 128 | tool_settings.use_snap_rotate = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_rotate"] 129 | tool_settings.use_snap_scale = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_scale"] 130 | tool_settings.use_snap_to_same_target = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_to_same_target"] 131 | bpy.context.scene.transform_orientation_slots[0].type = prefs[snap_combo]["TRANSFORMATION"] 132 | break 133 | self.report({"INFO"}, f"Snap Combo {self.idx} Loaded.") 134 | return {"FINISHED"} 135 | 136 | -------------------------------------------------------------------------------- /operators/split_screen_area_new.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import StringProperty, FloatProperty 3 | import copy 4 | 5 | 6 | def ContextOverride(area): 7 | for window in bpy.context.window_manager.windows: 8 | screen = window.screen 9 | for screen_area in screen.areas: 10 | if screen_area.ui_type == area.ui_type: 11 | for region in screen_area.regions: 12 | if region.type == "WINDOW": 13 | context_override = { 14 | "window": window, 15 | "screen": screen, 16 | "area": screen_area, 17 | "region": region, 18 | } 19 | return context_override 20 | raise Exception("ERROR: Override failed!") 21 | 22 | 23 | class IOPS_OT_SplitScreenArea(bpy.types.Operator): 24 | bl_idname = "iops.split_screen_area" 25 | bl_label = "IOPS Split Screen Area" 26 | 27 | area_type: StringProperty( 28 | name="Area Type", description="Which area to create", default="" 29 | ) 30 | 31 | ui: StringProperty( 32 | name="Area UI Type", description="Which UI to enable", default="" 33 | ) 34 | 35 | pos: StringProperty( 36 | name="Position", description="Where to create new area", default="" 37 | ) 38 | 39 | factor: FloatProperty( 40 | name="Factor", 41 | description="Area split factor", 42 | default=0.01, 43 | soft_min=0.01, 44 | soft_max=1, 45 | step=0.01, 46 | precision=2, 47 | ) 48 | 49 | def refresh_ui(self, area): 50 | pass 51 | 52 | def get_join_xy(self, context, ui, pos): 53 | x, y = 0, 0 54 | if ui == context.area.ui_type: 55 | if pos == "TOP": 56 | x = int(context.area.width / 2 + context.area.x) 57 | y = context.area.y - 1 58 | 59 | elif pos == "RIGHT": 60 | x = context.area.x - 1 61 | y = int(context.area.height / 2 + context.area.y) 62 | 63 | elif pos == "BOTTOM": 64 | x = int(context.area.width / 2 + context.area.x) 65 | y = context.area.height + context.area.y + 1 66 | 67 | elif pos == "LEFT": 68 | x = context.area.width + context.area.x + 1 69 | y = int(context.area.y + context.area.height / 2) 70 | 71 | else: 72 | if pos == "TOP": 73 | x = int(context.area.x + context.area.width / 2) 74 | y = context.area.height + context.area.y + 1 75 | 76 | elif pos == "RIGHT": 77 | x = context.area.x + context.area.width + 1 78 | y = int(context.area.y + context.area.height / 2) 79 | 80 | elif pos == "BOTTOM": 81 | x = int(context.area.x + context.area.width / 2) 82 | y = context.area.y 83 | 84 | elif pos == "LEFT": 85 | x = context.area.x 86 | y = int(context.area.y + context.area.height / 2) 87 | 88 | return (x, y) 89 | 90 | def get_side_area(self, context, area, pos): 91 | 92 | side_area = None 93 | 94 | for screen_area in context.screen.areas: 95 | if ( 96 | pos == "TOP" 97 | and screen_area.x == area.x 98 | and screen_area.width == area.width 99 | and screen_area.y == area.height + area.y + 1 100 | ): 101 | side_area = screen_area 102 | break 103 | 104 | elif ( 105 | pos == "RIGHT" 106 | and screen_area.x == area.x + area.width + 1 107 | and screen_area.height == area.height 108 | and screen_area.y == area.y 109 | ): 110 | side_area = screen_area 111 | break 112 | 113 | elif ( 114 | pos == "BOTTOM" 115 | and screen_area.width == area.width 116 | and screen_area.x == area.x 117 | and screen_area.height + screen_area.y + 1 == area.y 118 | ): 119 | side_area = screen_area 120 | break 121 | 122 | elif ( 123 | pos == "LEFT" 124 | and screen_area.width + screen_area.x + 1 == area.x 125 | and screen_area.y == area.y 126 | and screen_area.height == area.height 127 | ): 128 | side_area = screen_area 129 | break 130 | 131 | return side_area 132 | 133 | def join_areas(self, context, current_area, side_area, pos, swap): 134 | context_override = ContextOverride(side_area) 135 | with context.temp_override(**context_override): 136 | bpy.ops.iops.space_data_save() 137 | bpy.ops.screen.area_close() 138 | 139 | return side_area 140 | 141 | def execute(self, context): 142 | 143 | areas = list(context.screen.areas) 144 | current_area = context.area 145 | side_area = None 146 | 147 | # Fix stupid Blender's behaviour 148 | if self.factor == 0.5: 149 | self.factor = 0.499 150 | 151 | # Check if toggle fullscreen was activated 152 | if "nonnormal" in context.window.screen.name: 153 | bpy.ops.screen.back_to_previous() 154 | return {"FINISHED"} 155 | 156 | if current_area.type == self.area_type: 157 | context_override = ContextOverride(current_area) 158 | with context.temp_override(**context_override): 159 | bpy.ops.iops.space_data_save() 160 | bpy.ops.screen.area_close() 161 | return {"FINISHED"} 162 | 163 | for area in context.screen.areas: 164 | if area == current_area: 165 | continue 166 | elif area.type == self.area_type and area.ui_type == self.ui: 167 | context_override = ContextOverride(area) 168 | with context.temp_override(**context_override): 169 | bpy.ops.iops.space_data_save() 170 | bpy.ops.screen.area_close() 171 | 172 | self.report({"INFO"}, "Joined Areas") 173 | return {"FINISHED"} 174 | else: 175 | continue 176 | 177 | if current_area.ui_type == self.ui: 178 | if self.pos == "LEFT": 179 | mirror_pos = "RIGHT" 180 | elif self.pos == "RIGHT": 181 | mirror_pos = "LEFT" 182 | elif self.pos == "TOP": 183 | mirror_pos = "BOTTOM" 184 | elif self.pos == "BOTTOM": 185 | mirror_pos = "TOP" 186 | 187 | swap = False if mirror_pos in {"TOP", "RIGHT"} else True 188 | side_area = self.get_side_area(context, current_area, mirror_pos) 189 | if side_area: 190 | self.join_areas(context, current_area, side_area, mirror_pos, swap) 191 | self.report({"INFO"}, "Joined Areas") 192 | return {"FINISHED"} 193 | else: 194 | self.report({"INFO"}, "No side area to join") 195 | return {"FINISHED"} 196 | 197 | else: 198 | new_area = None 199 | swap = True if self.factor >= 0.5 else False 200 | 201 | direction = "VERTICAL" if self.pos in {"LEFT", "RIGHT"} else "HORIZONTAL" 202 | factor = (1 - self.factor) if self.pos in {"RIGHT", "TOP"} else self.factor 203 | bpy.ops.screen.area_split(direction=direction, factor=factor) 204 | 205 | for area in context.screen.areas: 206 | if area not in areas: 207 | new_area = area 208 | break 209 | 210 | if new_area: 211 | new_area.type = self.area_type 212 | new_area.ui_type = self.ui 213 | context_override = ContextOverride(new_area) 214 | with context.temp_override(**context_override): 215 | bpy.ops.iops.space_data_load() 216 | return {"FINISHED"} 217 | 218 | return {"FINISHED"} 219 | -------------------------------------------------------------------------------- /operators/ui_prop_switch.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class IOPS_OT_ActiveObject_Scroll_UP(bpy.types.Operator): 5 | """Cyclic switching properties types UP""" 6 | 7 | bl_idname = "iops.object_active_object_scroll_up" 8 | bl_label = "Cyclic switching active object in selection UP" 9 | 10 | def execute(self, context): 11 | selected_objects = bpy.context.view_layer.objects.selected 12 | active = bpy.context.active_object 13 | 14 | for index, elem in enumerate(selected_objects): 15 | if elem == active: 16 | if index <= len(selected_objects): 17 | idx = index + 1 18 | try: 19 | bpy.context.view_layer.objects.active = selected_objects[idx] 20 | except IndexError: 21 | idx = 0 22 | bpy.context.view_layer.objects.active = selected_objects[idx] 23 | else: 24 | idx = 0 25 | bpy.context.view_layer.objects.active = selected_objects[idx] 26 | self.report({"INFO"}, "Active: " + context.active_object.name) 27 | return {"FINISHED"} 28 | 29 | 30 | class IOPS_OT_ActiveObject_Scroll_DOWN(bpy.types.Operator): 31 | """Cyclic switching snap with types UP""" 32 | 33 | bl_idname = "iops.object_active_object_scroll_down" 34 | bl_label = "Cyclic switching active object in selection DOWN" 35 | 36 | def execute(self, context): 37 | selected_objects = bpy.context.view_layer.objects.selected 38 | active = bpy.context.active_object 39 | 40 | for index, elem in enumerate(selected_objects): 41 | if elem == active: 42 | if index > 0 and index <= len(selected_objects): 43 | idx = index - 1 44 | try: 45 | bpy.context.view_layer.objects.active = selected_objects[idx] 46 | except IndexError: 47 | idx = 0 48 | bpy.context.view_layer.objects.active = selected_objects[idx] 49 | else: 50 | idx = len(selected_objects) - 1 51 | try: 52 | bpy.context.view_layer.objects.active = selected_objects[idx] 53 | except IndexError: 54 | idx = 0 55 | bpy.context.view_layer.objects.active = selected_objects[idx] 56 | self.report({"INFO"}, "Active: " + context.active_object.name) 57 | 58 | return {"FINISHED"} 59 | -------------------------------------------------------------------------------- /prefs/addon_properties.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import PropertyGroup 3 | from bpy.props import ( 4 | FloatProperty, 5 | FloatVectorProperty, 6 | StringProperty, 7 | ) 8 | 9 | def update_exec_filter(self, context): 10 | scripts = bpy.context.scene['IOPS']['executor_scripts'] 11 | filtered_scripts = [script for script in scripts if self.iops_exec_filter.lower() in script.lower()] 12 | bpy.context.scene['IOPS']['filtered_executor_scripts'] = filtered_scripts if len(filtered_scripts) > 0 else None 13 | 14 | 15 | 16 | class IOPS_AddonProperties(PropertyGroup): 17 | iops_panel_mesh_info: bpy.props.BoolProperty( 18 | name="Show mesh info", description="Show mesh info panel", default=False 19 | ) 20 | 21 | iops_rotation_angle: FloatProperty( 22 | name="Angle", description="Degrees", default=90, min=0.0 23 | ) 24 | iops_split_previous: StringProperty( 25 | name="iops_split_previous", 26 | default="VIEW_3D", 27 | ) 28 | iops_exec_filter: StringProperty( 29 | name="Filter", 30 | default="", 31 | options={'TEXTEDIT_UPDATE'}, 32 | update=update_exec_filter, 33 | ) 34 | 35 | 36 | class IOPS_SceneProperties(PropertyGroup): 37 | dragsnap_point_a: FloatVectorProperty( 38 | name="DragSnap Point A", 39 | description="DragSnap Point A", 40 | default=(0.0, 0.0, 0.0), 41 | size=3, 42 | subtype="COORDINATES", 43 | ) 44 | 45 | dragsnap_point_b: FloatVectorProperty( 46 | name="DragSnap Point B", 47 | description="DragSnap Point B", 48 | default=(0.0, 0.0, 0.0), 49 | size=3, 50 | subtype="COORDINATES", 51 | ) 52 | 53 | iops_vertex_color: FloatVectorProperty( 54 | name="VertexColor", 55 | description="Color picker", 56 | default=(1.0, 1.0, 1.0, 1.0), 57 | min=0.0, 58 | max=1.0, 59 | subtype="COLOR", 60 | size=4, 61 | ) 62 | -------------------------------------------------------------------------------- /prefs/hotkeys_default.py: -------------------------------------------------------------------------------- 1 | # CTRL, ALT, Shift 2 | keys_default = [ 3 | # IOPS F Keys 4 | ("iops.function_f1", "F1", "PRESS", False, False, False, False), 5 | ("iops.function_f2", "F2", "PRESS", False, False, False, False), 6 | ("iops.function_f3", "F3", "PRESS", False, False, False, False), 7 | ("iops.function_f4", "F4", "PRESS", False, False, False, False), 8 | ("iops.function_f5", "F5", "PRESS", False, False, False, False), 9 | ("iops.function_esc", "ESC", "PRESS", False, False, False, False), 10 | #IOPS Operators Cursor 11 | ("iops.cursor_rotate", "F19", "PRESS", True, True, True, False), 12 | # IOPS Operators Mesh 13 | ("iops.mesh_to_verts", "F1", "PRESS", False, True, False, False), 14 | ("iops.mesh_to_edges", "F2", "PRESS", False, True, False, False), 15 | ("iops.mesh_to_faces", "F3", "PRESS", False, True, False, False), 16 | ("iops.mesh_align_origin_to_normal", "F5", "PRESS", False, True, False, False), 17 | ("iops.mesh_mouseover_fill_select", "F19", "PRESS", True, True, True, False), 18 | ("iops.mesh_to_grid", "UP_ARROW", "PRESS", False, False, False, False), 19 | ("iops.mesh_cursor_bisect", "F19", "PRESS", False, False, False, False), 20 | # Zaloopok's operators 21 | ("iops.z_grow_loop", "F19", "PRESS", True, True, True, False), 22 | ("iops.z_shrink_loop", "F19", "PRESS", True, True, True, False), 23 | ("iops.z_grow_ring", "F19", "PRESS", True, True, True, False), 24 | ("iops.z_shrink_ring", "F19", "PRESS", True, True, True, False), 25 | ("iops.z_select_bounded_loop", "F19", "PRESS", True, True, True, False), 26 | ("iops.z_select_bounded_ring", "F19", "PRESS", True, True, True, False), 27 | ("iops.z_connect", "F19", "PRESS", True, True, True, False), 28 | ("iops.z_line_up_edges", "F19", "PRESS", True, True, True, False), 29 | ("iops.z_eq_edges", "F19", "PRESS", True, True, True, False), 30 | ("iops.z_put_on", "F19", "PRESS", True, True, True, False), 31 | ("iops.z_mirror", "F19", "PRESS", True, True, True, False), 32 | ("iops.z_delete_mode", "F19", "PRESS", True, True, True, False), 33 | # IOPS Operators Object 34 | ("iops.object_to_grid_from_active", "F19", "PRESS", True, True, True, False), 35 | ("iops.object_modal_three_point_rotation", "F19", "PRESS", True, True, True, False), 36 | ("iops.object_normalize", "UP_ARROW", "PRESS", False, False, False, False), 37 | ("iops.object_drag_snap", "S", "PRESS", True, True, True, False), 38 | ("iops.object_drag_snap_cursor", "F19", "PRESS", True, True, True, False), 39 | ("iops.uv_drag_snap_uv", "S", "PRESS", True, True, True, False), 40 | ("iops.object_active_object_scroll_up", "WHEELUPMOUSE", "PRESS", True, True, False, True), 41 | ("iops.object_active_object_scroll_down", "WHEELDOWNMOUSE", "PRESS", True, True, False, True), 42 | ("iops.object_rotate_mx", "LEFT_ARROW", "PRESS", False, False, True, False), 43 | ("iops.object_rotate_x", "LEFT_ARROW", "PRESS", False, False, False, False), 44 | ("iops.object_rotate_my", "DOWN_ARROW", "PRESS", False, False, True, False), 45 | ("iops.object_rotate_y", "DOWN_ARROW", "PRESS", False, False, False, False), 46 | ("iops.object_rotate_mz", "RIGHT_ARROW", "PRESS", False, False, True, False), 47 | ("iops.object_rotate_z", "RIGHT_ARROW", "PRESS", False, False, False, False), 48 | ("iops.mesh_uv_channel_hop", "F19", "PRESS", True, True, True, False), 49 | # IOPS Scripts 50 | ("iops.scripts_call_mt_executor", "X", "PRESS", True, True, True, False), 51 | ("iops.scripts_run_text", "F19", "PRESS", True, True, True, False), 52 | # IOPS UI Pies 53 | ("iops.call_pie_menu", "Q", "PRESS", True, True, True, False), 54 | ("iops.call_pie_edit", "F19", "PRESS", True, True, True, False), 55 | ("iops.call_pie_split", "S", "PRESS", True, True, True, False), 56 | # IOPS UI Panels 57 | ("iops.call_panel_tps", "BUTTON4MOUSE", "PRESS", False, False, True, False), 58 | ("iops.call_panel_data", "BUTTON4MOUSE", "PRESS", True, False, True, False), 59 | ("iops.call_panel_tm", "T", "PRESS", True, True, True, False), 60 | ] 61 | -------------------------------------------------------------------------------- /prefs/iops_prefs.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def get_iops_prefs(): 5 | prefs = bpy.context.preferences.addons['InteractionOps'].preferences 6 | snap_combo_dict = {} 7 | 8 | for i in range(1, 9): 9 | try: 10 | snap_combo_key = f"snap_combo_{i}" 11 | snap_combo_dict[snap_combo_key] = { 12 | "SNAP_ELEMENTS": {k: prefs[snap_combo_key]["SNAP_ELEMENTS"][k] for k in prefs[snap_combo_key]["SNAP_ELEMENTS"] if prefs[snap_combo_key]["SNAP_ELEMENTS"][k]}, 13 | "TOOL_SETTINGS": {k: prefs[snap_combo_key]["TOOL_SETTINGS"][k] for k in prefs[snap_combo_key]["TOOL_SETTINGS"]}, 14 | "TRANSFORMATION": prefs[snap_combo_key]["TRANSFORMATION"] 15 | } 16 | except KeyError: 17 | snap_combo_dict[snap_combo_key] = { 18 | "SNAP_ELEMENTS": { 19 | "INCREMENT": False, 20 | "VERTEX": True, 21 | "EDGE": False, 22 | "FACE": False, 23 | "VOLUME": False, 24 | "EDGE_MIDPOINT": False, 25 | "EDGE_PERPENDICULAR": False, 26 | "FACE_PROJECT": False, 27 | "FACE_NEAREST": False 28 | }, 29 | "TOOL_SETTINGS": { 30 | "transform_pivot_point": "ACTIVE_ELEMENT", 31 | "snap_target": "ACTIVE", 32 | # "use_snap_grid_absolute": True, 33 | "use_snap_self": True, 34 | "use_snap_align_rotation": False, 35 | "use_snap_peel_object": True, 36 | "use_snap_backface_culling": False, 37 | "use_snap_selectable": False, 38 | "use_snap_translate": False, 39 | "use_snap_rotate": False, 40 | "use_snap_scale": False, 41 | "use_snap_to_same_target": False 42 | }, 43 | "TRANSFORMATION": "GLOBAL" 44 | } 45 | 46 | iops_prefs = { 47 | "IOPS_DEBUG": {"IOPS_DEBUG": prefs.IOPS_DEBUG}, 48 | "ALIGN_TO_EDGE": {"align_edge_color": list(prefs.align_edge_color)}, 49 | "EXECUTOR": { 50 | "executor_column_count": prefs.executor_column_count, 51 | "executor_scripts_folder": prefs.executor_scripts_folder, 52 | "executor_name_lenght": prefs.executor_name_lenght, 53 | "executor_use_script_path_user": prefs.executor_use_script_path_user, 54 | "executor_scripts_subfolder": prefs.executor_scripts_subfolder, 55 | }, 56 | "SPLIT_AREA_PIES": { 57 | f"PIE_{i}": { 58 | f"split_area_pie_{i}_factor": getattr(prefs, f"split_area_pie_{i}_factor"), 59 | f"split_area_pie_{i}_pos": getattr(prefs, f"split_area_pie_{i}_pos"), 60 | f"split_area_pie_{i}_ui": getattr(prefs, f"split_area_pie_{i}_ui") 61 | } for i in range(1, 10) if i != 5 # Exclude non-existing pie 5 62 | }, 63 | "UI_TEXT": { 64 | "text_color": list(prefs.text_color), 65 | "text_color_key": list(prefs.text_color_key), 66 | "text_pos_x": prefs.text_pos_x, 67 | "text_pos_y": prefs.text_pos_y, 68 | "text_shadow_color": list(prefs.text_shadow_color), 69 | "text_shadow_pos_x": prefs.text_shadow_pos_x, 70 | "text_shadow_pos_y": prefs.text_shadow_pos_y, 71 | "text_shadow_toggle": prefs.text_shadow_toggle, 72 | "text_size": prefs.text_size 73 | }, 74 | "UI_TEXT_STAT": { 75 | "iops_stat" : prefs.iops_stat, 76 | "text_color_stat": list(prefs.text_color_stat), 77 | "text_color_key_stat": list(prefs.text_color_key_stat), 78 | "text_color_error_stat": list(prefs.text_color_error_stat), 79 | "text_pos_x_stat": prefs.text_pos_x_stat, 80 | "text_pos_y_stat": prefs.text_pos_y_stat, 81 | "text_shadow_color_stat": list(prefs.text_shadow_color_stat), 82 | "text_shadow_pos_x_stat": prefs.text_shadow_pos_x_stat, 83 | "text_shadow_pos_y_stat": prefs.text_shadow_pos_y_stat, 84 | "text_shadow_toggle_stat": prefs.text_shadow_toggle_stat, 85 | "text_size_stat": prefs.text_size_stat, 86 | "text_column_offset_stat": prefs.text_column_offset_stat, 87 | "text_column_width_stat": prefs.text_column_width_stat 88 | }, 89 | "VISUAL_ORIGIN": { 90 | "vo_cage_ap_color": list(prefs.vo_cage_ap_color), 91 | "vo_cage_ap_size": prefs.vo_cage_ap_size, 92 | "vo_cage_color": list(prefs.vo_cage_color), 93 | "vo_cage_p_size": prefs.vo_cage_p_size, 94 | "vo_cage_points_color": list(prefs.vo_cage_points_color), 95 | "vo_cage_line_thickness": prefs.vo_cage_line_thickness 96 | }, 97 | "TEXTURE_TO_MATERIAL": { 98 | "texture_to_material_prefixes": prefs.texture_to_material_prefixes, 99 | "texture_to_material_suffixes": prefs.texture_to_material_suffixes 100 | }, 101 | "SNAP_COMBOS": { 102 | "snap_combo_1": snap_combo_dict["snap_combo_1"], 103 | "snap_combo_2": snap_combo_dict["snap_combo_2"], 104 | "snap_combo_3": snap_combo_dict["snap_combo_3"], 105 | "snap_combo_4": snap_combo_dict["snap_combo_4"], 106 | "snap_combo_5": snap_combo_dict["snap_combo_5"], 107 | "snap_combo_6": snap_combo_dict["snap_combo_6"], 108 | "snap_combo_7": snap_combo_dict["snap_combo_7"], 109 | "snap_combo_8": snap_combo_dict["snap_combo_8"] 110 | }, 111 | "DRAG_SNAP": { 112 | "drag_snap_line_thickness": prefs.drag_snap_line_thickness 113 | } 114 | } 115 | 116 | # # Update snap combos if exist in prefs 117 | # if getattr(prefs, "snap_combos", False): 118 | # for i in range(1, 9): 119 | # snap_combo = prefs.snap_combos[i-1] 120 | # iops_prefs[f"snap_combo_{i}"] = snap_combo 121 | 122 | return iops_prefs -------------------------------------------------------------------------------- /prefs/iops_prefs_list.py: -------------------------------------------------------------------------------- 1 | iops_prefs_list = [ 2 | "IOPS_DEBUG", 3 | "align_edge_color", 4 | "executor_column_count", 5 | "executor_scripts_folder", 6 | "split_area_pie_1_factor", 7 | "split_area_pie_1_pos", 8 | "split_area_pie_1_ui", 9 | "split_area_pie_2_factor", 10 | "split_area_pie_2_pos", 11 | "split_area_pie_2_ui", 12 | "split_area_pie_3_factor", 13 | "split_area_pie_3_pos", 14 | "split_area_pie_3_ui", 15 | "split_area_pie_4_factor", 16 | "split_area_pie_4_pos", 17 | "split_area_pie_4_ui", 18 | "split_area_pie_6_factor", 19 | "split_area_pie_6_pos", 20 | "split_area_pie_6_ui", 21 | "split_area_pie_7_factor", 22 | "split_area_pie_7_pos", 23 | "split_area_pie_7_ui", 24 | "split_area_pie_8_factor", 25 | "split_area_pie_8_pos", 26 | "split_area_pie_8_ui", 27 | "split_area_pie_9_factor", 28 | "split_area_pie_9_pos", 29 | "split_area_pie_9_ui", 30 | "text_color", 31 | "text_color_key", 32 | "text_pos_x", 33 | "text_pos_y", 34 | "text_shadow_color", 35 | "text_shadow_pos_x", 36 | "text_shadow_pos_y", 37 | "text_shadow_toggle", 38 | "text_size", 39 | "vo_cage_ap_color", 40 | "vo_cage_ap_size", 41 | "vo_cage_color", 42 | "vo_cage_p_size", 43 | "vo_cage_points_color", 44 | "vo_cage_line_thickness", 45 | "drag_snap_line_thickness", 46 | "texture_to_material_prefixes", 47 | "texture_to_material_suffixes", 48 | "switch_list_axis", 49 | "switch_list_ppoint", 50 | "switch_list_snap", 51 | "snap_combo_1", 52 | "snap_combo_2", 53 | "snap_combo_3", 54 | "snap_combo_4", 55 | "snap_combo_5", 56 | "snap_combo_6", 57 | ] 58 | 59 | -------------------------------------------------------------------------------- /ui/iops_pie_edit.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | from bpy.types import Menu 4 | 5 | 6 | def get_text_icon(context, operator): 7 | if context.object.type == "MESH": 8 | match operator: 9 | case "f1": 10 | return "Vertex", "VERTEXSEL" 11 | case "f2": 12 | return "Edge", "EDGESEL" 13 | case "f3": 14 | return "Face", "FACESEL" 15 | case "esc": 16 | return "Esc", "EVENT_ESC" 17 | elif context.object.type == "ARMATURE": 18 | match operator: 19 | case "f1": 20 | return "Edit Mode", "EDITMODE_HLT" 21 | case "f2": 22 | return "Pose Mode", "POSE_HLT" 23 | case "f3": 24 | return "Set Parent to Bone", "BONE_DATA" 25 | case "esc": 26 | return "Esc", "EVENT_ESC" 27 | elif context.object.type == "EMPTY": 28 | match operator: 29 | case "f1": 30 | return "Open Instance Collection .blend", "FILE_BACKUP" 31 | case "f2": 32 | return "Make instance real", "OUTLINER_OB_GROUP_INSTANCE" 33 | case "f3": 34 | return "F3", "EVENT_F3" 35 | case _: 36 | return "Esc", "EVENT_ESC" 37 | 38 | 39 | class IOPS_MT_Pie_Edit_Submenu(Menu): 40 | bl_label = "IOPS_MT_Pie_Edit_Submenu" 41 | 42 | def draw(self, context): 43 | layout = self.layout 44 | layout.label(text="IOPS Modes") 45 | layout.separator() 46 | layout.operator("object.mode_set", text="Object Mode").mode = "OBJECT" 47 | layout.operator("object.mode_set", text="Edit Mode").mode = "EDIT" 48 | layout.operator("object.mode_set", text="Sculpt Mode").mode = "SCULPT" 49 | layout.operator("object.mode_set", text="Vertex Paint").mode = "VERTEX_PAINT" 50 | layout.operator("object.mode_set", text="Weight Paint").mode = "WEIGHT_PAINT" 51 | 52 | 53 | class IOPS_MT_Pie_Edit(Menu): 54 | # bl_idname = "iops.pie_menu" 55 | bl_label = "IOPS_MT_Pie_Edit" 56 | 57 | @classmethod 58 | def poll(self, context): 59 | return ( 60 | context.area.type in {"VIEW_3D", "IMAGE_EDITOR"} and context.active_object 61 | ) 62 | 63 | def draw(self, context): 64 | layout = self.layout 65 | pie = layout.menu_pie() 66 | 67 | if context.area.type == "VIEW_3D": 68 | # Open Linked Library Blend 69 | if ( 70 | context.object.type == "EMPTY" 71 | and context.object.instance_collection 72 | and context.object.instance_type == "COLLECTION" 73 | # and context.object.instance_collection.library 74 | ): 75 | pie.separator() 76 | pie.separator() 77 | 78 | op = pie.operator("machin3.assemble_instance_collection", text="Expand Collection to Scene") 79 | 80 | if context.object.instance_collection.library: 81 | blendpath = os.path.abspath( 82 | bpy.path.abspath( 83 | context.object.instance_collection.library.filepath 84 | ) 85 | ) 86 | library = context.object.instance_collection.library.name 87 | 88 | op = pie.operator( 89 | "machin3.open_library_blend", 90 | text=f"Open {os.path.basename(blendpath)}", 91 | ) 92 | op.blendpath = blendpath 93 | op.library = library 94 | 95 | 96 | 97 | # Curve 98 | elif context.object.type == "CURVE": 99 | # 4 - LEFT 100 | pie.separator() 101 | # 6 - RIGHT 102 | pie.separator() 103 | # 2 - BOTTOM 104 | pie.operator("iops.function_esc", text="Esc", icon="EVENT_ESC") 105 | # 8 - TOP 106 | pie.operator("iops.function_f1", text="Edit", icon="CURVE_DATA") 107 | 108 | else: 109 | # 4 - LEFT 110 | pie.operator( 111 | "iops.function_f1", 112 | text=get_text_icon(context, "f1")[0], 113 | icon=get_text_icon(context, "f1")[1], 114 | ) 115 | # 6 - RIGHT 116 | pie.operator( 117 | "iops.function_f3", 118 | text=get_text_icon(context, "f3")[0], 119 | icon=get_text_icon(context, "f3")[1], 120 | ) 121 | # 2 - BOTTOM 122 | pie.operator( 123 | "iops.function_esc", 124 | text=get_text_icon(context, "esc")[0], 125 | icon=get_text_icon(context, "esc")[1], 126 | ) 127 | # 8 - TOP 128 | pie.operator( 129 | "iops.function_f2", 130 | text=get_text_icon(context, "f2")[0], 131 | icon=get_text_icon(context, "f2")[1], 132 | ) 133 | 134 | # 7 - TOP - LEFT 135 | # pie.separator() 136 | # 9 - TOP - RIGHT 137 | # pie.separator() 138 | # 1 - BOTTOM - LEFT 139 | # pie.separator() 140 | # 3 - BOTTOM - RIGHT 141 | # pie.separator() 142 | # Additional items underneath 143 | box = pie.split() 144 | column = box.column() 145 | column.scale_y = 1.5 146 | column.scale_x = 1.5 147 | 148 | row = column.row(align=True) 149 | 150 | # r = row.row(align=True) 151 | # r.active = False if context.mode == 'PAINT_GPENCIL' else True 152 | # r.operator("machin3.surface_draw_mode", text="", icon="GREASEPENCIL") 153 | 154 | # r = row.row(align=True) 155 | # r.active = False if context.mode == 'TEXTURE_PAINT' else True 156 | # r.operator("object.mode_set", text="", icon="TPAINT_HLT").mode = 'TEXTURE_PAINT' 157 | if context.object.type in {"MESH"}: 158 | r = row.row(align=True) 159 | r.active = False if context.mode == "WEIGHT_PAINT" else True 160 | r.operator("object.mode_set", text="", icon="WPAINT_HLT").mode = ( 161 | "WEIGHT_PAINT" 162 | ) 163 | 164 | r = row.row(align=True) 165 | r.active = False if context.mode == "VERTEX_PAINT" else True 166 | r.operator("object.mode_set", text="", icon="VPAINT_HLT").mode = ( 167 | "VERTEX_PAINT" 168 | ) 169 | 170 | r = row.row(align=True) 171 | r.active = False if context.mode == "SCULPT" else True 172 | r.operator( 173 | "object.mode_set", text="", icon="SCULPTMODE_HLT" 174 | ).mode = "SCULPT" 175 | 176 | r = row.row(align=True) 177 | r.active = False if context.mode == "OBJECT" else True 178 | r.operator("object.mode_set", text="", icon="OBJECT_DATA").mode = ( 179 | "OBJECT" 180 | ) 181 | 182 | r = row.row(align=True) 183 | r.active = False if context.mode == "EDIT_MESH" else True 184 | r.operator("object.mode_set", text="", icon="EDITMODE_HLT").mode = ( 185 | "EDIT" 186 | ) 187 | 188 | elif context.area.type == "IMAGE_EDITOR": 189 | if context.tool_settings.use_uv_select_sync: 190 | # 4 - LEFT 191 | pie.operator("iops.function_f1", text="Vertex", icon="VERTEXSEL") 192 | # 6 - RIGHT 193 | pie.operator("iops.function_f3", text="Face", icon="FACESEL") 194 | # 2 - BOTTOM 195 | pie.operator("iops.function_esc", text="Esc", icon="EVENT_ESC") 196 | # 8 - TOP 197 | pie.operator("iops.function_f2", text="Edge", icon="EDGESEL") 198 | # 7 - TOP - LEFT 199 | elif not context.tool_settings.use_uv_select_sync: 200 | # 4 - LEFT 201 | pie.operator("iops.function_f1", text="Vertex", icon="VERTEXSEL") 202 | # 6 - RIGHT 203 | pie.operator("iops.function_f3", text="Face", icon="FACESEL") 204 | # 2 - BOTTOM 205 | pie.operator("iops.function_esc", text="Esc", icon="EVENT_ESC") 206 | # 8 - TOP 207 | pie.operator("iops.function_f2", text="Edge", icon="EDGESEL") 208 | # 7 - TOP - LEFT 209 | pie.separator() 210 | # 9 - TOP - RIGHT 211 | pie.operator("iops.function_f4", text="Island", icon="UV_ISLANDSEL") 212 | 213 | 214 | class IOPS_OT_Call_Pie_Edit(bpy.types.Operator): 215 | """IOPS Pie""" 216 | 217 | bl_idname = "iops.call_pie_edit" 218 | bl_label = "IOPS Pie Edit" 219 | 220 | @classmethod 221 | def poll(self, context): 222 | return ( 223 | context.area.type in {"VIEW_3D", "IMAGE_EDITOR"} and context.active_object 224 | ) 225 | 226 | def execute(self, context): 227 | bpy.ops.wm.call_menu_pie(name="IOPS_MT_Pie_Edit") 228 | return {"FINISHED"} 229 | -------------------------------------------------------------------------------- /ui/iops_pie_menu.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import Menu 3 | from ..utils.functions import get_addon 4 | 5 | 6 | class IOPS_MT_Pie_Menu(Menu): 7 | # bl_idname = "iops.pie_menu" 8 | bl_label = "IOPS Pie" 9 | 10 | def draw(self, context): 11 | forgottentools, _, _, _ = get_addon("Forgotten Tools") 12 | optiloops, _, _, _ = get_addon("Optiloops") 13 | bmax_connector, _, _, _ = get_addon("BMAX Connector") 14 | bmoi_connector, _, _, _ = get_addon("BMOI Connector") 15 | # brush = context.tool_settings.image_paint.brush 16 | 17 | layout = self.layout 18 | pie = layout.menu_pie() 19 | 20 | # 4 - LEFT 21 | # pie.separator() 22 | # pie.operator("wm.call_menu_pie", text = "Some Other Pie 0", icon = "RIGHTARROW_THIN").name="Pie_menu" 23 | col = layout.menu_pie() 24 | box = col.column(align=True).box().column() 25 | box.label(text="IOPS") 26 | col.scale_x = 0.9 27 | col = box.column(align=True) 28 | col.prop( 29 | context.scene.IOPS, 30 | "iops_vertex_color", 31 | text="", 32 | ) 33 | col.operator("iops.mesh_assign_vertex_color", text="Set Vertex Color") 34 | col = box.column(align=True) 35 | row = col.row(align=True) 36 | row.operator("iops.mesh_assign_vertex_color", text="White").fill_color_white = True 37 | row.operator("iops.mesh_assign_vertex_color", text="Grey").fill_color_grey = True 38 | row.operator("iops.mesh_assign_vertex_color", text="Black").fill_color_black = True 39 | col = box.column(align=True) 40 | col.operator("iops.mesh_assign_vertex_color_alpha", text="Set Vertex Alpha") 41 | col.separator() 42 | col.operator("iops.materials_from_textures", text="Materials from Textures") 43 | col.separator() 44 | col.operator("iops.object_replace", text="Object Replace") 45 | col.operator("iops.object_align_between_two", text="Align Between Two") 46 | col.operator("iops.mesh_quick_snap", text="Quick Snap") 47 | col.operator("iops.object_drop_it", text="Drop It!") 48 | col.operator("iops.object_kitbash_grid", text="Grid") 49 | col.separator() 50 | col.operator("iops.modifier_easy_array_caps", text="Easy Modifier - Array Caps") 51 | col.operator("iops.modifier_easy_array_curve", text="Easy Modifier - Array Curve") 52 | col.operator("iops.modifier_easy_curve", text="Easy Modifier - Curve") 53 | col.operator("iops.modifier_easy_shwarp", text="Easy Modifier - SHWARP") 54 | col.separator() 55 | col.operator("iops.assets_render_asset_thumbnail", text="Render Asset Thumbnail") 56 | col.separator() 57 | col.operator("iops.reload_libraries", text="Reload Libraries") 58 | col.operator("iops.reload_images", text="Reload Images") 59 | 60 | # 6 - RIGHT 61 | # pie.separator() 62 | 63 | other = pie.row() 64 | gap = other.column() 65 | gap.separator() 66 | gap.scale_y = 7 67 | other_menu = other.box().column() 68 | other_menu.scale_y = 1 69 | if bmax_connector: 70 | bmax_prefs = bpy.context.preferences.addons["BMAX_Connector"].preferences 71 | other_menu.label(text="BMax") 72 | if bmax_prefs.file_format == "FBX": 73 | other_menu.operator( 74 | "bmax.export", icon="EXPORT", text="Send to Maya/3dsmax" 75 | ) 76 | other_menu.operator( 77 | "bmax.import", icon="IMPORT", text="Get from Maya/3dsmax" 78 | ) 79 | if bmax_prefs.file_format == "USD": 80 | other_menu.operator( 81 | "bmax.export_usd", icon="EXPORT", text="Send to Maya/3dsmax" 82 | ) 83 | other_menu.operator( 84 | "bmax.import_usd", icon="IMPORT", text="Get from Maya/3dsmax" 85 | ) 86 | row = other_menu.row(align=True) 87 | row.prop(bmax_prefs, "export_reset_location", icon="EVENT_L", text=" ") 88 | row.prop(bmax_prefs, "export_reset_rotation", icon="EVENT_R", text=" ") 89 | row.prop(bmax_prefs, "export_reset_scale", icon="EVENT_S", text=" ") 90 | other_menu = other.box().column() 91 | if bmoi_connector: 92 | other_menu.label(text="BMoI") 93 | other_menu.operator("bmoi3d.export", icon="EXPORT", text="Send to MoI3D") 94 | other_menu.operator("bmoi3d.import", icon="IMPORT", text="Get from MoI3D") 95 | 96 | # 2 - BOTTOM 97 | wm = context.window_manager 98 | prefs = context.preferences.addons["B2RUVL"].preferences 99 | uvl = prefs.uvlayout_enable 100 | ruv = prefs.rizomuv_enable 101 | uvl_path = prefs.uvlayout_app_path 102 | ruv_path = prefs.rizomuv_app_path 103 | 104 | col = layout.menu_pie() 105 | box = col.column(align=True).box().column() 106 | box.label(text="B2RUVL") 107 | col_top = box.column(align=True) 108 | row = col_top.row(align=True) 109 | col_left = row.column(align=True) 110 | col_right = row.column(align=True) 111 | col_left.prop(wm.B2RUVL_PanelProperties, "uvMap_mode", text="") 112 | col_right.prop(wm.B2RUVL_PanelProperties, "uvMap") 113 | col_uvl = col_top.column(align=True) 114 | col_uvl.enabled = uvl is not False and len(uvl_path) != 0 115 | col_uvl.operator("b2ruvl.send_to_uvlayout") 116 | col_ruv = col_top.column(align=True) 117 | col_ruv.enabled = ruv is not False and len(ruv_path) != 0 118 | col_ruv.operator("b2ruvl.send_to_rizomuv") 119 | col_ruv.operator("b2ruvl.retake_rizomuv") 120 | 121 | # 8 - TOP 122 | if forgottentools and context.mode == "EDIT_MESH": 123 | other = pie.column() 124 | gap = other.column() 125 | gap.separator() 126 | gap.scale_y = 7 127 | other_menu = other.box().column() 128 | other_menu.scale_y = 1 129 | other_menu.label(text="ForgottenTools") 130 | other_menu.operator("forgotten.mesh_connect_spread") 131 | other_menu.operator("forgotten.mesh_grid_fill_all") 132 | 133 | other_menu.operator("forgotten.mesh_dice_faces") 134 | other_menu.operator("forgotten.mesh_hinge") 135 | 136 | other_menu.operator("mesh.forgotten_separate_duplicate") 137 | other_menu.operator( 138 | "wm.call_panel", text="Selection Sets", icon="SELECT_SET" 139 | ).name = "FORGOTTEN_PT_SelectionSetsPanel" 140 | else: 141 | pie.separator() 142 | 143 | # 7 - TOP - LEFT 144 | pie.separator() 145 | # 9 - TOP - RIGHT 146 | if optiloops and context.mode == "EDIT_MESH": 147 | pie.operator("mesh.optiloops") 148 | # pie.separator() 149 | 150 | # 1 - BOTTOM - LEFT 151 | pie.separator() 152 | 153 | # 3 - BOTTOM - RIGHT 154 | pie.separator() 155 | 156 | 157 | class IOPS_OT_Call_Pie_Menu(bpy.types.Operator): 158 | """IOPS Pie""" 159 | 160 | bl_idname = "iops.call_pie_menu" 161 | bl_label = "IOPS Pie Menu" 162 | 163 | def execute(self, context): 164 | bpy.ops.wm.call_menu_pie(name="IOPS_MT_Pie_Menu") 165 | return {"FINISHED"} 166 | -------------------------------------------------------------------------------- /utils/split_areas_dict.py: -------------------------------------------------------------------------------- 1 | split_areas_dict = { 2 | "Empty": { 3 | "type": "EMPTY", 4 | "ui": "EMPTY", 5 | "icon": "NONE", 6 | "num": 0 7 | }, 8 | "3D Viewport": { 9 | "type": "VIEW_3D", 10 | "ui": "VIEW_3D", 11 | "icon": "VIEW3D", 12 | "num": 1 13 | }, 14 | "Image Editor": { 15 | "type": "IMAGE_EDITOR", 16 | "ui": "IMAGE_EDITOR", 17 | "icon": "IMAGE", 18 | "num": 2, 19 | }, 20 | "UV Editor": { 21 | "type": "IMAGE_EDITOR", 22 | "ui": "UV", 23 | "icon": "UV", 24 | "num": 3 25 | }, 26 | "Shader Editor": { 27 | "type": "NODE_EDITOR", 28 | "ui": "ShaderNodeTree", 29 | "icon": "NODE_MATERIAL", 30 | "num": 4, 31 | }, 32 | "Compositor": { 33 | "type": "NODE_EDITOR", 34 | "ui": "CompositorNodeTree", 35 | "icon": "NODE_COMPOSITING", 36 | "num": 5, 37 | }, 38 | "Texture Node Editor": { 39 | "type": "NODE_EDITOR", 40 | "ui": "TextureNodeTree", 41 | "icon": "NODE_TEXTURE", 42 | "num": 6, 43 | }, 44 | "Video Sequencer": { 45 | "type": "SEQUENCE_EDITOR", 46 | "ui": "SEQUENCE_EDITOR", 47 | "icon": "SEQUENCE", 48 | "num": 7, 49 | }, 50 | "Movie Clip Editor": { 51 | "type": "CLIP_EDITOR", 52 | "ui": "CLIP_EDITOR", 53 | "icon": "TRACKER", 54 | "num": 8, 55 | }, 56 | "Dope Sheet": { 57 | "type": "DOPESHEET_EDITOR", 58 | "ui": "DOPESHEET", 59 | "icon": "ACTION", 60 | "num": 9, 61 | }, 62 | "Timeline": { 63 | "type": "DOPESHEET_EDITOR", 64 | "ui": "TIMELINE", 65 | "icon": "TIME", 66 | "num": 10, 67 | }, 68 | "Graph Editor": { 69 | "type": "GRAPH_EDITOR", 70 | "ui": "FCURVES", 71 | "icon": "GRAPH", 72 | "num": 11, 73 | }, 74 | "Drivers": { 75 | "type": "GRAPH_EDITOR", 76 | "ui": "DRIVERS", 77 | "icon": "DRIVER", 78 | "num": 12, 79 | }, 80 | "Nonlinear Animation": { 81 | "type": "NLA_EDITOR", 82 | "ui": "NLA_EDITOR", 83 | "icon": "NLA", 84 | "num": 13, 85 | }, 86 | "Text Editor": { 87 | "type": "TEXT_EDITOR", 88 | "ui": "TEXT_EDITOR", 89 | "icon": "TEXT", 90 | "num": 14, 91 | }, 92 | "Python Console": { 93 | "type": "CONSOLE", 94 | "ui": "CONSOLE", 95 | "icon": "CONSOLE", 96 | "num": 15, 97 | }, 98 | "Info": {"type": "INFO", 99 | "ui": "INFO", 100 | "icon": "INFO", 101 | "num": 16}, 102 | "Outliner": { 103 | "type": "OUTLINER", 104 | "ui": "OUTLINER", 105 | "icon": "OUTLINER", 106 | "num": 17, 107 | }, 108 | "Properties": { 109 | "type": "PROPERTIES", 110 | "ui": "PROPERTIES", 111 | "icon": "PROPERTIES", 112 | "num": 18, 113 | }, 114 | "File Browser": { 115 | "type": "FILE_BROWSER", 116 | "ui": "FILES", 117 | "icon": "FILEBROWSER", 118 | "num": 19, 119 | }, 120 | "Preferences": { 121 | "type": "PREFERENCES", 122 | "ui": "PREFERENCES", 123 | "icon": "PREFERENCES", 124 | "num": 20, 125 | }, 126 | "Geometry Nodes": { 127 | "type": "NODE_EDITOR", 128 | "ui": "GeometryNodeTree", 129 | "icon": "NODETREE", 130 | "num": 21, 131 | }, 132 | "Spreadsheet": { 133 | "type": "SPREADSHEET", 134 | "ui": "SPREADSHEET", 135 | "icon": "SPREADSHEET", 136 | "num": 22, 137 | }, 138 | "Asset Manager": { 139 | "type": "FILE_BROWSER", 140 | "ui": "ASSETS", 141 | "icon": "ASSET_MANAGER", 142 | "num": 23, 143 | }, 144 | } 145 | 146 | split_areas_list = [ 147 | (v["ui"], k, "", v["icon"], v["num"]) for k, v in split_areas_dict.items() 148 | ] 149 | 150 | split_areas_position_list = [ 151 | ("LEFT", "LEFT", "", "", 0), 152 | ("RIGHT", "RIGHT", "", "", 1), 153 | ("TOP", "TOP", "", "", 2), 154 | ("BOTTOM", "BOTTOM", "", "", 3), 155 | ] 156 | --------------------------------------------------------------------------------