├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── addon_preferences.py ├── blender_manifest.toml ├── classes_keymap_items.py ├── icons ├── K.png ├── Op.png ├── curve.png ├── diffrance.png ├── diffrance_old.png ├── editmode.png ├── ey.png ├── hole.png ├── intersection.png ├── intersection_old.png ├── mesh.png ├── mesh_cube.png ├── mesh_cube_selected.png ├── mesh_icosphere.png ├── mesh_icosphere2.png ├── mod_decim.png ├── modifier.png ├── polycount.png ├── retopo.png ├── s.png ├── slice.png ├── slice_old.png ├── union.png └── union_old.png ├── menu └── pies.py ├── operators ├── add_mesh.py ├── add_modifier.py ├── atri_op.py ├── auto_delete.py ├── auto_lod.py ├── auto_smooth.py ├── cad_decimate.py ├── double_click_select_island.py ├── extrude_edge_along_normals.py ├── fast_merge.py ├── legacy_shortcuts.py ├── material_index.py ├── maya_navigation.py ├── maya_pivot.py ├── maya_shortcuts.py ├── modi_key.py ├── none_live_booleans.py ├── origin_to_selection.py ├── outliner_options.py ├── poly_count_list.py ├── quick_bake_name.py ├── quick_export.py ├── rebind.py ├── smart_apply_scale.py ├── smart_seam.py ├── smart_uv_sync.py ├── toggle_retopology.py ├── toolkit_panel.py ├── unique_collection_duplicate.py ├── utilities_panel_op.py ├── uv_tools.py ├── viewport_menu.py └── zip_release.py ├── ui └── toolkit_paneL_ui.py └── utils ├── mesh_utils.py ├── pref_utils.py ├── register_extensions.py └── utilities.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Note - This is outdated, for Blender 4.2 and higher its aviable on the Blender Extension Platform: https://extensions.blender.org/add-ons/key-ops-toolkit/ 2 | 3 | Key Ops: Toolkit is my first add-on which I have been developing for almost one year. My goal with it is to make Blender more industri standard like, and add new tools that makes it more efficent to use. While it’s not 100% finished yet, it’s still mostly functional and will hopefully bring value to some in the Blender community. Enjoy! 4 | 5 | [Download Latest](https://github.com/Dangry98/Key-Ops-Toolkit/releases/latest/download/keyops-toolkit.zip) - 6 | [Documentation](https://key-ops-toolkit.notion.site/Key-Ops-Toolkit-Documentation-8683460f070542669f0dab4a92734dc9) - [BlenderArtist](https://blenderartists.org/t/key-ops-toolkit-0-1-82/1517413) 7 | 8 | 9 | ## Features 10 | (All added functions can be enabled/disabled by the user) 11 | 12 | **Auto Smooth like in 4.0** - Will add and move the Auto Smooth modifier to the bottom of the stack and sync the angle and settings between instances when the operation is used. 13 | 14 | 15 | **Maya** 16 | 17 | * Industri Standard Keymaps: 18 | * [Maya Navigation](https://key-ops-toolkit.notion.site/Maya-f9a3b12b0da24e82b6fe9f9ed01fdae3) - Adds navigation that works similar to Maya in the 3d view 19 | * [Double Click to Select Mesh Island](https://key-ops-toolkit.notion.site/Maya-f9a3b12b0da24e82b6fe9f9ed01fdae3) 20 | * [Auto Delete ](https://key-ops-toolkit.notion.site/Maya-f9a3b12b0da24e82b6fe9f9ed01fdae3)- Deletes Verts, Edges, Faces without any pop up Menu 21 | * [Maya Pivot](https://key-ops-toolkit.notion.site/Maya-f9a3b12b0da24e82b6fe9f9ed01fdae3) - Maya Like Pivot in Object Mode 22 | * [Toggle Retopology](https://key-ops-toolkit.notion.site/Maya-f9a3b12b0da24e82b6fe9f9ed01fdae3) - One Click Toggle Retopology Overlay + settings and tool 23 | 24 | 25 | **UV** 26 | * [UV Tools - ](https://key-ops-toolkit.notion.site/UV-faa2eddaa1cd440088a31f25aa23a2d8)A collections of tools for the UV editor 27 | 28 | **Unwrap In Place -** Unwrap Islands in place withoute packing it 29 | 30 | **Sharp Edges From UV Islands** - Add sharp based on UV Islands to fix shading on uv unwraped meshes. With the only UV Island Borders option it will not add any sharp edges to for example an uv seam on a cylinder. 31 | 32 | **UV Cut** - Cut an Island, works more similar to cut in Maya 33 | 34 | * [UV Pie - ](https://key-ops-toolkit.notion.site/UV-faa2eddaa1cd440088a31f25aa23a2d8)UV Editor WIP 35 | 36 | 37 | **Pie Menus** 38 | 39 | * [Add Object Pie](https://key-ops-toolkit.notion.site/Pie-Menu-e3eb5b5c1d85423da9f5bad8867791d7) - Will add objects relative to the viewport scale so the object is never too big/small. 40 | * [View Camera Pie](https://key-ops-toolkit.notion.site/Pie-Menu-e3eb5b5c1d85423da9f5bad8867791d7) 41 | * [Add Modifier Pie (WIP)](https://key-ops-toolkit.notion.site/Pie-Menu-e3eb5b5c1d85423da9f5bad8867791d7) 42 | * [Workspace Pie ](https://key-ops-toolkit.notion.site/Pie-Menu-e3eb5b5c1d85423da9f5bad8867791d7) 43 | * [Cursor Pie](https://key-ops-toolkit.notion.site/Pie-Menu-e3eb5b5c1d85423da9f5bad8867791d7) - More Useful cursor pie menu 44 | 45 | **Extra** 46 | 47 | * [Fast Merge ](https://key-ops-toolkit.notion.site/Extra-de3a011e64b2403a94eeb2d6bc2f12df)- Merges the active vertex to vertex nearest mouse position 48 | * [Modifier Key ](https://key-ops-toolkit.notion.site/Extra-de3a011e64b2403a94eeb2d6bc2f12df)- Adds more shortcuts to the modifier panel 49 | * [Attributes Operations ](https://key-ops-toolkit.notion.site/Extra-de3a011e64b2403a94eeb2d6bc2f12df)- Adds new buttons to the attributes panel that makes it more similar to how adding/removing vertex groups works. 50 | 51 | 52 | **Game Art Toolkit** 53 | 54 | * [CAD Decimate](https://key-ops-toolkit.notion.site/Game-Art-Toolkit-4b6f85e7504c4cf1bf7ece9a095d929c) - Tool to decimate very high poly meshes 55 | * [Auto LOD](https://key-ops-toolkit.notion.site/Game-Art-Toolkit-4b6f85e7504c4cf1bf7ece9a095d929c) - Tool to quickly create LODs WIP 56 | * [Quick Bake Name](https://key-ops-toolkit.notion.site/Game-Art-Toolkit-4b6f85e7504c4cf1bf7ece9a095d929c) - Tool to quickly create bake names for high and low poly objects 57 | * [Polycount List](https://key-ops-toolkit.notion.site/Game-Art-Toolkit-4b6f85e7504c4cf1bf7ece9a095d929c) - Get a list of all the objects and there poly count 58 | * [Utilities Panel](https://key-ops-toolkit.notion.site/Game-Art-Toolkit-4b6f85e7504c4cf1bf7ece9a095d929c) - Adds new operations to to the Toolkit Panel WIP 59 | * [Quick Export](https://key-ops-toolkit.notion.site/Game-Art-Toolkit-4b6f85e7504c4cf1bf7ece9a095d929c) - Exports the currently selected meshes 60 | * [Material Index](https://key-ops-toolkit.notion.site/Game-Art-Toolkit-4b6f85e7504c4cf1bf7ece9a095d929c) - Tool to sort Material Indexes after a list, useful for 3ds Max multi materials 61 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # ***** BEGIN GPL LICENSE BLOCK ***** 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 | # 16 | # ***** END GPL LICENSE BLOCK ***** 17 | 18 | from .addon_preferences import KeyOpsPreferences 19 | from .operators.rebind import Rebind 20 | from .utils.register_extensions import get_extension, register_classes, unregister_classes, register_keymaps, unregister_keymaps 21 | from .utils.pref_utils import register_icons, unregister_icons 22 | from .ui.toolkit_paneL_ui import NewToolkitPanel 23 | 24 | default_classes = [KeyOpsPreferences,Rebind, NewToolkitPanel] 25 | debug = False 26 | 27 | if 'bpy' in locals(): 28 | import os 29 | import importlib 30 | 31 | for cls in default_classes: 32 | importlib.reload(cls) 33 | 34 | from . import classes_keymap_items 35 | 36 | for module in [classes_keymap_items]: 37 | importlib.reload(module) 38 | 39 | utils_modules = sorted([name[:-3] for name in os.listdir(os.path.join(__path__[0], "utils")) if name.endswith('.py')]) 40 | 41 | for module in utils_modules: 42 | impline = f"from . utils import {module}" 43 | 44 | if debug: 45 | print(f"reloading {__package__}.utils.{module}") 46 | 47 | exec(impline) 48 | importlib.reload(eval(module)) 49 | 50 | modules = [] 51 | 52 | for label in classes_keymap_items.classes: 53 | entries = classes_keymap_items.classes[label] 54 | for entry in entries: 55 | path = entry[0].split('.') 56 | module = path.pop(-1) 57 | 58 | if (path, module) not in modules: 59 | modules.append((path, module)) 60 | 61 | for path, module in modules: 62 | if path: 63 | impline = f"from . {'.'.join(path)} import {module}" 64 | else: 65 | impline = f"from . import {module}" 66 | 67 | if debug: 68 | print(f"reloading {__package__}.{'.'.join(path)}.{module}") 69 | 70 | exec(impline) 71 | importlib.reload(eval(module)) 72 | 73 | import bpy 74 | 75 | def register(): 76 | global classes, keymaps, default_classes, icons 77 | 78 | if debug: 79 | import time 80 | addon_start_time = time.time() 81 | 82 | icons = register_icons() 83 | 84 | # Register default classes 85 | for cls in default_classes: 86 | bpy.utils.register_class(cls) 87 | 88 | # Get extension classes and keymaps 89 | extension_classlists, extension_keylists = get_extension() 90 | 91 | # Register extension classes 92 | classes = register_classes(extension_classlists) 93 | 94 | # Register extension keymaps 95 | keymaps = register_keymaps(extension_keylists) 96 | if debug: 97 | print(f"{__package__} addon registered in {time.time() - addon_start_time:.4f} seconds") 98 | 99 | 100 | #print all addons names that are enabled 101 | # for addon in bpy.context.preferences.addons: 102 | # print(addon.module) 103 | 104 | def unregister(): 105 | for cls in default_classes: 106 | bpy.utils.unregister_class(cls) 107 | 108 | global classes, keymaps, icons 109 | unregister_keymaps(keymaps) 110 | unregister_classes(classes) 111 | 112 | classes, keymaps = None, None 113 | 114 | unregister_icons(icons) 115 | -------------------------------------------------------------------------------- /blender_manifest.toml: -------------------------------------------------------------------------------- 1 | schema_version = "1.0.0" 2 | 3 | id = "Key_Ops_Toolkit" 4 | version = "0.2.198" 5 | name = "Key Ops: Toolkit" 6 | tagline = "Industri Standard Tools & Shortcuts to Speed Up Blender Workflow" 7 | maintainer = "MACHIN3, Dan-Gry" 8 | type = "add-on" 9 | 10 | website = "https://key-ops-toolkit.notion.site/Key-Ops-Toolkit-Documentation-8683460f070542669f0dab4a92734dc9" 11 | 12 | tags = ["Modeling", "Mesh", "Object", "UV", "3D View"] 13 | 14 | blender_version_min = "4.2.0" 15 | 16 | license = [ 17 | "SPDX:GPL-3.0-or-later", 18 | ] 19 | 20 | [permissions] 21 | files = "Export/Import .obj files and use of temp folder" 22 | -------------------------------------------------------------------------------- /icons/K.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/K.png -------------------------------------------------------------------------------- /icons/Op.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/Op.png -------------------------------------------------------------------------------- /icons/curve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/curve.png -------------------------------------------------------------------------------- /icons/diffrance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/diffrance.png -------------------------------------------------------------------------------- /icons/diffrance_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/diffrance_old.png -------------------------------------------------------------------------------- /icons/editmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/editmode.png -------------------------------------------------------------------------------- /icons/ey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/ey.png -------------------------------------------------------------------------------- /icons/hole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/hole.png -------------------------------------------------------------------------------- /icons/intersection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/intersection.png -------------------------------------------------------------------------------- /icons/intersection_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/intersection_old.png -------------------------------------------------------------------------------- /icons/mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/mesh.png -------------------------------------------------------------------------------- /icons/mesh_cube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/mesh_cube.png -------------------------------------------------------------------------------- /icons/mesh_cube_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/mesh_cube_selected.png -------------------------------------------------------------------------------- /icons/mesh_icosphere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/mesh_icosphere.png -------------------------------------------------------------------------------- /icons/mesh_icosphere2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/mesh_icosphere2.png -------------------------------------------------------------------------------- /icons/mod_decim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/mod_decim.png -------------------------------------------------------------------------------- /icons/modifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/modifier.png -------------------------------------------------------------------------------- /icons/polycount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/polycount.png -------------------------------------------------------------------------------- /icons/retopo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/retopo.png -------------------------------------------------------------------------------- /icons/s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/s.png -------------------------------------------------------------------------------- /icons/slice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/slice.png -------------------------------------------------------------------------------- /icons/slice_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/slice_old.png -------------------------------------------------------------------------------- /icons/union.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/union.png -------------------------------------------------------------------------------- /icons/union_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dangry98/Key-Ops-Toolkit/d69c877f837def480dbbd24c427e73a34782f02a/icons/union_old.png -------------------------------------------------------------------------------- /operators/add_mesh.py: -------------------------------------------------------------------------------- 1 | import bpy.types, bmesh 2 | from ..utils.pref_utils import get_keyops_prefs 3 | from bpy.types import Menu 4 | 5 | """ 6 | TODO: 7 | Add Support for Custom Meshes from .blend, fbx and bpy.ops 8 | """ 9 | 10 | def calculate_scale(context, scale_property, use_relative_scale, scale_relative_float, custom_scale_factor=1.0): 11 | prefs = get_keyops_prefs() 12 | if use_relative_scale: 13 | view_loc = context.space_data.region_3d.view_matrix.inverted().translation 14 | cursor_location = context.scene.cursor.location 15 | distance = (view_loc - cursor_location).length 16 | 17 | #Need to calculate the distance from the camera pivot center to the cursor in order to compensate for the camera pivot offset from the cursor, otherwise the scale will be off 18 | 19 | scale = distance / scale_relative_float * custom_scale_factor 20 | 21 | if scale >= 10.0: 22 | scale = round(scale / 5) * 5 #Round to nearest multiple of 5 23 | elif scale >= 1.0: 24 | scale = round(scale * 2) / 2 #Round to nearest 0.5 25 | else: 26 | scale = scale * 1.75 #Add an 75% extra scale to make it easier to see 27 | scale_levels = [1.0, 0.75, 0.5, 0.25, 0.1, 0.05, 0.025, 0.01, 0.005, 0.0025, 0.001] 28 | for level in scale_levels: 29 | if scale >= level: 30 | scale = level 31 | break 32 | #Make sure the scale is never 0 33 | min_scale = prefs.add_object_pie_min_scale 34 | if scale < min_scale: 35 | scale = min_scale 36 | else: 37 | scale = scale_property 38 | return scale 39 | 40 | def draw_scale_prop(self, layout): 41 | prop_name = "scale_relative_float" if self.use_relative_scale else "scale_property" 42 | row = layout.row() 43 | row.prop(self, prop_name) 44 | row.prop(self, "use_relative_scale") 45 | 46 | def AddModCylinder(context, segment_amount, scale, use_relative_scale, scale_relative_float, scale_property): 47 | if bpy.context.mode == 'EDIT_MESH': 48 | bpy.ops.object.mode_set(mode='OBJECT') 49 | 50 | bpy.ops.object.select_all(action='DESELECT') 51 | scale = calculate_scale(context, scale_property, use_relative_scale, scale_relative_float, custom_scale_factor=0.5) 52 | if use_relative_scale==False: 53 | scale = scale*0.5 54 | 55 | bm = bmesh.new() 56 | 57 | v1 = bm.verts.new((0 * scale, 0 * scale, -1 * scale)) 58 | v2 = bm.verts.new((1 * scale, 0 * scale, -1 * scale)) 59 | v3 = bm.verts.new((1 * scale, 0 * scale, 1 * scale)) 60 | v4 = bm.verts.new((0 * scale, 0 * scale, 1 * scale)) 61 | 62 | # Create edges 63 | bm.edges.new((v1, v2)) 64 | bm.edges.new((v4, v3)) 65 | bm.edges.new((v2, v3)) 66 | 67 | mesh = bpy.data.meshes.new("ModifierCylinder") 68 | bm.to_mesh(mesh) 69 | bm.free() 70 | 71 | # link it to the scene 72 | obj = bpy.data.objects.new("ModifierCylinder", mesh) 73 | bpy.context.collection.objects.link(obj) 74 | 75 | obj.location = bpy.context.scene.cursor.location 76 | 77 | bpy.context.view_layer.objects.active = obj 78 | obj.select_set(True) 79 | 80 | # add screw modifier 81 | screw_modifier = obj.modifiers.new(name="Cylinder", type='SCREW') 82 | screw_modifier.use_smooth_shade = True 83 | screw_modifier.steps = segment_amount 84 | screw_modifier.use_normal_calculate = True 85 | screw_modifier.use_normal_flip = True 86 | screw_modifier.use_merge_vertices = True 87 | screw_modifier.merge_threshold = scale * 0.1 88 | 89 | 90 | def AddQuadSphere(context, scale, use_relative_scale, scale_relative_float, scale_property, sub_amount, use_modifiers): 91 | if bpy.context.mode == 'EDIT_MESH': 92 | bpy.ops.object.mode_set(mode='OBJECT') 93 | 94 | bpy.ops.object.select_all(action='DESELECT') 95 | 96 | scale = calculate_scale(context, scale_property, use_relative_scale, scale_relative_float, custom_scale_factor=1.0) 97 | scale = scale * 1.168 # round it to full Meters 98 | 99 | mesh = bpy.data.meshes.new("QuadSphere") 100 | obj = bpy.data.objects.new("QuadSphere", mesh) 101 | bpy.context.collection.objects.link(obj) 102 | 103 | bm = bmesh.new() 104 | bmesh.ops.create_cube(bm, size=scale) 105 | 106 | for poly in bm.faces: 107 | poly.smooth = True 108 | 109 | bm.to_mesh(mesh) 110 | bm.free() 111 | 112 | obj.location = bpy.context.scene.cursor.location 113 | 114 | bpy.context.view_layer.objects.active = obj 115 | obj.select_set(True) 116 | 117 | subdive_modifier = obj.modifiers.new(name="Subdivision", type='SUBSURF') 118 | subdive_modifier.levels = sub_amount 119 | subdive_modifier.show_only_control_edges = False 120 | cast_modifier = obj.modifiers.new(name="Cast", type='CAST') 121 | cast_modifier.factor = 1.0 122 | 123 | if not use_modifiers: 124 | bpy.ops.object.convert(target='MESH') 125 | 126 | def AddCube(context, scale, use_relative_scale, scale_relative_float, scale_property): 127 | scale = calculate_scale(context, scale_property, use_relative_scale, scale_relative_float, custom_scale_factor=1.0) 128 | bpy.ops.mesh.primitive_cube_add(size=scale, enter_editmode=False, align='WORLD', scale=(1, 1, 1)) 129 | 130 | def AddPlane(context, scale, use_relative_scale, scale_relative_float, scale_property): 131 | scale = calculate_scale(context, scale_property, use_relative_scale, scale_relative_float, custom_scale_factor=1.0) 132 | bpy.ops.mesh.primitive_plane_add(size=scale) 133 | 134 | def AddUVSphere(context, scale, use_relative_scale, scale_relative_float, scale_property, segment_amount, rings_count): 135 | scale = calculate_scale(context, scale_property, use_relative_scale, scale_relative_float, custom_scale_factor=0.7) 136 | if use_relative_scale==False: 137 | scale = scale*0.5 138 | bpy.ops.mesh.primitive_uv_sphere_add(radius=scale, ring_count=rings_count, segments=segment_amount) 139 | 140 | def AddCircle(context, scale, use_relative_scale, scale_relative_float, scale_property, Vertices): 141 | scale = calculate_scale(context, scale_property, use_relative_scale, scale_relative_float, custom_scale_factor=1.0) 142 | bpy.ops.mesh.primitive_circle_add(radius=scale*0.5, vertices=Vertices) 143 | 144 | def AddMonkey(context, scale, use_relative_scale, scale_relative_float, scale_property): 145 | scale = calculate_scale(context, scale_property, use_relative_scale, scale_relative_float, custom_scale_factor=1.0) 146 | bpy.ops.mesh.primitive_monkey_add(size=scale) 147 | 148 | def AddCylinder(context, segment_amount, scale, use_relative_scale, scale_relative_float, scale_property): 149 | scale = calculate_scale(context, scale_property, use_relative_scale, scale_relative_float, custom_scale_factor=0.5) 150 | if use_relative_scale==False: 151 | scale = scale*0.5 152 | bpy.ops.mesh.primitive_cylinder_add(radius=scale, depth=scale*2, vertices=segment_amount) 153 | 154 | def AddEmpty(context, scale, use_relative_scale, scale_relative_float, scale_property): 155 | scale = calculate_scale(context, scale_property, use_relative_scale, scale_relative_float, custom_scale_factor=1.0) 156 | bpy.ops.object.empty_add(type='ARROWS', radius=scale) 157 | 158 | def Add_Blend(context, scale, use_relative_scale, scale_relative_float, scale_property): 159 | prefs = get_keyops_prefs() 160 | 161 | blendpath = prefs.add_object_pie_blend_file_path 162 | namelist = prefs.add_object_pie_blend_object_name 163 | #add a , after each name to import many objects at once! 164 | namelist = namelist.split(",") 165 | 166 | with bpy.data.libraries.load(blendpath, link=False) as (data_from, data_to): 167 | data_to.objects = [name for name in data_from.objects if name in namelist] 168 | 169 | bpy.ops.object.select_all(action='DESELECT') 170 | 171 | for obj in data_to.objects: 172 | bpy.context.scene.collection.objects.link(obj) 173 | obj.select_set(True) 174 | context.view_layer.objects.active = obj 175 | 176 | def Add_Custom(context, scale, use_relative_scale, scale_relative_float, scale_property): 177 | scale = calculate_scale(context, scale_property, use_relative_scale, scale_relative_float, custom_scale_factor=1.0) 178 | prefs = get_keyops_prefs() 179 | 180 | op = prefs.add_object_pie_bpy_ops 181 | 182 | exec(op) 183 | #obj.scale = (scale,scale,scale) 184 | 185 | class AddMesh(bpy.types.Operator): 186 | bl_description = "Add a mesh" 187 | bl_idname = "keyops.add_mesh" 188 | bl_label = "Add Mesh" 189 | bl_options = {"REGISTER", "UNDO"} 190 | prefs = get_keyops_prefs() 191 | 192 | mesh_type: bpy.props.EnumProperty( 193 | items=[ 194 | ("MODCYLINDER", "Modifier Cylinder", "Add a cylinder with a screw modifier"), 195 | ("QUADSPHERE", "Quad Sphere", "Add a Quad Sphere"), 196 | ("CUBE", "Cube", "Add a cube"), 197 | ("PLANE", "Plane", "Add a plane"), 198 | ("UVSPHERE", "UV Sphere", "Add a UV sphere"), 199 | ("CIRCLE", "Circle", "Add a circle"), 200 | ("MONKEY", "Monkey", "Add a monkey"), 201 | ("CYLINDER", "Cylinder", "Add a cylinder"), 202 | ("EMPTY", "Empty", "Add an empty"), 203 | ("BLEND", ".blend", "Add a object from a .blend file"), 204 | ("CUSTOM", "Custom", "Add a object with bpy.ops")], 205 | name="Mesh Type", default="CUBE") #type:ignore 206 | scale_property: bpy.props.FloatProperty(name="Size", default=prefs.add_object_pie_default_scale, min=0.1, max=10.0,description="Absolute scale of the mesh", unit='LENGTH') #type:ignore 207 | use_relative_scale: bpy.props.BoolProperty(name="Relative Scale", default=prefs.add_object_pie_use_relative) #type:ignore 208 | scale_relative_float: bpy.props.FloatProperty(name="Scale", default=prefs.add_object_pie_relative_scale, min=1.0, max=25.0, description="Relative scale of the mesh") #type:ignore 209 | segment_amount: bpy.props.IntProperty(name="Segments", default=32, min=3, max=256, description="Amount of segments the cylinder has") #type:ignore 210 | sub_amount: bpy.props.IntProperty(name="Subdive Amount", default=4, min=1, max=6) #type:ignore 211 | use_modifiers: bpy.props.BoolProperty(name="Use Modifiers", default=True) #type:ignore 212 | Vertices: bpy.props.IntProperty(name="Vertices",default=32,min=3,max=256, description="Amount of vertices the circle has") #type:ignore 213 | rings_count: bpy.props.IntProperty(name="Rings", default=16, min=3, max=256, description="Amount of rings the sphere has") #type:ignore 214 | 215 | def draw(self, context): 216 | layout = self.layout 217 | if self.mesh_type == "MODCYLINDER": 218 | layout.label(text="Modifier Cylinder", icon="MOD_SCREW") 219 | layout.prop(self, "segment_amount") 220 | elif self.mesh_type == "QUADSPHERE": 221 | layout.label(text="Quad Sphere", icon="MESH_UVSPHERE") 222 | layout.prop(self, "sub_amount") 223 | layout.prop(self, "use_modifiers") 224 | elif self.mesh_type == "CUBE": 225 | layout.label(text="Cube", icon="MESH_CUBE") 226 | elif self.mesh_type == "PLANE": 227 | layout.label(text="Plane", icon="MESH_PLANE") 228 | elif self.mesh_type == "UVSPHERE": 229 | layout.label(text="UV Sphere", icon="SHADING_WIRE") 230 | layout.prop(self, "segment_amount") 231 | layout.prop(self, "rings_count") 232 | elif self.mesh_type == "CIRCLE": 233 | layout.label(text="Circle", icon="MESH_CIRCLE") 234 | layout.prop(self, "Vertices") 235 | elif self.mesh_type == "MONKEY": 236 | layout.label(text="Monkey", icon="MESH_MONKEY") 237 | elif self.mesh_type == "CYLINDER": 238 | layout.label(text="Cylinder", icon="MESH_CYLINDER") 239 | layout.prop(self, "segment_amount") 240 | elif self.mesh_type == "EMPTY": 241 | layout.label(text="EMPTY", icon="EMPTY_ARROWS") 242 | elif self.mesh_type == "BLEND": 243 | layout.label(text=".blend", icon="BLENDER") 244 | elif self.mesh_type == "CUSTOM": 245 | layout.label(text="Custom", icon="FILE_SCRIPT") 246 | if not self.mesh_type == "BLEND": 247 | draw_scale_prop(self, layout) 248 | 249 | def execute(self, context): 250 | if self.mesh_type == "MODCYLINDER": 251 | AddModCylinder(context, self.segment_amount, self.scale_property, self.use_relative_scale, self.scale_relative_float, self.scale_property) 252 | elif self.mesh_type == "QUADSPHERE": 253 | AddQuadSphere(context, self.scale_property, self.use_relative_scale, self.scale_relative_float, self.scale_property, self.sub_amount, self.use_modifiers) 254 | elif self.mesh_type == "CUBE": 255 | AddCube(context, self.scale_property, self.use_relative_scale, self.scale_relative_float, self.scale_property) 256 | elif self.mesh_type == "PLANE": 257 | AddPlane(context, self.scale_property, self.use_relative_scale, self.scale_relative_float, self.scale_property) 258 | elif self.mesh_type == "UVSPHERE": 259 | AddUVSphere(context, self.scale_property, self.use_relative_scale, self.scale_relative_float, self.scale_property, self.segment_amount, self.rings_count) 260 | elif self.mesh_type == "CIRCLE": 261 | AddCircle(context, self.scale_property, self.use_relative_scale, self.scale_relative_float, self.scale_property, self.Vertices) 262 | elif self.mesh_type == "MONKEY": 263 | AddMonkey(context, self.scale_property, self.use_relative_scale, self.scale_relative_float, self.scale_property) 264 | elif self.mesh_type == "CYLINDER": 265 | AddCylinder(context, self.segment_amount, self.scale_property, self.use_relative_scale, self.scale_relative_float, self.scale_property) 266 | elif self.mesh_type == "EMPTY": 267 | if context.mode == 'EDIT_MESH': 268 | bpy.ops.object.editmode_toggle() 269 | AddEmpty(context, self.scale_property, self.use_relative_scale, self.scale_relative_float, self.scale_property) 270 | elif self.mesh_type == "BLEND": 271 | Add_Blend(context, self.scale_property, self.use_relative_scale, self.scale_relative_float, self.scale_property) 272 | return {'FINISHED'} 273 | def register(): 274 | bpy.utils.register_class(AddObjectsPie) 275 | def unregister(): 276 | bpy.utils.unregister_class(AddObjectsPie) 277 | 278 | class AddObjectsPie(Menu): 279 | bl_idname = "KEYOPS_MT_add_objects_pie_menu" 280 | bl_label = "Add Mesh Pie" 281 | 282 | def draw(self, context): 283 | layout = self.layout 284 | pie = layout.menu_pie() 285 | prefs = get_keyops_prefs() 286 | 287 | pie.operator("keyops.add_mesh", text="UV Sphere", icon="SHADING_WIRE").mesh_type = "UVSPHERE" # LEFT 288 | 289 | pie.operator("keyops.add_mesh", text="Cube", icon="MESH_CUBE").mesh_type = "CUBE" # RIGHT 290 | 291 | pie.operator("keyops.add_mesh", text="Cylinder", icon="MESH_CYLINDER").mesh_type = "CYLINDER" # BOTTOM 292 | 293 | pie.operator("keyops.add_mesh", text="Plane", icon="MESH_PLANE").mesh_type = "PLANE" # TOP 294 | 295 | pie.operator("keyops.add_mesh", text="Mod Cylinder", icon="MOD_SCREW").mesh_type = "MODCYLINDER" # LEFT TOP 296 | 297 | if prefs.add_object_pie_Enum == "EMPTY": 298 | pie.operator("keyops.add_mesh", text="Empty", icon="EMPTY_ARROWS").mesh_type = "EMPTY" # RIGHT TOP 299 | if prefs.add_object_pie_Enum == "MONKEY": 300 | pie.operator("keyops.add_mesh", text="Monkey", icon="MESH_MONKEY").mesh_type = "MONKEY" # RIGHT TOP 301 | if prefs.add_object_pie_Enum == "OTHER": 302 | if context.mode == 'EDIT_MESH': 303 | pie.operator("wm.call_menu", text="Other", icon="ADD").name = "VIEW3D_MT_mesh_add" # RIGHT TOP 304 | else: 305 | pie.operator("wm.call_menu", text="Other", icon="ADD").name = "VIEW3D_MT_add" # RIGHT TOP 306 | if prefs.add_object_pie_Enum == "BLEND": 307 | pie.operator("keyops.add_mesh", text=".blend", icon="BLENDER").mesh_type = "BLEND" # RIGHT TOP 308 | if prefs.add_object_pie_Enum == "CUSTOM": 309 | pie.operator("keyops.add_mesh", text="Custom", icon="FILE_SCRIPT").mesh_type = "CUSTOM" # RIGHT TOP 310 | 311 | pie.operator("keyops.add_mesh", text="Quad Sphere", icon="MESH_UVSPHERE").mesh_type = "QUADSPHERE" # LEFT BOTTOM 312 | 313 | pie.operator("keyops.add_mesh", text="Circle", icon="MESH_CIRCLE").mesh_type = "CIRCLE" # RIGHT BOTTOM 314 | 315 | -------------------------------------------------------------------------------- /operators/atri_op.py: -------------------------------------------------------------------------------- 1 | import bpy.types, bmesh 2 | 3 | def update_float_value(self, context): 4 | wm = bpy.context.window_manager 5 | bpy.ops.mesh.attribute_set('EXEC_DEFAULT', True, value_float=wm.float_value) 6 | 7 | def update_float_color_value(self, context): 8 | wm = bpy.context.window_manager 9 | bpy.ops.mesh.attribute_set('EXEC_DEFAULT', True, value_color=wm.float_color_value) 10 | 11 | def update_integer_value(self, context): 12 | wm = bpy.context.window_manager 13 | bpy.ops.mesh.attribute_set('EXEC_DEFAULT', True, value_int=wm.integer_value) 14 | 15 | def draw_custom_button(self, context): 16 | wm = bpy.context.window_manager 17 | 18 | if context.active_object.mode == 'EDIT': 19 | layout = self.layout 20 | row = layout.row(align=True) 21 | if bpy.context.object.data.attributes.active and bpy.context.object.data.attributes.active.data_type == "BOOLEAN": 22 | row.operator("keyops.atri_op", text="Assign").type = "Assign" 23 | row.operator("keyops.atri_op", text="Remove").type = "Remove" 24 | row.operator("keyops.atri_op", text="Select").type = "Select" 25 | if bpy.context.object.data.attributes.active and bpy.context.object.data.attributes.active.data_type == "FLOAT": 26 | row.prop(wm, "float_value", text="Float Value") 27 | if bpy.context.object.data.attributes.active and bpy.context.object.data.attributes.active.data_type == "FLOAT_COLOR": 28 | row.operator("keyops.atri_op", text="Assign").type = "Assign" 29 | row.operator("keyops.atri_op", text="Remove").type = "Remove" 30 | row.prop(wm, "float_color_value", text="") 31 | if bpy.context.object.data.attributes.active and bpy.context.object.data.attributes.active.data_type == "INT": 32 | # get active object and active face, get the integer value from the face 33 | # obj = bpy.context.active_object 34 | # bm = bmesh.from_edit_mesh(obj.data) 35 | # face = bm.select_history.active 36 | # if face: 37 | # layer = bm.faces.layers.int.get(bpy.context.object.data.attributes.active.name) 38 | # if layer: 39 | # wm.integer_value = face[layer] 40 | 41 | row.prop(wm, "integer_value", text="Integer Value") 42 | 43 | layout = self.layout 44 | row = layout.row(align=True) 45 | row.operator("keyops.atri_op", text="Vertex Group", icon="VERTEXSEL").type = "Vertex" 46 | row.operator("keyops.atri_op", text="Edge Group", icon="EDGESEL").type = "Edge" 47 | row.operator("keyops.atri_op", text="Face Group", icon="FACESEL").type = "Face" 48 | 49 | 50 | class AtriOP(bpy.types.Operator): 51 | bl_idname = "keyops.atri_op" 52 | bl_label = "KeyOps: Atri OP" 53 | bl_description = "Atributte Operations" 54 | bl_options = {'REGISTER', 'UNDO'} 55 | 56 | type: bpy.props.StringProperty(default="") # type:ignore 57 | 58 | @classmethod 59 | def poll(cls, context): 60 | return context.active_object is not None and context.active_object.mode == 'EDIT' 61 | 62 | def execute(self, context): 63 | wm = bpy.context.window_manager 64 | sel_mode = context.tool_settings.mesh_select_mode[:] 65 | active_type = bpy.context.object.data.attributes.active 66 | if self.type == "Assign": 67 | if active_type and active_type.data_type == "BOOLEAN": 68 | bpy.ops.mesh.attribute_set('EXEC_DEFAULT', True, value_bool=True) 69 | if active_type and active_type.data_type == "FLOAT": 70 | bpy.ops.mesh.attribute_set('EXEC_DEFAULT', True, value_float=wm.float_value) 71 | if active_type and active_type.data_type == "FLOAT_COLOR": 72 | bpy.ops.mesh.attribute_set('EXEC_DEFAULT', True, value_color=wm.float_color_value) 73 | 74 | elif self.type == "Remove": 75 | if active_type and active_type.data_type == "BOOLEAN": 76 | bpy.ops.mesh.attribute_set('EXEC_DEFAULT', True, value_bool=False) 77 | if active_type and active_type.data_type == "FLOAT": 78 | bpy.ops.mesh.attribute_set('EXEC_DEFAULT', True, value_float=0.0) 79 | if active_type and active_type.data_type == "FLOAT_COLOR": 80 | bpy.ops.mesh.attribute_set('EXEC_DEFAULT', True, value_color=(0.0, 0.0, 0.0, 0.0)) 81 | 82 | elif self.type == "Select": 83 | # need to change to the select mode first of the attribute, otherwise it will not work 84 | if active_type and active_type.domain == "POINT": 85 | context.tool_settings.mesh_select_mode = (True, False, False) 86 | elif active_type and active_type.domain == "EDGE": 87 | context.tool_settings.mesh_select_mode = (False, True, False) 88 | elif active_type and active_type.domain == "FACE": 89 | context.tool_settings.mesh_select_mode = (False, False, True) 90 | bpy.ops.mesh.select_by_attribute() 91 | 92 | elif self.type == "Vertex": 93 | context.tool_settings.mesh_select_mode = (True, False, False) 94 | bpy.ops.geometry.attribute_add(name="Vertex_Group", data_type='BOOLEAN', domain='POINT') 95 | bpy.ops.keyops.atri_op(type="Assign") 96 | context.tool_settings.mesh_select_mode = (sel_mode[0], sel_mode[1], sel_mode[2]) 97 | elif self.type == "Edge": 98 | context.tool_settings.mesh_select_mode = (False, True, False) 99 | bpy.ops.geometry.attribute_add(name="Edge_Group", data_type='BOOLEAN', domain='EDGE') 100 | bpy.ops.keyops.atri_op(type="Assign") 101 | context.tool_settings.mesh_select_mode = (sel_mode[0], sel_mode[1], sel_mode[2]) 102 | elif self.type == "Face": 103 | context.tool_settings.mesh_select_mode = (False, False, True) 104 | bpy.ops.geometry.attribute_add(name="Face_Group", data_type='BOOLEAN', domain='FACE') 105 | bpy.ops.keyops.atri_op(type="Assign") 106 | context.tool_settings.mesh_select_mode = (sel_mode[0], sel_mode[1], sel_mode[2]) 107 | 108 | return {'FINISHED'} 109 | 110 | def register(): 111 | bpy.types.DATA_PT_mesh_attributes.append(draw_custom_button) 112 | bpy.types.WindowManager.float_value = bpy.props.FloatProperty(name="Float Value", default=1.0, update=update_float_value) 113 | bpy.types.WindowManager.float_color_value = bpy.props.FloatVectorProperty(size=4, subtype="COLOR", name="Float Color Value", default=(1.0, 1.0, 1.0, 1.0), min=0.0, max=1.0, update=update_float_color_value) 114 | bpy.types.WindowManager.integer_value = bpy.props.IntProperty(name="Integer Value", default=0, update=update_integer_value) 115 | def unregister(): 116 | bpy.types.DATA_PT_mesh_attributes.remove(draw_custom_button) 117 | del bpy.types.WindowManager.float_value 118 | del bpy.types.WindowManager.float_color_value 119 | del bpy.types.WindowManager.integer_value 120 | -------------------------------------------------------------------------------- /operators/auto_delete.py: -------------------------------------------------------------------------------- 1 | import bpy.types 2 | from ..utils.pref_utils import get_keyops_prefs 3 | 4 | def remove_confirm_delete(): 5 | prefs = get_keyops_prefs() 6 | 7 | if not prefs.auto_delete_confirm_object_mode: 8 | # wm = bpy.context.window_manager 9 | # km = wm.keyconfigs.active.keymaps['Object Mode'] 10 | # km.keymap_items.new('object.delete', 'X', 'PRESS').properties.confirm = False 11 | 12 | #this does not work since the keymap is not aviable at startup 13 | for keymap in bpy.context.window_manager.keyconfigs.user.keymaps: 14 | if keymap.name == 'Object Mode': 15 | for keymap_item in keymap.keymap_items: 16 | if keymap_item.name == "Delete" and keymap_item.type == "X": 17 | if keymap_item.properties.confirm == True: 18 | keymap_item.properties.confirm = False 19 | # for keymap in bpy.context.window_manager.keyconfigs.user.keymaps: 20 | # if keymap.name == 'Object Mode': 21 | # for keymap_item in keymap.keymap_items: 22 | # if keymap_item.name == "Delete" and keymap_item.type == "X": 23 | # if keymap_item.properties.use_global == True: 24 | # keymap_item.properties.use_global = True 25 | # print("delete") 26 | 27 | def default_confirm_delete(): 28 | for keymap in bpy.context.window_manager.keyconfigs.user.keymaps: 29 | if keymap.name == 'Object Mode': 30 | for keymap_item in keymap.keymap_items: 31 | if keymap_item.name == "Delete" and keymap_item.type == "X": 32 | if keymap_item.properties.confirm == False: 33 | keymap_item.properties.confirm = True 34 | 35 | def update_auto_delete_confirm_delete(self, context): 36 | if self.auto_delete_confirm_object_mode: 37 | default_confirm_delete() 38 | else: 39 | remove_confirm_delete() 40 | 41 | class AutoDelete(bpy.types.Operator): 42 | bl_idname = "keyops.auto_delete" 43 | bl_label = "KeyOps: Auto Delete" 44 | bl_options = {'REGISTER', 'UNDO'} 45 | 46 | object_mode: bpy.props.BoolProperty( 47 | name="Object Mode", 48 | description="Delete objects in object mode", 49 | default=False 50 | ) # type:ignore 51 | 52 | @classmethod 53 | def poll(cls, context): 54 | return context.active_object is not None and context.active_object.mode == 'EDIT' 55 | 56 | # def invoke(self, context, event): 57 | # prefs = get_keyops_prefs() 58 | # if prefs.auto_delete_confirm_object_mode: 59 | # return context.window_manager.invoke_confirm(self, event) 60 | # return self.execute(context) 61 | 62 | 63 | def execute(self, context): 64 | if self.object_mode: 65 | # fast delete test 66 | import time 67 | start = time.time() 68 | for obj in bpy.context.selected_objects: 69 | for collection in list(obj.users_collection): 70 | collection.objects.unlink(obj) 71 | # bpy.data.objects.remove(obj) 72 | 73 | # bpy.data.batch_remove() 74 | # bpy.ops.object.delete(use_global=False, confirm=True) 75 | 76 | print("Time: ", time.time() - start) 77 | 78 | prefs = get_keyops_prefs() 79 | select_mode = context.tool_settings.mesh_select_mode 80 | if select_mode[0]: 81 | bpy.ops.mesh.delete(type='VERT') 82 | elif select_mode[1]: 83 | if prefs.auto_delete_dissolv_edge: 84 | bpy.ops.mesh.dissolve_edges() 85 | else: 86 | bpy.ops.mesh.delete(type='EDGE') 87 | elif select_mode[2]: 88 | bpy.ops.mesh.delete(type='FACE') 89 | else: 90 | self.report({'ERROR'}, "Invalid selection mode") 91 | 92 | return {'FINISHED'} 93 | 94 | def register(): 95 | remove_confirm_delete() 96 | 97 | def unregister(): 98 | default_confirm_delete() 99 | -------------------------------------------------------------------------------- /operators/auto_lod.py: -------------------------------------------------------------------------------- 1 | import bpy.types 2 | import math 3 | from ..utils.pref_utils import get_keyops_prefs, get_icon 4 | 5 | prefs = get_keyops_prefs() 6 | 7 | # to do: 8 | # add X symmetrize option for decimate modifier - DONE 9 | # orginize menu panel better 10 | # replace delete small face with delete by mesh island 11 | # use split edge node instead of modifer 12 | # place the lods in the current collection? 13 | # add auto smooth angle 14 | # add data transfere for normals - DONE 15 | # fix node group for delete small faces error and update to last version 16 | # Convert Deletesmall Faces/DeleteLooseEdges to function instead 17 | #option for planar decimation 18 | 19 | def delete_loose_edge_node_group(): 20 | delete_loose_edge = bpy.data.node_groups.new(type = 'GeometryNodeTree', name = "Delete Loose Edge") 21 | 22 | delete_loose_edge.is_modifier = True 23 | 24 | #initialize delete_loose_edge nodes 25 | #delete_loose_edge interface 26 | #Socket Geometry 27 | geometry_socket = delete_loose_edge.interface.new_socket(name = "Geometry", in_out='OUTPUT', socket_type = 'NodeSocketGeometry') 28 | geometry_socket.attribute_domain = 'POINT' 29 | 30 | #Socket Geometry 31 | geometry_socket_1 = delete_loose_edge.interface.new_socket(name = "Geometry", in_out='INPUT', socket_type = 'NodeSocketGeometry') 32 | geometry_socket_1.attribute_domain = 'POINT' 33 | 34 | 35 | #node Group Input 36 | group_input = delete_loose_edge.nodes.new("NodeGroupInput") 37 | group_input.name = "Group Input" 38 | 39 | #node Boolean Math 40 | boolean_math = delete_loose_edge.nodes.new("FunctionNodeBooleanMath") 41 | boolean_math.name = "Boolean Math" 42 | boolean_math.operation = 'NOT' 43 | #Boolean_001 44 | boolean_math.inputs[1].default_value = False 45 | 46 | #node Delete Geometry 47 | delete_geometry = delete_loose_edge.nodes.new("GeometryNodeDeleteGeometry") 48 | delete_geometry.name = "Delete Geometry" 49 | delete_geometry.domain = 'EDGE' 50 | delete_geometry.mode = 'ALL' 51 | 52 | #node Edge Neighbors 53 | edge_neighbors = delete_loose_edge.nodes.new("GeometryNodeInputMeshEdgeNeighbors") 54 | edge_neighbors.name = "Edge Neighbors" 55 | 56 | #node Group Output 57 | group_output = delete_loose_edge.nodes.new("NodeGroupOutput") 58 | group_output.name = "Group Output" 59 | group_output.is_active_output = True 60 | 61 | 62 | 63 | 64 | #Set locations 65 | group_input.location = (-340.0, 0.0) 66 | boolean_math.location = (-203.79037475585938, -102.6886215209961) 67 | delete_geometry.location = (-35.70556640625, 64.91195678710938) 68 | edge_neighbors.location = (-388.9553527832031, -148.6557159423828) 69 | group_output.location = (166.10665893554688, 65.12358093261719) 70 | 71 | #Set dimensions 72 | group_input.width, group_input.height = 140.0, 100.0 73 | boolean_math.width, boolean_math.height = 140.0, 100.0 74 | delete_geometry.width, delete_geometry.height = 140.0, 100.0 75 | edge_neighbors.width, edge_neighbors.height = 140.0, 100.0 76 | group_output.width, group_output.height = 140.0, 100.0 77 | 78 | #initialize delete_loose_edge links 79 | #group_input.Geometry -> delete_geometry.Geometry 80 | delete_loose_edge.links.new(group_input.outputs[0], delete_geometry.inputs[0]) 81 | #delete_geometry.Geometry -> group_output.Geometry 82 | delete_loose_edge.links.new(delete_geometry.outputs[0], group_output.inputs[0]) 83 | #boolean_math.Boolean -> delete_geometry.Selection 84 | delete_loose_edge.links.new(boolean_math.outputs[0], delete_geometry.inputs[1]) 85 | #edge_neighbors.Face Count -> boolean_math.Boolean 86 | delete_loose_edge.links.new(edge_neighbors.outputs[0], boolean_math.inputs[0]) 87 | return delete_loose_edge 88 | 89 | class AutoLODProperties(bpy.types.PropertyGroup): 90 | suffix: bpy.props.StringProperty(default="_LOD") # type: ignore 91 | iterativ_lod_suffix: bpy.props.BoolProperty(default=True) # type: ignore 92 | lod_parent_object: bpy.props.BoolProperty(default=False) # type: ignore 93 | apply_decimate_modifier: bpy.props.BoolProperty(default=False) # type: ignore 94 | amount_of_lods: bpy.props.IntProperty(default=3, min=1) # type: ignore 95 | lod_difference: bpy.props.FloatProperty(default=45.0, min=0.0, max=100.0) # type: ignore 96 | unlock_normals_on_all_lods: bpy.props.BoolProperty(default=False) # type: ignore 97 | edge_split: bpy.props.BoolProperty(default=False) # type: ignore 98 | edge_split_angle: bpy.props.FloatProperty(default=60.0) # type: ignore 99 | max_face_size: bpy.props.FloatProperty(default=0.0005) # type: ignore 100 | remove_non_manifold_faces: bpy.props.BoolProperty(default=False) # type: ignore 101 | multipler: bpy.props.FloatProperty(default=2.0) # type: ignore 102 | delete_loose_edges: bpy.props.BoolProperty(default=True) # type: ignore 103 | keep_symmetry_X: bpy.props.BoolProperty(default=False) # type: ignore 104 | transfere_normals: bpy.props.BoolProperty(default=True) # type: ignore 105 | transfere_edge_normals: bpy.props.BoolProperty(default=True) # type: ignore 106 | create_collection: bpy.props.BoolProperty(default=False) # type: ignore 107 | add_weighted_normals: bpy.props.BoolProperty(default=True) # type: ignore 108 | 109 | 110 | class AutoLOD(bpy.types.Operator): 111 | bl_idname = "keyops.auto_lod" 112 | bl_label = "Auto LOD" 113 | bl_description = "Auto LOD" 114 | bl_options = {'REGISTER', 'UNDO'} 115 | 116 | def execute(self, context): 117 | props = context.scene.auto_lod_props 118 | selected_objects = [obj for obj in context.selected_objects if obj.type == 'MESH'] 119 | suffix = props.suffix 120 | parent_object = props.lod_parent_object 121 | apply_decimate_modifier = props.apply_decimate_modifier 122 | amount_of_lods = props.amount_of_lods 123 | lod_difference = props.lod_difference / 100.0 124 | edge_split = props.edge_split 125 | max_face_size = props.max_face_size 126 | remove_non_manifold_faces = props.remove_non_manifold_faces 127 | multipler = props.multipler 128 | transfere_normals = props.transfere_normals 129 | transfere_edge_normals = props.transfere_edge_normals 130 | create_collection = props.create_collection 131 | add_weighted_normals = props.add_weighted_normals 132 | 133 | total_steps = len(selected_objects) * amount_of_lods 134 | current_step = 0 135 | 136 | # Begin progress bar 137 | wm = bpy.context.window_manager 138 | wm.progress_begin(0, total_steps) 139 | 140 | orginal_objects = selected_objects.copy() 141 | 142 | if props.iterativ_lod_suffix: 143 | for obj in orginal_objects: 144 | if "_LOD_" in obj.name: 145 | obj.name = obj.name[:-13] 146 | obj["LOD"] = str(0) 147 | 148 | 149 | if props.delete_loose_edges: 150 | if bpy.data.node_groups.get("Delete Loose Edge") is None: 151 | delete_loose_edge_node_group() 152 | 153 | for obj in selected_objects: 154 | for i in range(amount_of_lods): 155 | # Update progress cursor 156 | wm.progress_update(current_step) 157 | 158 | new_object = obj.copy() 159 | new_object.data = obj.data.copy() 160 | 161 | if not props.iterativ_lod_suffix: 162 | lod_name = f"{obj.name}{suffix}{(i + 1)}" 163 | else: 164 | start_prefix = "_LOD_" 165 | underscore = "_" 166 | 167 | lod_name = f"{obj.name}{start_prefix}{underscore * (i + 1)}{i + 1}{underscore * (6 - i)}" 168 | new_object["LOD"] = str(i + 1) 169 | 170 | new_object.name = lod_name 171 | 172 | if obj.type == 'MESH': 173 | bpy.context.view_layer.objects.active = obj 174 | bpy.context.collection.objects.link(new_object) 175 | 176 | if edge_split: 177 | split_edge_modifier = new_object.modifiers.new(name="EDGE_SPLIT", type="EDGE_SPLIT") 178 | split_edge_modifier.show_in_editmode = False 179 | split_angle_degrees = props.edge_split_angle 180 | split_angle_radians = math.radians(split_angle_degrees) 181 | split_edge_modifier.split_angle = split_angle_radians 182 | 183 | decimate_modifier = new_object.modifiers.new(name="Decimate", type='DECIMATE') 184 | decimate_modifier.ratio = (1 - lod_difference) ** (i + 1) 185 | if props.keep_symmetry_X: 186 | decimate_modifier.use_symmetry = True 187 | 188 | if props.unlock_normals_on_all_lods: 189 | bpy.ops.mesh.customdata_custom_splitnormals_clear() 190 | 191 | if apply_decimate_modifier: 192 | bpy.ops.object.modifier_apply(modifier="EDGE_SPLIT") 193 | bpy.ops.object.modifier_apply(modifier="Decimate") 194 | 195 | if parent_object: 196 | new_object.parent = obj 197 | new_object.matrix_parent_inverse = obj.matrix_world.inverted() 198 | 199 | bpy.context.view_layer.objects.active = new_object 200 | 201 | if props.delete_loose_edges: 202 | if bpy.context.object.modifiers.get("Delete Loose Edge") is None: 203 | bpy.context.object.modifiers.new('Delete Loose Edge', 'NODES') 204 | bpy.context.object.modifiers['Delete Loose Edge'].node_group = bpy.data.node_groups['Delete Loose Edge'] 205 | 206 | if remove_non_manifold_faces: 207 | bpy.ops.object.delete_small_faces() 208 | 209 | modifier = bpy.context.object.modifiers.get("Delete Small Faces") 210 | if modifier is not None: 211 | modifier["Input_2"] = max_face_size * (multipler ** i) 212 | else: 213 | print("Warning: 'Delete Small Faces' modifier not found.") 214 | 215 | if transfere_normals or transfere_edge_normals: 216 | mod = new_object.modifiers.new(type='DATA_TRANSFER', name='Transfere_Normals') 217 | mod.object = obj 218 | mod.use_object_transform = False 219 | 220 | if transfere_edge_normals: 221 | mod.data_types_edges = {'SHARP_EDGE'} 222 | 223 | if transfere_normals: 224 | mod.data_types_loops = {'CUSTOM_NORMAL'} 225 | 226 | if add_weighted_normals: 227 | mod = new_object.modifiers.new(type='WEIGHTED_NORMAL', name='Weighted Normals') 228 | mod.keep_sharp = True 229 | 230 | current_step += 1 231 | 232 | if apply_decimate_modifier: 233 | bpy.ops.object.modifier_apply(modifier="Decimate") 234 | bpy.ops.object.modifier_apply(modifier="EDGE_SPLIT") 235 | 236 | # End progress bar 237 | 238 | # Rename the original objects with the suffix 239 | for obj in orginal_objects: 240 | if obj.type == 'MESH': 241 | if props.iterativ_lod_suffix: 242 | obj.name = obj.name + "_LOD_0_______" 243 | else: 244 | obj.name = f"{obj.name}{suffix}0" 245 | 246 | # create collections for each orginal object and name them the same name as the orginal object - suffix 247 | if create_collection: 248 | for obj in orginal_objects: 249 | # remove the suffix from the name to get the collection name without suffix 250 | collection_name = obj.name[:-len(suffix)] 251 | collection = bpy.data.collections.new(collection_name) 252 | bpy.context.scene.collection.children.link(collection) 253 | wm.progress_end() 254 | 255 | return {'FINISHED'} 256 | def register(): 257 | bpy.utils.register_class(AutoLODProperties) 258 | bpy.utils.register_class(GenerateLODPanel) 259 | bpy.types.Scene.auto_lod_props = bpy.props.PointerProperty(type=AutoLODProperties) 260 | 261 | def unregister(): 262 | bpy.utils.unregister_class(AutoLODProperties) 263 | bpy.utils.unregister_class(GenerateLODPanel) 264 | del bpy.types.Scene.auto_lod_props 265 | 266 | def draw_lod_panel(self, context, draw_header=False): 267 | layout = self.layout 268 | box = layout.box() 269 | props = context.scene.auto_lod_props 270 | 271 | if draw_header: 272 | row = box.row() 273 | row.label(text="Generate LOD", icon_value=get_icon("mesh_icosphere2")) 274 | row = box.row() 275 | 276 | if not props.iterativ_lod_suffix: 277 | row.prop(props, "suffix") 278 | row.prop(props, "iterativ_lod_suffix", text="Auto") 279 | else: 280 | row.alignment = 'LEFT' 281 | row.label(text="suffix:") 282 | row.prop(props, "iterativ_lod_suffix", text="Auto") 283 | row = box.row(align=True) 284 | row.prop(props, "lod_parent_object", text="Parent") 285 | 286 | row.prop(props, "keep_symmetry_X", text="Symmetry") 287 | 288 | row = box.row() 289 | row.label(text="Transfere Normals:") 290 | row = box.row(align=True) 291 | row.prop(props, "transfere_normals", text="Face Normals", toggle=True) 292 | row.prop(props, "transfere_edge_normals", text="Edge Normals", toggle=True) 293 | 294 | row = box.row() 295 | row.prop(props, "add_weighted_normals", text="Weighted Normals") 296 | 297 | row = box.row() 298 | row.prop(props, "unlock_normals_on_all_lods", text="Unlock Normals") 299 | 300 | row = box.row() 301 | row.prop(props, "amount_of_lods", text="Amount of LODs") 302 | 303 | row = box.row() 304 | row.prop(props, "lod_difference", text="LOD Difference %") 305 | 306 | row = box.row() 307 | row.scale_y = 1.4 308 | row.operator("keyops.auto_lod", text="Generate LODs", icon='MESH_ICOSPHERE') 309 | 310 | class GenerateLODPanel(bpy.types.Panel): 311 | bl_idname = "OBJECT_PT_generate_lod" 312 | bl_label = "Generate LOD" 313 | bl_space_type = 'VIEW_3D' 314 | bl_region_type = 'UI' 315 | bl_category = 'Toolkit' 316 | bl_options = {'DEFAULT_CLOSED'} 317 | # bl_parent_id = "KEYOPS_PT_toolkit_panel" 318 | 319 | @classmethod 320 | def poll(cls, context): 321 | if context.mode == "OBJECT": 322 | return True 323 | 324 | def draw_header(self, context): 325 | layout = self.layout 326 | layout.label(text="", icon_value=get_icon("mesh_icosphere")) 327 | 328 | def draw(self, context): 329 | draw_lod_panel(self, context) -------------------------------------------------------------------------------- /operators/auto_smooth.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | class AutoSmooth(bpy.types.Operator): 4 | """Set auto smooth on selected objects""" 5 | bl_idname = "keyops.auto_smooth" 6 | bl_label = "Auto Smooth" 7 | bl_options = {'REGISTER', 'UNDO'} 8 | 9 | auto_smooth: bpy.props.BoolProperty( 10 | name="Auto Smooth", 11 | description="Auto Smooth", 12 | default=True 13 | ) #type: ignore 14 | 15 | angle: bpy.props.FloatProperty( 16 | name="Angle", 17 | description="Angle", 18 | default=0.785398, #45 degrees 19 | min=0, 20 | max=3.14159, 21 | subtype="ANGLE", 22 | ) #type: ignore 23 | 24 | ignore_sharp: bpy.props.BoolProperty( 25 | name="Ignore Sharp", 26 | description="Ignore Sharp", 27 | default=True 28 | ) #type: ignore 29 | 30 | # remove_existing_sharp: bpy.props.BoolProperty( 31 | # name="Remove Existing Sharp Edges", 32 | # description="Delete Sharp", 33 | # default=True 34 | # ) #type: ignore 35 | 36 | all_instances: bpy.props.BoolProperty( 37 | name="Apply on All Instances", 38 | description="Apply on All Instances", 39 | default=True 40 | ) #type: ignore 41 | 42 | move_to_bottom: bpy.props.BoolProperty( 43 | name="Move to Bottom", 44 | description="Move to Bottom", 45 | default=True 46 | ) #type: ignore 47 | 48 | @classmethod 49 | def poll(cls, context): 50 | return context.active_object is not None 51 | 52 | def execute(self, context): 53 | active_object = context.active_object 54 | selected_objects = context.selected_objects 55 | 56 | if "Smooth by Angle" not in bpy.data.node_groups: 57 | bpy.ops.object.modifier_add_node_group(asset_library_type='ESSENTIALS', asset_library_identifier="", relative_asset_identifier="geometry_nodes\\smooth_by_angle.blend\\NodeTree\\Smooth by Angle") 58 | active_object = context.active_object 59 | active_object.modifiers.remove(active_object.modifiers.get("Smooth by Angle")) 60 | 61 | if self.all_instances: 62 | for obj in context.selected_objects: 63 | instances = [o for o in bpy.data.objects if o.data == obj.data] 64 | for inst in instances: 65 | if inst not in selected_objects: 66 | selected_objects.extend([inst]) 67 | 68 | if self.auto_smooth: 69 | def add_smooth_modifier(obj): 70 | for m in obj.modifiers: 71 | if "Auto Smooth" in m.name: 72 | if m == obj.modifiers.get("!!Auto Smooth"): 73 | pass 74 | else: 75 | obj.modifiers.remove(m) 76 | 77 | if "!!Auto Smooth" in [m.name for m in obj.modifiers]: 78 | context.view_layer.objects.active = obj 79 | if self.move_to_bottom: 80 | if obj.modifiers[-1].name != "!!Auto Smooth": 81 | obj.modifiers.move(obj.modifiers.find("!!Auto Smooth"), len(obj.modifiers)-1) 82 | context.object.modifiers["!!Auto Smooth"]["Socket_2"] = self.angle 83 | context.object.modifiers["!!Auto Smooth"]["Socket_3"] = self.ignore_sharp 84 | 85 | if "Smooth by Angle" in [m.name for m in obj.modifiers]: 86 | obj.modifiers.remove(obj.modifiers.get("Smooth by Angle")) 87 | 88 | if "Smooth by Angle" in [m.name for m in obj.modifiers]: 89 | context.view_layer.objects.active = obj 90 | if self.move_to_bottom: 91 | if obj.modifiers[-1].name != "Smooth by Angle": 92 | obj.modifiers.move(obj.modifiers.find("Smooth by Angle"), len(obj.modifiers)-1) 93 | context.object.modifiers["Smooth by Angle"]["Input_1"] = self.angle 94 | context.object.modifiers["Smooth by Angle"]["Socket_1"] = self.ignore_sharp 95 | context.object.modifiers["Smooth by Angle"].show_group_selector = False 96 | 97 | elif "Smooth by Angle" not in [m.name for m in obj.modifiers] and "!!Auto Smooth" not in [m.name for m in obj.modifiers]: 98 | context.view_layer.objects.active = obj 99 | obj.modifiers.new ("Smooth by Angle", 'NODES') 100 | context.object.modifiers["Smooth by Angle"].node_group = bpy.data.node_groups["Smooth by Angle"] 101 | bpy.context.object.modifiers["Smooth by Angle"].show_group_selector = False 102 | 103 | if self.move_to_bottom: 104 | if obj.modifiers[-1].name != "Smooth by Angle": 105 | obj.modifiers.move(obj.modifiers.find("Smooth by Angle"), len(obj.modifiers)-1) 106 | context.object.modifiers["Smooth by Angle"]["Input_1"] = self.angle 107 | context.object.modifiers["Smooth by Angle"]["Socket_1"] = self.ignore_sharp 108 | 109 | 110 | for obj in selected_objects: 111 | add_smooth_modifier(obj) 112 | 113 | else: 114 | def remove_smooth_modifier(obj): 115 | context.view_layer.objects.active = obj 116 | if "!!Auto Smooth" in [m.name for m in obj.modifiers]: 117 | obj.modifiers.remove(obj.modifiers.get("!!Auto Smooth")) 118 | if "Smooth by Angle" in [m.name for m in obj.modifiers]: 119 | obj.modifiers.remove(obj.modifiers.get("Smooth by Angle")) 120 | if "Auto Smooth" in [m.name for m in obj.modifiers]: 121 | obj.modifiers.remove(obj.modifiers.get("Auto Smooth")) 122 | 123 | for obj in selected_objects: 124 | remove_smooth_modifier(obj) 125 | 126 | #refresh - profile to make sure it's not too slow in big scenes, ideally should be done only once at the first run 127 | if "Smooth by Angle" in bpy.data.node_groups: 128 | bpy.data.node_groups["Smooth by Angle"].interface.items_tree[2].subtype = 'ANGLE' 129 | if "Auto Smooth" in bpy.data.node_groups: 130 | bpy.data.node_groups["Auto Smooth"].interface.items_tree[2].subtype = 'ANGLE' 131 | 132 | context.view_layer.objects.active = active_object 133 | return {'FINISHED'} 134 | 135 | def register(): 136 | bpy.types.VIEW3D_MT_object_context_menu.prepend(draw_menu) 137 | def unregister(): 138 | bpy.types.VIEW3D_MT_object_context_menu.remove(draw_menu) 139 | 140 | def draw_menu(self, context): 141 | if context.active_object is not None: 142 | layout = self.layout 143 | layout.operator(AutoSmooth.bl_idname, text="Auto Smooth") 144 | -------------------------------------------------------------------------------- /operators/cad_decimate.py: -------------------------------------------------------------------------------- 1 | import bpy.types 2 | import os 3 | import time 4 | from ..utils.pref_utils import get_keyops_prefs, get_icon 5 | import tempfile 6 | 7 | prefs = get_keyops_prefs() 8 | totall_tris = 0 9 | 10 | class CADDecimate(bpy.types.Operator): 11 | bl_idname = "keyops.cad_decimate" 12 | bl_label = "Auto CAD Decimate" 13 | bl_description = "Auto CAD Decimate" 14 | bl_options = {'REGISTER', 'UNDO'} 15 | 16 | totall_time = 0 17 | 18 | def execute(self, context): 19 | totall_time = time.time() 20 | if bpy.context.scene.fast_mode == False: 21 | export_time = time.time() 22 | bpy.ops.keyops.export_cad_decimate() 23 | export_time = time.time() - export_time 24 | 25 | decimate_time = time.time() 26 | bpy.ops.keyops.import_cad_decimate() 27 | decimate_time = time.time() - decimate_time 28 | 29 | if bpy.context.scene.fast_mode == False: 30 | import_time = time.time() 31 | bpy.ops.keyops.import_all_cad_decimate() 32 | import_time = time.time() - import_time 33 | 34 | totall_time = time.time() - totall_time 35 | if bpy.context.scene.fast_mode == False: 36 | print(f"Exported in {export_time:.4f} seconds") 37 | print(f"Decimated in {decimate_time:.4f} seconds") 38 | print(f"Imported in {import_time:.4f} seconds") 39 | print(f"Total Time: {totall_time:.4f} seconds") 40 | 41 | return {'FINISHED'} 42 | 43 | def register(): 44 | bpy.utils.register_class(ExportCADDecimate) 45 | bpy.utils.register_class(ImportCADDecimate) 46 | bpy.utils.register_class(ImportAllCADDecimate) 47 | bpy.utils.register_class(CADDecimatePanel) 48 | def unregister(): 49 | bpy.utils.unregister_class(ExportCADDecimate) 50 | bpy.utils.unregister_class(ImportCADDecimate) 51 | bpy.utils.unregister_class(ImportAllCADDecimate) 52 | bpy.utils.unregister_class(CADDecimatePanel) 53 | 54 | class ExportCADDecimate(bpy.types.Operator): 55 | bl_idname = "keyops.export_cad_decimate" 56 | bl_label = "Export CAD Decimate" 57 | bl_description = "CAD Decimate" 58 | bl_options = {'REGISTER', 'UNDO'} 59 | 60 | export_time = 0 61 | 62 | @classmethod 63 | def poll(cls, context): 64 | return bpy.context.scene.fast_mode == False 65 | 66 | def execute(self, context): 67 | global totall_tris 68 | 69 | export_time = time.time() 70 | export_type = None 71 | 72 | if bpy.context.scene.fast_mode == False: 73 | if bpy.context.scene.export_selected_only: 74 | export_type = [obj for obj in context.selected_objects if obj.type == 'MESH' or obj.type == 'CURVE' or obj.type == 'FONT' or obj.type == 'SURFACE' or obj.type == 'META'] 75 | else: 76 | export_type = [obj for obj in context.scene.objects if obj.type == 'MESH' or obj.type == 'CURVE' or obj.type == 'FONT' or obj.type == 'SURFACE' or obj.type == 'META'] 77 | else: 78 | export_type = [obj for obj in context.selected_objects if obj.type == 'MESH' or obj.type == 'CURVE' or obj.type == 'FONT' or obj.type == 'SURFACE' or obj.type == 'META'] 79 | 80 | # if bpy.context.scene.procent_or_amount == 'AMOUNT': 81 | # for obj in export_type: 82 | # if obj.type == 'MESH' or obj.type == 'CURVE' or obj.type == 'SURFACE' or obj.type == 'FONT' or obj.type == 'META': 83 | # m = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh() 84 | # tris = len(m.loop_triangles) 85 | # totall_tris += tris 86 | 87 | bpy.ops.wm.console_toggle() 88 | output_directory = bpy.context.scene.cad_decimate_export_path 89 | os.makedirs(output_directory, exist_ok=True) 90 | bpy.ops.object.select_all(action='DESELECT') 91 | preserv_uv = bpy.context.scene.preserv_uvs 92 | 93 | for obj in export_type: 94 | if obj.type == 'MESH' or obj.type == 'CURVE' or obj.type == 'FONT' or obj.type == 'SURFACE' or obj.type == 'META': 95 | obj.select_set(True) 96 | 97 | export_file = os.path.join(output_directory, f"{obj.name}.obj") 98 | #export_file = os.path.join(output_directory, f"{obj.name}.ply") 99 | 100 | bpy.ops.wm.obj_export(filepath=export_file, export_selected_objects=True, export_uv=preserv_uv, export_materials=False) 101 | #bpy.ops.wm.ply_export(filepath=export_file, export_selected_objects=True, export_uv=preserv_uv, ascii_format=True) 102 | 103 | bpy.ops.object.delete() 104 | if bpy.context.scene.auto_clear_garbage_data: 105 | bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) 106 | export_time = time.time() - export_time 107 | print(f"Exported in {export_time:.4f} seconds") 108 | 109 | return {'FINISHED'} 110 | 111 | #High Quality (Slow) 112 | # OBJ = 113 | # Exported in 92.2890 seconds 114 | # Decimated in 587.5323 seconds 115 | # Imported in 39.3740 seconds 116 | # Total Time: 719.1953 seconds 117 | 118 | 119 | # PLY = 120 | # Exported in 58.7132 seconds 121 | # Decimated in 624.0251 seconds 122 | # Imported in 46.0575 seconds 123 | # Total Time: 728.7957 seconds 124 | 125 | 126 | #Medium Quality (Fast) 127 | # OBJ = 128 | # Exported in 72.8535 seconds 129 | # Decimated in 91.0822 seconds 130 | # Imported in 29.1319 seconds 131 | # Total Time: 193.0676 seconds 132 | 133 | #PLY = 134 | # Exported in 86.0192 seconds 135 | # Decimated in 53.5972 seconds 136 | # Imported in 36.6675 seconds 137 | # Total Time: 176.2838 seconds 138 | 139 | def high_quality_decimate(): 140 | global totall_tris 141 | 142 | bpy.ops.object.modifier_add(type='DECIMATE') 143 | if bpy.context.scene.procent_or_amount == 'PROCENT': 144 | bpy.context.object.modifiers["Decimate"].ratio = bpy.context.scene.decimation_ratio 145 | else: 146 | bpy.context.object.modifiers["Decimate"].ratio = bpy.context.scene.tris_count / len(bpy.context.object.data.loop_triangles) 147 | def medium_quality_decimate(): 148 | bpy.ops.object.modifier_add(type='WELD') 149 | bpy.context.object.modifiers["Weld"].merge_threshold = bpy.context.scene.merge_distance 150 | def low_quality_decimate(): 151 | weld_modifier = bpy.context.object.modifiers.new(name="Weld", type='WELD') 152 | weld_modifier.mode = 'CONNECTED' 153 | weld_modifier.merge_threshold = bpy.context.scene.merge_distance 154 | 155 | class ImportCADDecimate(bpy.types.Operator): 156 | bl_idname = "keyops.import_cad_decimate" 157 | bl_label = "Import CAD Decimate" 158 | bl_description = "Import and Decimate, then Export" 159 | bl_options = {'REGISTER', 'UNDO'} 160 | 161 | decimate_time = 0 162 | decimate_type = None 163 | 164 | def execute(self, context): 165 | global totall_tris 166 | 167 | export_time = time.time() 168 | output_directory = bpy.context.scene.cad_decimate_export_path 169 | os.makedirs(output_directory, exist_ok=True) 170 | 171 | 172 | # export_type = [obj for obj in context.selected_objects if obj.type == 'MESH' or obj.type == 'CURVE' or obj.type == 'FONT' or obj.type == 'SURFACE' or obj.type == 'META'] 173 | 174 | # if bpy.context.scene.procent_or_amount == 'AMOUNT' and bpy.context.scene.fast_mode == True: 175 | # for obj in export_type: 176 | # if obj.type == 'MESH' or obj.type == 'CURVE' or obj.type == 'SURFACE' or obj.type == 'FONT' or obj.type == 'META': 177 | # m = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh() 178 | # tris = len(m.loop_triangles) 179 | # totall_tris += tris 180 | 181 | # print(totall_tris) 182 | 183 | preserv_uv = bpy.context.scene.preserv_uvs 184 | 185 | if bpy.context.scene.deceimate_type == 'DECIMATE': 186 | self.decimate_type = high_quality_decimate 187 | elif bpy.context.scene.deceimate_type == 'WELD': 188 | self.decimate_type = medium_quality_decimate 189 | elif bpy.context.scene.deceimate_type == 'CONNECT': 190 | self.decimate_type = low_quality_decimate 191 | if bpy.context.scene.fast_mode == False: 192 | 193 | for file_name in os.listdir(output_directory): 194 | if file_name.endswith(".obj"): 195 | input_file = os.path.join(output_directory, file_name) 196 | output_file = os.path.join(output_directory, file_name) 197 | 198 | bpy.ops.wm.obj_import(filepath=input_file) 199 | 200 | self.decimate_type() 201 | 202 | #bpy.ops.wm.ply_export(filepath=output_file, export_selected_objects=True, export_uv=preserv_uv, ascii_format=True) 203 | bpy.ops.wm.obj_export(filepath=output_file, export_selected_objects=True, export_uv=preserv_uv, export_materials=False) 204 | 205 | 206 | bpy.ops.object.delete() 207 | else: 208 | selection = [obj for obj in context.selected_objects if obj.type == 'MESH' or obj.type == 'CURVE' or obj.type == 'FONT' or obj.type == 'SURFACE' or obj.type == 'META'] 209 | for o in selection: 210 | bpy.context.view_layer.objects.active = o 211 | self.decimate_type() 212 | 213 | if bpy.context.scene.auto_clear_garbage_data: 214 | bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) 215 | decimate_time = time.time() - export_time 216 | print(f"Decimated in {decimate_time:.4f} seconds") 217 | 218 | return {'FINISHED'} 219 | 220 | class ImportAllCADDecimate(bpy.types.Operator): 221 | bl_idname = "keyops.import_all_cad_decimate" 222 | bl_label = "Import CAD Decimate" 223 | bl_description = "Import all in folder" 224 | bl_options = {'REGISTER', 'UNDO'} 225 | 226 | @classmethod 227 | def poll(cls, context): 228 | return bpy.context.scene.fast_mode == False 229 | 230 | def execute(self, context): 231 | output_directory = bpy.context.scene.cad_decimate_export_path 232 | os.makedirs(output_directory, exist_ok=True) 233 | 234 | imported_files = [] 235 | 236 | for file_name in os.listdir(output_directory): 237 | if file_name.endswith(".obj"): 238 | input_file = os.path.join(output_directory, file_name) 239 | imported_files.append(file_name) 240 | 241 | bpy.ops.wm.obj_import(filepath=input_file, directory=output_directory, files=[{"name": file_name} for file_name in imported_files]) 242 | 243 | if bpy.context.scene.delete_files_after_import: 244 | for file_name in os.listdir(output_directory): 245 | if file_name.endswith(".obj"): 246 | input_file = os.path.join(output_directory, file_name) 247 | imported_files.append(file_name) 248 | os.remove(input_file) 249 | 250 | bpy.ops.wm.console_toggle() 251 | return {'FINISHED'} 252 | 253 | 254 | class KEYOPS_PT_cad_decimate_panel_manual(bpy.types.Panel): 255 | bl_parent_id = "KEYOPS_PT_cad_decimate_panel" 256 | bl_label = "Manual Decimation" 257 | bl_space_type = 'VIEW_3D' 258 | bl_region_type = 'UI' 259 | bl_category = 'Toolkit' 260 | bl_options = {'DEFAULT_CLOSED'} 261 | 262 | @classmethod 263 | def poll(cls, context): 264 | if context.mode == "OBJECT" and bpy.context.scene.fast_mode == False: 265 | return True 266 | 267 | def draw(self, context): 268 | layout = self.layout 269 | layout.operator("keyops.export_cad_decimate", text="Export") 270 | layout.operator("keyops.import_cad_decimate", text="CAD Decimate") 271 | layout.operator("keyops.import_all_cad_decimate", text="Import All") 272 | 273 | def draw_cad_decimate_panel(self, context, draw_header=False): 274 | layout = self.layout 275 | box = layout.box() 276 | 277 | if get_keyops_prefs().enable_cad_decimate: 278 | if draw_header: 279 | box.label(text="CAD Decimate", icon_value=get_icon("mod_decim")) 280 | box.prop(context.scene, "deceimate_type", text="") 281 | if context.scene.deceimate_type != 'DECIMATE': 282 | box.label(text="Does not preserv UVs", icon='ERROR') 283 | box.prop(context.scene, "fast_mode", text="Enable Fast Mode", toggle=True) 284 | col = box.column(align=True) 285 | row = col.row(align=True) 286 | if bpy.context.scene.deceimate_type == 'DECIMATE': 287 | row.prop(context.scene, "procent_or_amount", text="Procent or Amount", expand=True) 288 | row = col.row(align=True) 289 | if bpy.context.scene.procent_or_amount == 'PROCENT': 290 | row.prop(context.scene, "decimation_ratio", text="Decimation %") 291 | else: 292 | row.prop(context.scene, "tris_count", text="Target Tris:") 293 | else: 294 | box.prop(context.scene, "merge_distance", text="Merge Amount") 295 | if bpy.context.scene.fast_mode == False: 296 | box.prop(context.scene, "export_selected_only", text="Selected Only") 297 | if bpy.context.scene.fast_mode == False and bpy.context.scene.deceimate_type == 'DECIMATE': 298 | box.prop(context.scene, "preserv_uvs", text="Preserve UVs") 299 | if bpy.context.scene.fast_mode == False: 300 | box.prop(context.scene, "delete_files_after_import", text="Delete Files After Import") 301 | box.prop(context.scene, "auto_clear_garbage_data", text="Clear Mesh Data") 302 | box.prop(context.scene, "cad_decimate_export_path", text="Path") 303 | 304 | row = box.row() 305 | row.operator("keyops.cad_decimate", text="CAD Decimate", icon='MOD_DECIM') 306 | row.scale_y = 1.4 307 | class CADDecimatePanel(bpy.types.Panel): 308 | bl_description = "CAD Decimate Panel" 309 | bl_label = "CAD Decimate" 310 | bl_idname = "KEYOPS_PT_cad_decimate_panel" 311 | bl_space_type = 'VIEW_3D' 312 | bl_region_type = 'UI' 313 | bl_category = 'Toolkit' 314 | bl_options = {'DEFAULT_CLOSED'} 315 | # bl_parent_id = "KEYOPS_PT_toolkit_panel" 316 | 317 | def draw_header(self, context): 318 | layout = self.layout 319 | layout.label(text="", icon_value=get_icon("mod_decim")) 320 | 321 | 322 | @classmethod 323 | def poll(cls, context): 324 | if context.mode == "OBJECT": 325 | return True 326 | 327 | def draw(self, context): 328 | draw_cad_decimate_panel(self, context, draw_header=False) 329 | 330 | def register(): 331 | bpy.utils.register_class(KEYOPS_PT_cad_decimate_panel_manual) 332 | bpy.types.Scene.auto_clear_garbage_data = bpy.props.BoolProperty(name="Auto Clear Garbage Data", default=True, description="Auto Clear Mesh Data Blocks (Saves a lot of Memory, but is slower)") 333 | bpy.types.Scene.preserv_uvs = bpy.props.BoolProperty(name="Preserve UVs",default=True, description="Preserve UVs") 334 | bpy.types.Scene.cad_decimate_export_path = bpy.props.StringProperty(name="Path",subtype='FILE_PATH',description="Path for exporting files",default=os.path.join(tempfile.gettempdir(),"CAD_Decimate")) 335 | bpy.types.Scene.decimation_ratio = bpy.props.FloatProperty(name="Decimation Ratio",default=0.2, min=0.0,max=1.0,subtype='FACTOR',description="Decimation Ratio") 336 | bpy.types.Scene.delete_files_after_import = bpy.props.BoolProperty(name="Delete Files After Import", default=True, description="Delete Files After Import") 337 | bpy.types.Scene.export_selected_only = bpy.props.BoolProperty(name="Export Selected Only",default=True,description="Export Selected Only") 338 | bpy.types.Scene.deceimate_type = bpy.props.EnumProperty(name="deceimate_type", 339 | items=[('DECIMATE', 'High Quality (Slow)', 'High Quality, but slow'), 340 | ('WELD', 'Medium Quality (Fast)', 'Medium Quality, does not preserv UVs'), 341 | ('CONNECT', 'Low Quality (Fastest)', 'Low Quality, very fast! But does not preserv UVs')], 342 | default='DECIMATE',description="Decimate Quality") 343 | bpy.types.Scene.merge_distance = bpy.props.FloatProperty(name="Merge Distance",default=0.02,min=0.001,max=1.0,subtype='FACTOR',description="Merge Distance (higher value = more decimation)") 344 | bpy.types.Scene.format = bpy.props.EnumProperty(name="format", 345 | items=[('OBJ', 'OBJ(Reliable, but slower)', 'OBJ'), 346 | ('PLY', 'PLY (Faster, but more error prone)', 'PLY'), 347 | ('COLLADA', 'COLLADA (Fastest, but always triangulated)', 'COLLADA')], 348 | default='COLLADA',description="Format") 349 | bpy.types.Scene.fast_mode = bpy.props.BoolProperty(default=True, description="Disabling 'Fast Mode' is Slower, but it can handle way more polygons without crashing Blender") 350 | bpy.types.Scene.procent_or_amount = bpy.props.EnumProperty(name="Procent or Amount", 351 | items=[('PROCENT', 'By Procent', 'By Procent'), 352 | ('AMOUNT', 'By Tris Count', 'By Tris Amount')], 353 | default='PROCENT',description="Procent or Amount") 354 | bpy.types.Scene.tris_count = bpy.props.IntProperty(name="Target Tris Count",default=10000,min=1,description="Target Tris Count") 355 | 356 | def unregister(): 357 | bpy.utils.unregister_class(KEYOPS_PT_cad_decimate_panel_manual) 358 | del bpy.types.Scene.auto_clear_garbage_data 359 | del bpy.types.Scene.preserv_uvs 360 | del bpy.types.Scene.cad_decimate_export_path 361 | del bpy.types.Scene.decimation_ratio 362 | del bpy.types.Scene.delete_files_after_import 363 | del bpy.types.Scene.export_selected_only 364 | del bpy.types.Scene.deceimate_type 365 | del bpy.types.Scene.merge_distance 366 | del bpy.types.Scene.format 367 | del bpy.types.Scene.fast_mode 368 | del bpy.types.Scene.procent_or_amount 369 | del bpy.types.Scene.tris_count 370 | 371 | -------------------------------------------------------------------------------- /operators/double_click_select_island.py: -------------------------------------------------------------------------------- 1 | import bpy.types 2 | 3 | class DoubleClickSelectIsland(bpy.types.Operator): 4 | bl_idname = "keyops.double_click_select_island" 5 | bl_label = "KeyOps: Double Click Select Island" 6 | bl_description = "" 7 | bl_options = {'INTERNAL', 'UNDO'} 8 | 9 | def execute(self, context): 10 | if context.active_object is not None and context.active_object.type == 'MESH': 11 | if bpy.context.scene.tool_settings.use_uv_select_sync: 12 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') 13 | bpy.ops.uv.select_linked_pick('INVOKE_DEFAULT') 14 | elif context.active_object is not None and context.active_object.type == 'CURVE': 15 | bpy.ops.curve.select_linked_pick('INVOKE_DEFAULT') 16 | elif context.active_object is not None and context.active_object.type == 'CURVES': 17 | bpy.ops.curves.select_linked_pick('INVOKE_DEFAULT') 18 | return {'FINISHED'} 19 | 20 | def register(): 21 | bpy.utils.register_class(SelectEdgeLoop) 22 | bpy.utils.register_class(SelectEdgeLoopShift) 23 | def unregister(): 24 | bpy.utils.unregister_class(SelectEdgeLoop) 25 | bpy.utils.unregister_class(SelectEdgeLoopShift) 26 | 27 | class SelectEdgeLoop(bpy.types.Operator): 28 | bl_idname = "keyops.select_edge_loop" 29 | bl_label = "KeyOps: Select Edge Loop" 30 | bl_options = {'INTERNAL', 'UNDO'} 31 | 32 | @classmethod 33 | def poll(cls, context): 34 | return context.active_object is not None and context.active_object.type == 'MESH' and context.scene.tool_settings.mesh_select_mode[1] 35 | 36 | def execute(self, context): 37 | bpy.ops.mesh.loop_select("INVOKE_DEFAULT", extend=True) 38 | return {'FINISHED'} 39 | 40 | 41 | #needs a seperate class, registering two keymaps of the same operator causes issues for some reason 42 | class SelectEdgeLoopShift(bpy.types.Operator): 43 | bl_idname = "keyops.select_edge_loop_shift" 44 | bl_label = "KeyOps: Select Edge Loop Shift" 45 | bl_options = {'INTERNAL', 'UNDO'} 46 | 47 | @classmethod 48 | def poll(cls, context): 49 | return context.active_object is not None and context.active_object.type == 'MESH' and context.scene.tool_settings.mesh_select_mode[1] 50 | 51 | def execute(self, context): 52 | bpy.ops.mesh.loop_select("INVOKE_DEFAULT", extend=True) 53 | return {'FINISHED'} 54 | -------------------------------------------------------------------------------- /operators/extrude_edge_along_normals.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | class ExtrudeEdgeAlongNormals(bpy.types.Operator): 4 | """Extrude selected edges along their normals""" 5 | bl_idname = "keyops.extrude_edge_along_normals" 6 | bl_label = "Extrude Edge Along Normals" 7 | bl_options = {'REGISTER', 'UNDO'} 8 | 9 | amount: bpy.props.FloatProperty( 10 | name="Amount", 11 | description="Amount", 12 | default=0.1, 13 | min=-100, 14 | max=100, 15 | subtype="DISTANCE", 16 | ) #type: ignore 17 | 18 | def execute(self, context): 19 | bpy.ops.mesh.offset_edge_loops_slide(MESH_OT_offset_edge_loops={"use_cap_endpoint":False}, TRANSFORM_OT_edge_slide={"value":0.00001, "single_side":True, "use_even":False, "flipped":False, "use_clamp":True, "mirror":True, "snap":False, "snap_elements":{'INCREMENT', 'VERTEX'}, "use_snap_project":False, "snap_target":'CLOSEST', "use_snap_self":True, "use_snap_edit":True, "use_snap_nonedit":True, "use_snap_selectable":False, "snap_point":(0, 0, 0), "correct_uv":True, "release_confirm":False, "use_accurate":False, "alt_navigation":False}) 20 | bpy.ops.mesh.loop_to_region() 21 | bpy.ops.mesh.loop_multi_select(ring=False) 22 | bpy.ops.mesh.select_all(action='INVERT') 23 | bpy.ops.transform.shrink_fatten(value=self.amount, use_even_offset=False, mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False, snap=False) 24 | 25 | return {'FINISHED'} 26 | 27 | def register(): 28 | bpy.types.VIEW3D_MT_edit_mesh_extrude.prepend(draw_menu) 29 | def unregister(): 30 | bpy.types.VIEW3D_MT_edit_mesh_extrude.remove(draw_menu) 31 | 32 | def draw_menu(self, context): 33 | self.layout.operator("keyops.extrude_edge_along_normals") -------------------------------------------------------------------------------- /operators/fast_merge.py: -------------------------------------------------------------------------------- 1 | import bpy.types 2 | import bmesh 3 | from mathutils import Vector 4 | from bpy_extras.view3d_utils import location_3d_to_region_2d 5 | from ..utils.pref_utils import get_keyops_prefs 6 | from ..utils.pref_utils import get_is_addon_enabled 7 | 8 | has_clicked_once = None 9 | half_knife = None 10 | 11 | def find_nearest_visible_vertex_to_mouse(context, mouse_position, vertices, matrix_world): 12 | def is_vertex_visible(vertex): 13 | world_position = matrix_world @ vertex.co 14 | screen_pos = location_3d_to_region_2d(context.region, context.space_data.region_3d, world_position) 15 | return screen_pos is not None 16 | 17 | def distance_to_mouse(vertex): 18 | world_position = matrix_world @ vertex.co 19 | screen_pos = location_3d_to_region_2d(context.region, context.space_data.region_3d, world_position) 20 | return (mouse_position - screen_pos).length if screen_pos else float('inf') 21 | 22 | return min(filter(is_vertex_visible, vertices), key=distance_to_mouse) 23 | 24 | 25 | class FastMerge(bpy.types.Operator): 26 | bl_idname = "keyops.fast_merge" 27 | bl_label = "Fast Merge" 28 | bl_description = "Fast Merge" 29 | bl_options = {'REGISTER', 'UNDO'} 30 | 31 | preserve_uvs: bpy.props.BoolProperty(name="Preserve UVs", description="Try Preserve UVs (Slower and does not always work)", default=False) # type: ignore 32 | prefs = get_keyops_prefs() 33 | mouse_position = Vector((0, 0)) 34 | draw_preserve_uvs = False 35 | 36 | @classmethod 37 | def poll(cls, context): 38 | return context.mode == 'EDIT_MESH' and context.scene.tool_settings.mesh_select_mode[0] 39 | 40 | def invoke(self, context, event): 41 | self.mouse_position[0] = event.mouse_region_x 42 | self.mouse_position[1] = event.mouse_region_y 43 | return self.execute(context) 44 | 45 | def draw(self, context): 46 | self.layout.prop(self, "preserve_uvs") 47 | 48 | def execute(self, context): 49 | bm = bmesh.from_edit_mesh(context.active_object.data) 50 | selected_verts = [vert for vert in bm.verts if vert.select] 51 | global has_clicked_once 52 | active_vert = bm.select_history.active 53 | 54 | def merge(self, context, bm=bm, selected_verts=selected_verts): 55 | active_vert = bm.select_history.active 56 | if not selected_verts == None: 57 | if len(selected_verts) == 0: 58 | self.report({'WARNING'}, "No vertices selected") 59 | return {'FINISHED'} 60 | if len(selected_verts) == 1 and self.prefs.fast_merge_merge_options == "only merge if active or only 1 verts is selected" or self.prefs.fast_merge_merge_options == "always merge to nearest vert" or self.prefs.fast_merge_merge_options == "merge to nerest vert if no active" and active_vert == None or len(selected_verts) == 1: 61 | obj = context.object 62 | matrix_world = obj.matrix_world.copy() 63 | bm = bmesh.from_edit_mesh(obj.data) 64 | vertices = [] 65 | 66 | for vert in selected_verts: 67 | vertices.extend(edge.other_vert(vert) for edge in vert.link_edges) 68 | 69 | vertices += selected_verts 70 | vertices = list(set(vertices)) 71 | nearest_vertex = find_nearest_visible_vertex_to_mouse(context, self.mouse_position, vertices, matrix_world) 72 | 73 | if nearest_vertex: 74 | if nearest_vertex not in selected_verts: 75 | selected_verts.append(nearest_vertex) 76 | 77 | if self.preserve_uvs: 78 | uv_layer = bm.loops.layers.uv.active 79 | #fixes error in some very rare cases, but might not be worth the potential performance hit 80 | if nearest_vertex.link_loops: 81 | for vert in selected_verts: 82 | for loop in vert.link_loops: 83 | if loop[uv_layer] and nearest_vertex.link_loops[0][uv_layer]: 84 | loop[uv_layer].uv = nearest_vertex.link_loops[0][uv_layer].uv 85 | 86 | bmesh.ops.pointmerge(bm, verts=selected_verts, merge_co=nearest_vertex.co) 87 | bmesh.update_edit_mesh(obj.data) 88 | else: 89 | bmesh.ops.pointmerge(bm, verts=selected_verts, merge_co=nearest_vertex.co) 90 | bmesh.update_edit_mesh(obj.data) 91 | 92 | self.report({'INFO'}, "Merged %d vertices" % (len(selected_verts) - 1)) 93 | return {'FINISHED'} 94 | 95 | else: 96 | selected_history = bm.select_history.active 97 | active_vert = selected_history if selected_history in selected_verts else [] 98 | 99 | if active_vert: 100 | if self.preserve_uvs: 101 | amount_of_verts_merged = len(selected_verts) 102 | bpy.ops.mesh.merge(uvs=True, type='LAST') 103 | else: 104 | amount_of_verts_merged = len(selected_verts) 105 | bmesh.ops.pointmerge(bm, verts=selected_verts, merge_co=active_vert.co) 106 | bmesh.update_edit_mesh(context.active_object.data) 107 | 108 | self.report({'INFO'}, "Merged %d vertices" % (amount_of_verts_merged - 1)) 109 | 110 | else: 111 | self.report({'WARNING'}, "No Active Vertex to Merge to") 112 | 113 | if self.prefs.fast_merge_soft_limit == "no_limit": 114 | merge(self, context, bm, selected_verts) 115 | 116 | elif self.prefs.fast_merge_soft_limit == "max_polycount": 117 | if self.prefs.fast_merge_polycount >= len(selected_verts): 118 | merge(self, context, bm, selected_verts) 119 | else: 120 | self.report({'WARNING'}, "Limit - Too many vertices selected") 121 | elif self.prefs.fast_merge_soft_limit == "all_selected": 122 | if len(selected_verts) == len(bm.verts): 123 | self.report({'WARNING'}, "Limit - All vertices are selected") 124 | else: 125 | merge(self, context, bm, selected_verts) 126 | 127 | elif self.prefs.fast_merge_soft_limit == "max_limit_&_all_selected": 128 | if len(selected_verts) == len(bm.verts) and self.prefs.fast_merge_polycount < len(selected_verts): 129 | self.report({'WARNING'}, "Limit - All vertices are selected and too many vertices selected") 130 | elif self.prefs.fast_merge_polycount >= len(selected_verts): 131 | merge(self, context, bm, selected_verts) 132 | else: 133 | self.report({'WARNING'}, "Limit - Too many vertices selected") 134 | 135 | elif self.prefs.fast_merge_soft_limit == "ask_if_no_active_vert": 136 | if active_vert == None: 137 | if has_clicked_once == None: 138 | has_clicked_once = True 139 | if not self.prefs.fast_merge_merge_options == "only merge if active or only 1 verts is selected": 140 | self.report({'WARNING'}, "Limit - Press again to merge to nearest vertex") 141 | return {'FINISHED'} 142 | else: 143 | has_clicked_once = None 144 | merge(self, context, bm, selected_verts) 145 | current_selected_verts = [vert for vert in bm.verts if vert.select] 146 | if len(current_selected_verts) > 0: 147 | bm.select_history.add(current_selected_verts[0]) 148 | else: 149 | merge(self, context, bm, selected_verts) 150 | 151 | has_clicked_once = None 152 | 153 | return {'FINISHED'} 154 | def register(): 155 | bpy.utils.register_class(FastConnect) 156 | bpy.utils.register_class(Connect) 157 | #bpy.utils.register_class(FastKnife) 158 | 159 | def unregister(): 160 | bpy.utils.unregister_class(FastConnect) 161 | bpy.utils.unregister_class(Connect) 162 | #bpy.utils.unregister_class(FastKnife) 163 | 164 | class FastKnife(bpy.types.Operator): 165 | bl_idname = 'mesh.fast_knife' 166 | bl_label = 'FastKnife' 167 | bl_options = {'REGISTER', 'UNDO'} 168 | 169 | mouse_position = Vector((0, 0)) 170 | 171 | @classmethod 172 | def poll(cls, context): 173 | return context.mode == 'EDIT_MESH' 174 | 175 | def invoke(self, context, event): 176 | self.mouse_position[0] = event.mouse_region_x 177 | self.mouse_position[1] = event.mouse_region_y 178 | return self.execute(context) 179 | 180 | def execute(self, context): 181 | 182 | 183 | obj = context.edit_object 184 | me = obj.data 185 | bm = bmesh.from_edit_mesh(me) 186 | 187 | 188 | 189 | def dissolve_redundant_edges(bm, vert, excluded_verts = []): 190 | dissolved_edges = [] 191 | l = len(vert.link_edges) 192 | for e in vert.link_edges: 193 | if not e.other_vert(vert) in excluded_verts and l > 2: 194 | dissolved_edges.append(e) 195 | l -= 1 196 | bmesh.ops.dissolve_edges(bm, edges = dissolved_edges, use_verts = False, use_face_split = False) 197 | 198 | 199 | def addVertOnFace(self, co, face): 200 | vert = bmesh.ops.poke(self.bmesh, faces=[face])['verts'][0] 201 | vert.co = co 202 | dissolve_redundant_edges(self.bmesh, vert) 203 | return vert 204 | 205 | def addVertOnEdge(self, co, edge): 206 | edge, vert = bmesh.utils.edge_split(edge, edge.verts[0], .5) 207 | vert.co = co 208 | return vert 209 | 210 | 211 | def find_closest_edge_or_face_or_vert_to_mouse(self, context, mouse_position, bm): 212 | obj = context.object 213 | matrix_world = obj.matrix_world.copy() 214 | vertices = [v for v in bm.verts if v.select] 215 | edges = [e for e in bm.edges if e.select] 216 | faces = [f for f in bm.faces if f.select] 217 | 218 | def distance_to_mouse(element): 219 | world_position = matrix_world @ element.calc_center_median() 220 | screen_pos = location_3d_to_region_2d(context.region, context.space_data.region_3d, world_position) 221 | return (mouse_position - screen_pos).length if screen_pos else float('inf') 222 | 223 | closest_element = min(vertices + edges + faces, key=distance_to_mouse) 224 | return closest_element 225 | 226 | #finds the closest edge or face or vert to the mouse and then decides if it should add a vert on edge ot add vert on face 227 | closest_element = find_closest_edge_or_face_or_vert_to_mouse(self, context, self.mouse_position, bm) 228 | 229 | if isinstance(closest_element, bmesh.types.BMVert): 230 | addVertOnEdge(self, closest_element.co, closest_element.link_edges[0]) 231 | elif isinstance(closest_element, bmesh.types.BMEdge): 232 | addVertOnEdge(self, closest_element.verts[0].co.lerp(closest_element.verts[1].co, .5), closest_element) 233 | elif isinstance(closest_element, bmesh.types.BMFace): 234 | addVertOnFace(self, closest_element.calc_center_median(), closest_element) 235 | 236 | return {'FINISHED'} 237 | 238 | class FastConnect(bpy.types.Operator): 239 | bl_idname = 'mesh.fast_connect' 240 | bl_label = 'FastConnect' 241 | bl_options = {'REGISTER', 'UNDO'} 242 | 243 | @classmethod 244 | def poll(cls, context): 245 | return context.mode == 'EDIT_MESH' 246 | 247 | def execute(self, context): 248 | global half_knife 249 | 250 | if half_knife is None: 251 | half_knife = get_is_addon_enabled('Half_Knife') 252 | 253 | obj = context.edit_object 254 | me = obj.data 255 | bm = bmesh.from_edit_mesh(me) 256 | 257 | sel_mode = bpy.context.tool_settings.mesh_select_mode[:] 258 | 259 | if sel_mode[0]: 260 | vert_sel = {v for v in bm.verts if v.select} 261 | 262 | if not vert_sel or len(vert_sel) == 1: 263 | if half_knife: 264 | bpy.ops.mesh.half_knife_operator('INVOKE_DEFAULT', auto_cut=True, cut_with_preview_from_void = False) 265 | else: 266 | bpy.ops.mesh.knife_tool('INVOKE_DEFAULT') 267 | return {'FINISHED'} 268 | 269 | elif len(vert_sel) > 1: 270 | try: 271 | bpy.ops.mesh.vert_connect_path('INVOKE_DEFAULT') 272 | except: 273 | self.report({'WARNING'}, "Could not connect vertices") 274 | return {'FINISHED'} 275 | 276 | elif sel_mode[1]: 277 | edge_sel = {e for e in bm.edges if e.select} 278 | 279 | if not edge_sel: 280 | bpy.ops.mesh.knife_tool('INVOKE_DEFAULT') 281 | return {'FINISHED'} 282 | else: 283 | bpy.ops.mesh.connect2('INVOKE_DEFAULT') 284 | 285 | 286 | elif sel_mode[2]: 287 | face_sel = {f for f in bm.faces if f.select} 288 | 289 | if not face_sel: 290 | bpy.ops.mesh.knife_tool('INVOKE_DEFAULT') 291 | return {'FINISHED'} 292 | 293 | elif len(face_sel) > 1: 294 | bpy.ops.mesh.connect2('INVOKE_DEFAULT') 295 | 296 | return {'FINISHED'} 297 | 298 | class Connect(bpy.types.Operator): 299 | bl_idname = 'mesh.connect2' 300 | bl_label = 'Connect' 301 | bl_options = {'REGISTER', 'UNDO'} 302 | 303 | edge_count: bpy.props.IntProperty(name="Cuts", default=1, min=1, max=128) # type: ignore 304 | set_flow: bpy.props.BoolProperty(name="Set Flow", default=False) # type: ignore 305 | 306 | def draw(self, context): 307 | self.layout.prop(self, "edge_count") 308 | self.layout.prop(self, "set_flow") 309 | 310 | def execute(self, context): 311 | sel_mode = bpy.context.tool_settings.mesh_select_mode[:] 312 | edit_mode_objects = [obj for obj in context.objects_in_mode if obj.type == 'MESH'] 313 | 314 | if sel_mode[0] or sel_mode[2]: 315 | for obj in edit_mode_objects: 316 | if sel_mode[0]: 317 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') 318 | 319 | me = obj.data 320 | bm = bmesh.from_edit_mesh(me) 321 | 322 | edge_sel = {e for e in bm.edges if e.select} 323 | if not edge_sel: 324 | continue 325 | 326 | #Based on Armored Wolf's answer on https://blender.stackexchange.com/questions/142197 327 | bpy.ops.mesh.region_to_loop() 328 | perimeter_edges = set(e for e in bm.edges if e.select) 329 | contained_edges = edge_sel - perimeter_edges 330 | 331 | bpy.ops.mesh.select_all(action='DESELECT') 332 | 333 | for e in contained_edges: 334 | e.select = True 335 | 336 | bpy.ops.mesh.loop_multi_select(ring=True) 337 | ring_edges = set(e for e in bm.edges if e.select) 338 | 339 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') 340 | corner_edges = set(e for e in bm.edges if e.select).intersection(perimeter_edges) 341 | 342 | ring_edges -= corner_edges 343 | 344 | bpy.ops.mesh.select_all(action='DESELECT') 345 | 346 | ring_sel = (perimeter_edges.intersection(ring_edges) - corner_edges).union(contained_edges) 347 | 348 | new_edges = bmesh.ops.subdivide_edges(bm, edges=list(ring_sel), cuts=self.edge_count, use_grid_fill=True) 349 | for e in new_edges['geom_inner']: 350 | e.select = True 351 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') 352 | bmesh.update_edit_mesh(me) 353 | bm.free() 354 | 355 | 356 | elif sel_mode[1]: 357 | for obj in edit_mode_objects: 358 | if obj.data.total_edge_sel > 0: 359 | me = obj.data 360 | bm = bmesh.from_edit_mesh(me) 361 | 362 | edge_sel = set(e for e in bm.edges if e.select) 363 | 364 | for e in bm.edges: 365 | e.select = False 366 | 367 | new_edges = bmesh.ops.subdivide_edges(bm, edges=list(edge_sel), cuts=self.edge_count, use_grid_fill=True) 368 | remove_edges = set(e for e in new_edges['geom_split'] if isinstance(e, bmesh.types.BMEdge)) 369 | 370 | for e in new_edges['geom_inner']: 371 | e.select = True 372 | for e in remove_edges: 373 | e.select = False 374 | bmesh.update_edit_mesh(me) 375 | bm.free() 376 | 377 | if self.set_flow: 378 | if get_is_addon_enabled('EdgeFlow-blender_28') or get_is_addon_enabled('EdgeFlow'): 379 | bpy.ops.mesh.set_edge_flow(tension=180, iterations=3) 380 | else: 381 | self.report({'WARNING'}, "EdgeFlow Add-on not Installed, search for it in Get Extensions") 382 | 383 | return {'FINISHED'} -------------------------------------------------------------------------------- /operators/legacy_shortcuts.py: -------------------------------------------------------------------------------- 1 | import bpy.types 2 | 3 | class LegacyShortcuts(bpy.types.Operator): 4 | bl_idname = "keyops.legacy_shortcuts" 5 | bl_label = "KeyOps: Legacy Shortcuts" 6 | bl_description = "Adds Legacy Shortcuts From Blender 2.79x" 7 | bl_options = {'INTERNAL'} 8 | 9 | 10 | -------------------------------------------------------------------------------- /operators/maya_navigation.py: -------------------------------------------------------------------------------- 1 | import bpy.types 2 | from ..utils.pref_utils import get_keyops_prefs 3 | 4 | tablet_keymap_config = [ 5 | ('3D View', 'view3d.zoom', 'LEFTMOUSE', 'CLICK_DRAG', {'ctrl': True, 'alt': True}), 6 | ('3D View', 'view3d.move', 'LEFTMOUSE', 'CLICK_DRAG', {'alt': True, 'shift': True}), 7 | ('View2D', 'view2d.pan', 'LEFTMOUSE', 'CLICK_DRAG', {'alt': True}), 8 | ('View2D', 'view2d.zoom', 'LEFTMOUSE', 'CLICK_DRAG', {'ctrl': True, 'alt': True}), 9 | ('View2D', 'view2d.pan', 'LEFTMOUSE', 'PRESS', {'alt': True, 'shift': True}), 10 | ('View2D Buttons List', 'view2d.pan', 'LEFTMOUSE', 'PRESS', {'alt': True}), 11 | ('Image', 'image.view_zoom', 'LEFTMOUSE', 'PRESS', {'ctrl': True, 'alt': True}), 12 | ('Image', 'image.view_pan', 'LEFTMOUSE', 'PRESS', {'alt': True, 'shift': True}), 13 | ('Image', 'image.view_pan', 'LEFTMOUSE', 'PRESS', {'alt': True}), 14 | ] 15 | def add_tablet_navigation(): 16 | prefs = get_keyops_prefs() 17 | 18 | if prefs.maya_navigation_tablet_navigation: 19 | wm = bpy.context.window_manager 20 | try: 21 | for keymap_name, idname, key, value, modifiers in tablet_keymap_config: 22 | km = wm.keyconfigs.active.keymaps[keymap_name] 23 | kmi = km.keymap_items.new(idname, key, value) 24 | for mod, enabled in modifiers.items(): 25 | setattr(kmi, mod, enabled) 26 | except KeyError as e: 27 | print(f"Error adding tablet navigation keymap: {e}") 28 | return 29 | 30 | def remove_tablet_navigation(): 31 | wm = bpy.context.window_manager 32 | 33 | try: 34 | for keymap_name, idname, key, value, modifiers in tablet_keymap_config: 35 | km = wm.keyconfigs.active.keymaps[keymap_name] 36 | items_to_remove = [ 37 | kmi for kmi in km.keymap_items 38 | if kmi.idname == idname and kmi.type == key and kmi.value == value and all( 39 | getattr(kmi, mod) == enabled for mod, enabled in modifiers.items() 40 | ) 41 | ] 42 | for kmi in items_to_remove: 43 | km.keymap_items.remove(kmi) 44 | except KeyError as e: 45 | print(f"Error removing tablet navigation keymap: {e}") 46 | return 47 | 48 | def update_tablet_navigation(self, context): 49 | if self.maya_navigation_tablet_navigation: 50 | add_tablet_navigation() 51 | else: 52 | remove_tablet_navigation() 53 | 54 | 55 | class MayaNavigation(bpy.types.Operator): 56 | bl_idname = "keyops.maya_navigation" 57 | bl_label = "KeyOps: Maya Navigation" 58 | bl_options = {'INTERNAL'} 59 | 60 | def register(): 61 | add_tablet_navigation() 62 | 63 | def unregister(): 64 | remove_tablet_navigation() -------------------------------------------------------------------------------- /operators/maya_shortcuts.py: -------------------------------------------------------------------------------- 1 | import bpy.types 2 | 3 | class MayaShortcuts(bpy.types.Operator): 4 | bl_idname = "keyops.maya_shortcuts" 5 | bl_label = "KeyOps: Maya Shortcuts" 6 | bl_options = {'REGISTER', 'UNDO'} 7 | 8 | def execute(self, context): 9 | return {'FINISHED'} 10 | -------------------------------------------------------------------------------- /operators/none_live_booleans.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.app.handlers import persistent 3 | 4 | is_transforming = False 5 | was_transforming = False 6 | booleon_cutter_info_dict = {} 7 | temp_cutter_clones = [] 8 | obj_to_delete = [] 9 | 10 | def reset_globals(): 11 | global is_transforming, was_transforming, booleon_cutter_info_dict, temp_cutter_clones 12 | is_transforming = False 13 | was_transforming = False 14 | booleon_cutter_info_dict = {} 15 | temp_cutter_clones = [] 16 | 17 | def create_copy_of_cutter_object(cutting_object): 18 | c = bpy.context 19 | # depsgraph = c.evaluated_depsgraph_get() 20 | # cutting_object_eval = cutting_object.evaluated_get(depsgraph) 21 | # new_cutter = cutting_object_eval.copy() 22 | # new_cutter.data = cutting_object_eval.data.copy() 23 | new_cutter = cutting_object.copy() 24 | new_cutter.data = cutting_object.data.copy() 25 | c.collection.objects.link(new_cutter) 26 | 27 | new_cutter.name = "temp_boolean_cutter_" + cutting_object.name 28 | 29 | # hide all modifiers 30 | # for mod in new_cutter.modifiers: 31 | # mod.show_viewport = False 32 | # mod.show_render = False 33 | return new_cutter 34 | 35 | def find_boolean_relationships(): 36 | c = bpy.context 37 | selected_objects = set(c.selected_objects) # Use a set for O(1) lookups, also de-duplicates duplicates (not that it matters in this case, but I want to remember to it for the future) 38 | if not selected_objects: 39 | return 40 | 41 | boolean_links = {} 42 | 43 | objects_with_modifiers = [obj for obj in c.scene.objects if obj.modifiers] 44 | 45 | for obj in objects_with_modifiers: 46 | for mod in obj.modifiers: 47 | # Check if the modifier is a Boolean modifier and references a selected object 48 | if mod.type == 'BOOLEAN' and mod.object in selected_objects: 49 | cutting_object = mod.object 50 | boolean_links.setdefault(cutting_object, []).append({ 51 | 'target_object': obj, 52 | 'modifier': mod 53 | }) 54 | 55 | return boolean_links 56 | 57 | # Print results 58 | # for cutting_object, links in boolean_links.items(): 59 | # print(f"Boolean Object: {cutting_object.name}") 60 | # for link in links: 61 | # print(f" Cuts Into: {link['target_object']}, Modifier: {link['modifier']}") 62 | 63 | 64 | def faster_objs_delete(objs): # deleting objects in Blender is crazy slow by deafult, this is a faster way to do it 65 | for obj in objs: 66 | for collection in obj.users_collection: 67 | collection.objects.unlink(obj) 68 | 69 | def is_current_running_modal_transform_none_live_booleans(dummy1, dummy2): 70 | global is_transforming, was_transforming, booleon_cutter_info_dict, temp_cutter_clones, obj_to_delete 71 | c = bpy.context 72 | is_transforming = False 73 | 74 | if c.mode == 'OBJECT': 75 | selected = c.selected_objects 76 | if selected: 77 | wm = c.window_manager 78 | 79 | for window in wm.windows: 80 | for op in window.modal_operators: 81 | op_prefix = op.bl_idname.split("_OT_") 82 | 83 | if "TRANSFORM" in op_prefix: 84 | if hasattr(op, 'cursor_transform') and op.cursor_transform: 85 | continue 86 | is_transforming = True 87 | break 88 | 89 | if is_transforming: 90 | if not booleon_cutter_info_dict: # gizmo only 91 | booleon_cutter_info_dict = find_boolean_relationships() 92 | 93 | for cutting_object, links in booleon_cutter_info_dict.items(): 94 | for link in links: 95 | if link['modifier'].object: 96 | link['modifier'].object = None 97 | 98 | # selected_objects_names = [obj.name for obj in selected] 99 | # print("Transforming: " + str(selected_objects_names)) 100 | elif was_transforming and not is_transforming: 101 | # print("Stopped transforming") 102 | if not temp_cutter_clones: # gizmo only 103 | for cutting_object, links in booleon_cutter_info_dict.items(): 104 | for link in links: 105 | if link['modifier'].object != cutting_object: 106 | link['modifier'].object = cutting_object 107 | else: 108 | iteration = -1 109 | for cutting_object, links in booleon_cutter_info_dict.items(): 110 | iteration += 1 111 | for link in links: 112 | cutting_object.hide_set(False) 113 | cutting_object.select_set(True) 114 | 115 | # copy the location, rotation and scale of the cutter object to the boolean object 116 | cutting_object.location = temp_cutter_clones[iteration].location 117 | cutting_object.rotation_euler = temp_cutter_clones[iteration].rotation_euler 118 | cutting_object.scale = temp_cutter_clones[iteration].scale 119 | # remove the temp cutter clones, too slow in bigger scenes, just let Blender handle the unused data instead 120 | # for new_cutter in temp_cutter_clones: 121 | # get new_cutter mesh data from the object 122 | # new_cutter_mesh = new_cutter.data 123 | # bpy.data.objects.remove(new_cutter) 124 | # bpy.data.meshes.remove(new_cutter_mesh) 125 | 126 | for new_cutter in temp_cutter_clones: 127 | obj_to_delete.append(new_cutter.name) 128 | 129 | faster_objs_delete(temp_cutter_clones) 130 | 131 | booleon_cutter_info_dict = {} 132 | temp_cutter_clones = [] 133 | 134 | was_transforming = is_transforming 135 | 136 | def restore_boolean_modifiers(): 137 | """Restore Boolean modifier object references after undo/redo.""" 138 | global booleon_cutter_info_dict 139 | for cutting_object, links in booleon_cutter_info_dict.items(): 140 | for link in links: 141 | if link['modifier'].object is None: 142 | link['modifier'].object = cutting_object 143 | 144 | def delete_temp_objs_on_redo_undo(dummy1, dummy2): # need to do this since the bpy.opps.transform.translate() creates a undo step where the temp objects still exist 145 | import time 146 | exec_time = time.time() 147 | global obj_to_delete 148 | all_objects = bpy.data.objects 149 | 150 | objects_to_delete = [all_objects[obj_name] for obj_name in obj_to_delete if obj_name in all_objects] 151 | 152 | faster_objs_delete(objects_to_delete) 153 | 154 | for obj in objects_to_delete: 155 | cutter_name = obj.name.replace("temp_boolean_cutter_", "") 156 | if cutter_name in all_objects: 157 | cutter = bpy.data.objects[cutter_name] 158 | if cutter.name in bpy.context.view_layer.objects: 159 | cutter.hide_set(False) 160 | cutter.select_set(True) 161 | 162 | # refresh viewport the object someitmes does not translate to the correct location at first undo/redo due to it being in the last captured undo step in that lcoation before the temp objects took over 163 | # bpy.ops.transform.translate(value=(0, 0, 0), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False, snap=False, snap_elements={'GRID', 'VERTEX', 'FACE'}, use_snap_project=False, snap_target='CLOSEST', use_snap_self=True, use_snap_edit=True, use_snap_nonedit=True, use_snap_selectable=False) 164 | # print("Deleted temp objects in: " + str(time.time() - exec_time) + " seconds") 165 | 166 | # We need to detect if the user presses G, R, S, to invoke a transform operator, to skip any initial lag spikes due to recalculations of the modifiers. 167 | # Seems like the only way to get over the first lag spike when transforming. RIP Gizmo users, I guess. 168 | class ENTERING_TRANSFORM_OT_None_Live_Booleans(bpy.types.Operator): 169 | bl_idname = "transform.entering_transform_none_live_booleans" 170 | bl_label = "Entering Transform" 171 | bl_options = {'INTERNAL'} 172 | 173 | def execute(self, context): 174 | # bpy.ops.ed.undo_push() 175 | # print("Entering Transform") 176 | if bpy.context.selected_objects: 177 | global booleon_cutter_info_dict, temp_cutter_clones 178 | booleon_cutter_info_dict = find_boolean_relationships() 179 | 180 | for cutting_object, links in booleon_cutter_info_dict.items(): 181 | for link in links: 182 | # hide the cutter object 183 | cutting_object.hide_set(True) 184 | cutting_object.select_set(False) 185 | 186 | # create a copy of the cutter object 187 | new_cutter = create_copy_of_cutter_object(cutting_object) 188 | new_cutter.select_set(True) 189 | temp_cutter_clones.append(new_cutter) 190 | return {'PASS_THROUGH'} 191 | 192 | def register(): 193 | # add the keymap 194 | wm = bpy.context.window_manager 195 | km = wm.keyconfigs.addon.keymaps.new(name='Object Mode', space_type='EMPTY') 196 | kmi = km.keymap_items.new(ENTERING_TRANSFORM_OT_None_Live_Booleans.bl_idname, type='G', value='PRESS', shift=False, ctrl=False, alt=False, oskey=False) 197 | kmi = km.keymap_items.new(ENTERING_TRANSFORM_OT_None_Live_Booleans.bl_idname, type='R', value='PRESS', shift=False, ctrl=False, alt=False, oskey=False) 198 | kmi = km.keymap_items.new(ENTERING_TRANSFORM_OT_None_Live_Booleans.bl_idname, type='S', value='PRESS', shift=False, ctrl=False, alt=False, oskey=False) 199 | 200 | handlers = [ 201 | (bpy.app.handlers.depsgraph_update_pre, is_current_running_modal_transform_none_live_booleans), 202 | (bpy.app.handlers.load_post, unregister_none_live_booleans), 203 | (bpy.app.handlers.undo_post, delete_temp_objs_on_redo_undo), 204 | (bpy.app.handlers.redo_post, delete_temp_objs_on_redo_undo)] 205 | 206 | for handler_list, handler in handlers: 207 | if handler not in handler_list: 208 | handler_list.append(handler) 209 | 210 | def unregister(): 211 | handlers = [ 212 | (bpy.app.handlers.depsgraph_update_pre, is_current_running_modal_transform_none_live_booleans), 213 | (bpy.app.handlers.load_post, unregister_none_live_booleans), 214 | (bpy.app.handlers.undo_post, delete_temp_objs_on_redo_undo), 215 | (bpy.app.handlers.redo_post, delete_temp_objs_on_redo_undo)] 216 | 217 | for handler_list, handler in handlers: 218 | if handler in handler_list: 219 | handler_list.remove(handler) 220 | 221 | # remove the keymap 222 | wm = bpy.context.window_manager 223 | km = wm.keyconfigs.addon.keymaps['Object Mode'] 224 | 225 | for kmi in km.keymap_items: 226 | if kmi.idname == ENTERING_TRANSFORM_OT_None_Live_Booleans.bl_idname: 227 | km.keymap_items.remove(kmi) 228 | 229 | reset_globals() 230 | 231 | 232 | @persistent 233 | def unregister_none_live_booleans(dummy, dummy2): 234 | try: 235 | bpy.utils.unregister_class(ENTERING_TRANSFORM_OT_None_Live_Booleans) 236 | except: 237 | pass 238 | -------------------------------------------------------------------------------- /operators/origin_to_selection.py: -------------------------------------------------------------------------------- 1 | import bpy.types 2 | from bpy.types import Menu 3 | 4 | class OriginToSelection(bpy.types.Operator): 5 | bl_description = "Set the origin of the selected object to the center of its geometry or selection" 6 | bl_idname = "keyops.origin_to_selection" 7 | bl_label = "Origin to Selection" 8 | bl_options = {'INTERNAL'} 9 | 10 | type: bpy.props.StringProperty(name='Type') # type:ignore 11 | 12 | def execute(self, context): 13 | if self.type == "origin_to_geometry": 14 | if context.mode == 'OBJECT': 15 | bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') 16 | bpy.ops.ed.undo_push() 17 | elif context.mode == 'EDIT_MESH': 18 | cursor_loc = context.scene.cursor.location 19 | pos2 = (cursor_loc[0], cursor_loc[1], cursor_loc[2]) 20 | 21 | bpy.ops.view3d.snap_cursor_to_selected() 22 | bpy.ops.object.editmode_toggle() 23 | bpy.ops.object.origin_set(type='ORIGIN_CURSOR') 24 | bpy.ops.ed.undo_push() # this works out better before editmode_toggle, otherwise it jumps around a lot in edit mode 25 | bpy.ops.object.editmode_toggle() 26 | bpy.ops.ed.undo_push() # makes it so you return to edit mode after the operation on undo 27 | 28 | context.scene.cursor.location = (pos2[0], pos2[1], pos2[2]) 29 | elif self.type == "origin_to_3d_cursor": 30 | if context.mode == 'OBJECT': 31 | bpy.ops.object.origin_set(type='ORIGIN_CURSOR') 32 | bpy.ops.ed.undo_push() 33 | elif context.mode == 'EDIT_MESH': 34 | bpy.ops.object.editmode_toggle() 35 | bpy.ops.object.origin_set(type='ORIGIN_CURSOR') 36 | bpy.ops.ed.undo_push() # this works out better before editmode_toggle, otherwise it jumps around a lot in edit mode 37 | bpy.ops.object.editmode_toggle() 38 | bpy.ops.ed.undo_push() # makes it so you return to edit mode after the operation on undo 39 | 40 | return {'FINISHED'} 41 | def register(): 42 | bpy.utils.register_class(CursorPie) 43 | def unregister(): 44 | bpy.utils.unregister_class(CursorPie) 45 | 46 | class CursorPie(Menu): 47 | bl_idname = "KEYOPS_MT_cursor_pie" 48 | bl_label = "Cursor Pie" 49 | 50 | def draw(self, context): 51 | layout = self.layout 52 | pie = layout.menu_pie() 53 | 54 | pie.operator("keyops.origin_to_selection", text="Origin to 3D Cursor", icon="LAYER_USED").type= "origin_to_3d_cursor" #LEFT 55 | 56 | pie.operator("keyops.origin_to_selection", text="Origin to Selection", icon="LAYER_USED").type= "origin_to_geometry" #RIGHT 57 | 58 | pie.operator("view3d.snap_cursor_to_selected", text="Cursor to Selection", icon="CURSOR") #BOTTOM 59 | 60 | pie.operator("view3d.snap_selected_to_cursor", text="Selection to Cursor", icon="RESTRICT_SELECT_OFF") #TOP 61 | 62 | pie.operator("view3d.snap_selected_to_grid", text="Selection to Grid", icon="SNAP_GRID") #LEFT TOP 63 | 64 | pie.operator('view3d.snap_cursor_to_grid', text="Cursor to Grid", icon="SNAP_GRID") #RIGHT TOP 65 | 66 | pie.operator("view3d.snap_cursor_to_center", text="Cursor to World Origin", icon="PIVOT_CURSOR") #LEFT BOTTOM 67 | 68 | pie.operator('view3d.snap_cursor_to_active', text="Cursor to Active", icon="CURSOR") #RIGHT BOTTOM 69 | 70 | -------------------------------------------------------------------------------- /operators/outliner_options.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.app.handlers import persistent 3 | 4 | previous_selected_objects = [] 5 | clicked_in_outliner = False 6 | 7 | def register_outliner_click_keymap(register=True): 8 | wm = bpy.context.window_manager 9 | km = wm.keyconfigs.addon.keymaps.new(name='Outliner', space_type='OUTLINER') 10 | kmi = km.keymap_items.new('keyops.outliner_click', 'LEFTMOUSE', 'CLICK') 11 | kmi = km.keymap_items.new('keyops.outliner_click', 'RIGHTMOUSE', 'CLICK') 12 | kmi.active = True 13 | kmi = km.keymap_items.new('keyops.outliner_click', 'LEFTMOUSE', 'CLICK', shift=True) 14 | kmi = km.keymap_items.new('keyops.outliner_click', 'LEFTMOUSE', 'CLICK', ctrl=True) 15 | kmi = km.keymap_items.new('keyops.outliner_click', 'LEFTMOUSE', 'CLICK', ctrl=True, shift=True) 16 | kmi = km.keymap_items.new('keyops.outliner_click', 'A', 'PRESS', alt=True) 17 | kmi = km.keymap_items.new('keyops.outliner_click', 'A', 'PRESS') 18 | kmi.active = True 19 | 20 | if not register: 21 | wm.keyconfigs.addon.keymaps.remove(km) 22 | 23 | return km, kmi 24 | 25 | def unregister_outliner_click_keymap(): 26 | wm = bpy.context.window_manager 27 | for km in wm.keyconfigs.addon.keymaps: 28 | if km.name == 'Outliner': 29 | if 'keyops.outliner_click' in [k.idname for k in km.keymap_items]: 30 | wm.keyconfigs.addon.keymaps.remove(km) 31 | break 32 | 33 | # Register the handler after the add-on has loaded if the option is enabled 34 | @persistent 35 | def register_handler(dummy=None, depsgraph=None): 36 | bpy.app.timers.register(lambda: enabled_selection_options(bpy.context.scene, bpy.context), first_interval=0.3) 37 | 38 | def selection_option_handler(dummy, depsgraph): 39 | global previous_selected_objects 40 | global clicked_in_outliner 41 | current_selected_objects = bpy.context.selected_objects 42 | 43 | if previous_selected_objects != current_selected_objects: 44 | previous_selected_objects = current_selected_objects 45 | 46 | if bpy.context.scene.auto_focus_in_outliner or bpy.context.scene.collapse_unselected_collections: 47 | if not clicked_in_outliner: 48 | outliner_areas = [a for a in bpy.context.screen.areas if a.type == "OUTLINER"] 49 | if not outliner_areas: 50 | return # No outliner area found, exit the function 51 | largest_area = max(outliner_areas, key=lambda a: a.width * a.height) 52 | largest_region = next(r for r in largest_area.regions if r.type == "WINDOW") 53 | 54 | override_context = { 55 | 'area': largest_area, 56 | 'region': largest_region 57 | } 58 | 59 | with bpy.context.temp_override(**override_context): 60 | if bpy.context.scene.collapse_unselected_collections: 61 | bpy.ops.outliner.expanded_toggle() 62 | bpy.ops.outliner.expanded_toggle() 63 | 64 | bpy.ops.outliner.show_active() 65 | 66 | def walk_children(ob): 67 | yield ob 68 | for child in ob.children: 69 | yield from walk_children(child) 70 | 71 | if bpy.context.scene.select_children and clicked_in_outliner: 72 | for obj in current_selected_objects: 73 | for child in walk_children(obj): 74 | child.select_set(True) 75 | 76 | clicked_in_outliner = False 77 | 78 | def draw_options_in_outliner(self, context): 79 | layout = self.layout 80 | layout.label(text="Selection") 81 | row = layout.row() 82 | row.prop(context.scene, 'auto_focus_in_outliner') 83 | row = layout.row() 84 | row.prop(context.scene, 'collapse_unselected_collections') 85 | row = layout.row() 86 | row.prop(context.scene, 'select_children') 87 | 88 | def enabled_selection_options(self, context): 89 | if self.auto_focus_in_outliner or self.select_children or self.collapse_unselected_collections: 90 | if selection_option_handler not in bpy.app.handlers.depsgraph_update_post: 91 | bpy.app.handlers.depsgraph_update_post.append(selection_option_handler) 92 | if not any('keyops.outliner_click' in km.keymap_items for km in bpy.context.window_manager.keyconfigs.addon.keymaps): 93 | register_outliner_click_keymap() 94 | else: 95 | if selection_option_handler in bpy.app.handlers.depsgraph_update_post: 96 | bpy.app.handlers.depsgraph_update_post.remove(selection_option_handler) 97 | unregister_outliner_click_keymap() 98 | 99 | class OutlinerClick(bpy.types.Operator): 100 | bl_idname = "keyops.outliner_click" 101 | bl_label = "KeyOps: Outliner Click" 102 | bl_description = "" 103 | bl_options = {"INTERNAL"} 104 | 105 | @classmethod 106 | def poll(cls, context): 107 | return (context.scene.auto_focus_in_outliner or 108 | context.scene.select_children or 109 | context.scene.collapse_unselected_collections) 110 | 111 | def invoke(self, context, event): 112 | global clicked_in_outliner 113 | clicked_in_outliner = True 114 | return {'PASS_THROUGH'} 115 | 116 | 117 | class OutlinerOptions(bpy.types.Operator): 118 | bl_idname = "keyops.outliner_options" 119 | bl_label = "KeyOps: Outliner Options" 120 | bl_description = "" 121 | bl_options = {'INTERNAL'} 122 | 123 | def register(): 124 | bpy.utils.register_class(OutlinerClick) 125 | bpy.types.Scene.auto_focus_in_outliner = bpy.props.BoolProperty( 126 | name="Auto Select in Outliner", 127 | description="Automatically focus on the selected objects in the outliner, can be slow with many objects", 128 | default=False, 129 | update=enabled_selection_options 130 | ) 131 | bpy.types.Scene.select_children = bpy.props.BoolProperty( 132 | name="Select Children", 133 | description="Automatically select children of selected objects", 134 | default=False, 135 | update=enabled_selection_options 136 | ) 137 | bpy.types.Scene.collapse_unselected_collections = bpy.props.BoolProperty( 138 | name="Collapse Unselected Collections", 139 | description="Automatically collapse collections that are not related to the selected objects", 140 | default=False, 141 | update=enabled_selection_options 142 | ) 143 | bpy.types.OUTLINER_PT_filter.prepend(draw_options_in_outliner) 144 | 145 | bpy.app.timers.register(register_handler) 146 | bpy.app.handlers.load_post.append(register_handler) 147 | 148 | def unregister(): 149 | bpy.utils.unregister_class(OutlinerClick) 150 | if selection_option_handler in bpy.app.handlers.depsgraph_update_post: 151 | bpy.app.handlers.depsgraph_update_post.remove(selection_option_handler) 152 | 153 | handlers = [h.__name__ for h in bpy.app.handlers.load_post] 154 | if 'register_handler' in handlers: 155 | bpy.app.handlers.load_post.remove(register_handler) 156 | 157 | del bpy.types.Scene.auto_focus_in_outliner 158 | del bpy.types.Scene.select_children 159 | del bpy.types.Scene.collapse_unselected_collections 160 | 161 | if bpy.types.OUTLINER_PT_filter: 162 | bpy.types.OUTLINER_PT_filter.remove(draw_options_in_outliner) 163 | 164 | unregister_outliner_click_keymap() 165 | -------------------------------------------------------------------------------- /operators/quick_bake_name.py: -------------------------------------------------------------------------------- 1 | # Based on the excellent Baking-Namepair-Creator Addon by PLyczkowski 2 | # https://github.com/PLyczkowski/Baking-Namepair-Creator 3 | # Now it also works when more than 2 objects are selected, the active object will be the low poly and the rest will be high poly with a suffix of .001, .002 etc 4 | # In Marmoset Toolbag and Substance Painter, the suffix will make the high poly objects with the same name bake together as one high poly object. 5 | 6 | """ 7 | TODO: 8 | - add a reverse function when many objects are selected, so the active object is the high poly and the rest are low poly with a suffix of .001, .002 etc. 9 | - add new operation that do it based on bounding box size or pivot point distance 10 | - make it fast - DONE, about 200-1000 faster now! 11 | """ 12 | 13 | import bpy 14 | import random 15 | import string 16 | 17 | from bpy.types import Context 18 | from ..utils.pref_utils import get_keyops_prefs 19 | 20 | prefs = get_keyops_prefs() 21 | 22 | def random_name(size=5, chars=string.ascii_uppercase + string.digits): 23 | return ''.join(random.choice(chars) for _ in range(size)) 24 | 25 | class QuickBakeName(bpy.types.Operator): 26 | bl_description = "Quick Bake Name" 27 | bl_idname = "keyops.quick_bake_name" 28 | bl_label = "Quick Bake Name" 29 | bl_options = {'REGISTER', 'UNDO'} 30 | 31 | random_name : bpy.props.BoolProperty(name="Random Name", description="Random Name", default=True) # type: ignore 32 | rename_datablock : bpy.props.BoolProperty(name="Rename Datablock", description="Rename Datablock, slower but decreases risk of errors when baking", default=True) # type: ignore 33 | hide : bpy.props.BoolProperty(name="Hide Objects", description="Hide", default=False) # type: ignore 34 | type : bpy.props.StringProperty(name="Type", description="Type", default="", options={'SKIP_SAVE'}) # type: ignore 35 | new_name : bpy.props.StringProperty( 36 | name="New Name", 37 | description="Enter a new name for high and low poly objects, it will automatically find them based on the name", 38 | default="", options={'SKIP_SAVE'}, 39 | ) # type: ignore 40 | def invoke(self, context, event): 41 | if self.type == "RENAME": 42 | active_object = bpy.context.object 43 | if not active_object: 44 | self.report({'WARNING'}, "Please select an active object") 45 | return {'CANCELLED'} 46 | name = active_object.name 47 | self.new_name = name.split("_low")[0] 48 | self.new_name = self.new_name.split("_high")[0] 49 | return context.window_manager.invoke_props_dialog(self) 50 | else: 51 | return self.execute(context) 52 | 53 | def draw(self, context): 54 | if self.type == "RENAME": 55 | self.layout.prop(self, "new_name") 56 | else: 57 | # if more than 2 objects are selected, show text that says the the active object will be the low poly and the rest will be high poly with a suffix of .001, .002 etc. 58 | selected = bpy.context.selected_objects 59 | if len(selected) > 2: 60 | self.layout.label(text=">2 objects, the active one is set as low-poly", icon="INFO") 61 | layout = self.layout 62 | row = layout.row() 63 | row.prop(self, "hide") 64 | row = layout.row() 65 | row.prop(self, "random_name") 66 | row = layout.row() 67 | row.prop(self, "rename_datablock") 68 | row = layout.row() 69 | 70 | def execute(self, context): 71 | if self.type == "RENAME": 72 | new_name = self.new_name 73 | old_name = bpy.context.object.name.split("_low")[0] 74 | old_name = old_name.split("_high")[0] 75 | 76 | for obj in bpy.context.scene.objects: 77 | obj_name = obj.name 78 | if ("_low" in obj_name or "_high" in obj_name) and old_name in obj_name: 79 | if "_low" in obj_name: 80 | suffix = obj_name.split("_low")[1] 81 | obj.name = new_name + "_low" + suffix 82 | if "_high" in obj_name: 83 | suffix = obj_name.split("_high")[1] 84 | obj.name = new_name + "_high" + suffix 85 | 86 | return {'FINISHED'} 87 | 88 | if self.type in {"set_high_name", "set_low_name"}: 89 | suffix = "_high" if self.type == "set_high_name" else "_low" 90 | selected = bpy.context.selected_objects 91 | if selected: 92 | for obj in selected: 93 | if "_low" in obj.name or "_high" in obj.name: 94 | obj.name = obj.name.split("_low")[0].split("_high")[0] + suffix 95 | else: 96 | obj.name += suffix 97 | return {'FINISHED'} 98 | 99 | selected = bpy.context.selected_objects 100 | 101 | in_high_collection = False 102 | in_low_collection = False 103 | 104 | if len(selected) == 1: 105 | self.report({'WARNING'}, "Please select at least two objects") 106 | return {'CANCELLED'} 107 | 108 | if len(selected) == 2: 109 | for obj in selected: 110 | for coll in obj.users_collection: 111 | if coll.name.lower() == "high": 112 | in_high_collection = True 113 | high_object = obj 114 | elif coll.name.lower() == "low": 115 | in_low_collection = True 116 | low_object = obj 117 | 118 | if in_high_collection and in_low_collection: 119 | pass 120 | else: 121 | selected = bpy.context.selected_objects 122 | 123 | highest_poly_count_object = None 124 | highest_poly_count = 0 125 | 126 | for obj in selected: 127 | m = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh() 128 | poly_count = len(m.loop_triangles) 129 | 130 | if poly_count > highest_poly_count: 131 | highest_poly_count_object = obj 132 | highest_poly_count = poly_count 133 | 134 | if highest_poly_count_object: 135 | high_object = highest_poly_count_object 136 | low_object = selected[0] if selected[1] == high_object else selected[1] 137 | 138 | if self.random_name: 139 | low_object.name = random_name() + "_low" 140 | high_object.name = low_object.name[:-4] + "_high" 141 | if self.rename_datablock: 142 | low_object.data.name = low_object.name 143 | high_object.data.name = high_object.name 144 | else: 145 | if low_object.name.endswith("_low"): 146 | low_object.name = low_object.name[:-4] 147 | low_name = bpy.data.objects.get(low_object.name + "_low") 148 | high_name = bpy.data.objects.get(low_object.name + "_high") 149 | 150 | if low_name == None and high_name == None: 151 | high_object.name = low_object.name + "_high" 152 | low_object.name = low_object.name + "_low" 153 | if self.rename_datablock: 154 | low_object.data.name = low_object.name 155 | high_object.data.name = high_object.name 156 | else: 157 | self.report({'ERROR'}, "Name already exists, please rename the low poly object.") 158 | if self.hide: 159 | low_object.hide_set(True) 160 | high_object.hide_set(True) 161 | return {'FINISHED'} 162 | 163 | else: 164 | active_object = bpy.context.object 165 | high_exists = False 166 | rename_count = 0 167 | 168 | if self.random_name and active_object: 169 | active_object.name = random_name() 170 | 171 | for selected_object in bpy.context.selected_objects: 172 | if active_object != selected_object: 173 | if high_exists: 174 | rename_count += 1 175 | elif active_object.name.endswith('_high'): 176 | high_exists = True 177 | 178 | if active_object.name.endswith('_low'): 179 | selected_object.name = active_object.name[:-4] + f'_high.00{rename_count}' 180 | else: 181 | selected_object.name = active_object.name + f'_high.00{rename_count}' 182 | 183 | if not active_object.name.endswith('_low'): 184 | active_object.name += '_low' 185 | 186 | for obj in bpy.context.selected_objects: 187 | obj.name = obj.name.replace(".000", "") 188 | 189 | if self.hide: 190 | for obj in bpy.context.selected_objects: 191 | obj.hide_set(True) 192 | return {'FINISHED'} 193 | 194 | def register(): 195 | bpy.utils.register_class(QuickBakeNamePanel) 196 | bpy.utils.register_class(HideShowLowHigh) 197 | def unregister(): 198 | bpy.utils.unregister_class(QuickBakeNamePanel) 199 | bpy.utils.unregister_class(HideShowLowHigh) 200 | 201 | class HideShowLowHigh(bpy.types.Operator): 202 | bl_idname = "keyops.hide_show_low_high" 203 | bl_label = "Hide Show Low High" 204 | bl_description = "Hide Show Low High" 205 | bl_options = {'INTERNAL', 'UNDO'} 206 | 207 | group : bpy.props.EnumProperty(items = { 208 | ('HIGH', "High", "High"), 209 | ('LOW', "Low", "Low"), 210 | ('ALL', "All", "All"), 211 | }) # type: ignore 212 | action : bpy.props.EnumProperty(items = { 213 | ('SHOW', "Show", "Show"), 214 | ('HIDE', "Hide", "Hide") 215 | }) # type: ignore 216 | 217 | def execute(self, context): 218 | if self.group == 'HIGH': 219 | suffixes = ("_high",) + tuple("_high.{:03d}".format(i) for i in range(128)) 220 | elif self.group == 'LOW': 221 | suffixes = ("_low",) + tuple("_low.{:03d}".format(i) for i in range(128)) 222 | else: 223 | suffixes = ("_low",) + tuple("_low.{:03d}".format(i) for i in range(128)) + ("_high",) + tuple("_high.{:03d}".format(i) for i in range(30)) 224 | 225 | for obj in bpy.data.objects: 226 | if not obj.name.endswith(suffixes): 227 | continue 228 | if self.action == 'SHOW': 229 | obj.hide_set(False) 230 | else: 231 | obj.hide_set(True) 232 | return {'FINISHED'} 233 | 234 | 235 | def draw_quick_bake_name(self, context, draw_header=False): 236 | def draw_toggle_viewport(context, group, layout): 237 | row = layout.row() 238 | row.label(text=group.lower()) 239 | op = row.operator("keyops.hide_show_low_high", text="SHOW") 240 | op.group = group 241 | op.action = 'SHOW' 242 | op = row.operator("keyops.hide_show_low_high", text="HIDE") 243 | op.group = group 244 | op.action = 'HIDE' 245 | 246 | layout = self.layout 247 | box = layout.box() 248 | row = box.row(align=True) 249 | if draw_header: 250 | row.label(text="Quick Bake Name", icon='MATSHADERBALL') 251 | row = box.row(align=True) 252 | row.operator("keyops.quick_bake_name", text="Quick Bake Name") 253 | row.scale_y = 1.3 254 | row.scale_x = 0.7 255 | row.operator("keyops.quick_bake_name", text="Rename").type = "RENAME" 256 | 257 | row = box.row(align=True) 258 | row.scale_y = 0.95 259 | row.operator("keyops.quick_bake_name", text="Add _high").type = "set_high_name" 260 | row.operator("keyops.quick_bake_name", text="Add _low").type = "set_low_name" 261 | 262 | draw_toggle_viewport(context, 'HIGH', box) 263 | draw_toggle_viewport(context, 'LOW', box) 264 | # draw_toggle_viewport(context, 'ALL', box) 265 | 266 | class QuickBakeNamePanel(bpy.types.Panel): 267 | bl_description = "Quick Bake Name Panel" 268 | bl_label = "Quick Bake Name" 269 | bl_idname = "KEYOPS_PT_quick_bake_name_panel" 270 | bl_space_type = 'VIEW_3D' 271 | bl_region_type = 'UI' 272 | bl_category = 'Toolkit' 273 | bl_options = {'DEFAULT_CLOSED'} 274 | # bl_parent_id = "KEYOPS_PT_toolkit_panel" 275 | 276 | @classmethod 277 | def poll(cls, context): 278 | if context.mode == "OBJECT": 279 | return True 280 | 281 | def draw_header(self, context): 282 | layout = self.layout 283 | layout.label(icon='MATSHADERBALL') 284 | 285 | def draw(self, context): 286 | draw_quick_bake_name(self, context) -------------------------------------------------------------------------------- /operators/quick_export.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | from ..utils.pref_utils import get_keyops_prefs 4 | 5 | """"" 6 | TODO: 7 | add more filetypes, settings and rules what should be exported 8 | skip objects that has wire shading mode, to not export out booleons 9 | """ 10 | 11 | class QuickExport(bpy.types.Operator): 12 | bl_idname = "keyops.quick_export" 13 | bl_label = "KeyOps: Quick Export" 14 | bl_options = {'REGISTER', 'UNDO'} 15 | 16 | scale: bpy.props.FloatProperty(name="Scale", default=1.0) # type: ignore 17 | format: bpy.props.EnumProperty(name="Format", items=[("OBJ", "Wavefront (.obj", "Wavefront (.obj)"), ("FBX", ".fbx", ".fbx"), ("GLTF", "GLTF", "GLTF"), ("USD", "USD", "USD"),("ABC", "Alembic (.ABC)", "Alembic")], default="OBJ") # type: ignore 18 | selected: bpy.props.BoolProperty(name="Selected", default=True) # type: ignore 19 | skip_wire: bpy.props.BoolProperty(name="Skip Wire Display", default=True) # type: ignore 20 | only_visible: bpy.props.BoolProperty(name="Only Visible", default=True) # type: ignore 21 | 22 | def draw(self, context): 23 | layout = self.layout 24 | layout.prop(self, "scale") 25 | layout.prop(self, "selected") 26 | layout.prop(self, "skip_wire") 27 | layout.prop(self, "only_visible") 28 | layout.prop(self, "format") 29 | 30 | def execute(self, context): 31 | active_collection = bpy.context.view_layer.active_layer_collection 32 | blend_file_name = bpy.path.basename(bpy.context.blend_data.filepath) 33 | file_name, file_ext = os.path.splitext(blend_file_name) 34 | file_name = ''.join([c for c in file_name if not c.isdigit() and c != '.']) 35 | blend_file_path = bpy.data.filepath 36 | dirs = blend_file_path.split(os.sep) 37 | dirs.pop() 38 | 39 | if not dirs: 40 | self.report({'ERROR'}, "Blend file not saved") 41 | return {'CANCELLED'} 42 | export_path = blend_file_path 43 | 44 | if "low" in active_collection.name.lower() or "high" in active_collection.name.lower(): 45 | suffix = "_high" if "high" in active_collection.name.lower() else "_low" 46 | file_name += suffix 47 | 48 | export_path = os.path.join(os.path.dirname(blend_file_path), "Bake", file_name + f".{self.format.lower()}") 49 | if not os.path.exists(os.path.dirname(export_path)): 50 | os.makedirs(os.path.dirname(export_path)) 51 | self.report({'INFO'}, f"Exported: {file_name} to Bake Folder") 52 | else: 53 | export_path = os.path.join(os.path.dirname(blend_file_path), file_name + f".{self.format.lower()}") 54 | self.report({'INFO'}, f"Exported: {file_name} to .blend file folder") 55 | 56 | if self.selected: 57 | if self.only_visible: 58 | selected = context.selected_objects 59 | for obj in context.scene.objects: 60 | if obj.select_get(): 61 | obj.select_set(False) 62 | 63 | for obj in selected: 64 | obj.select_set(True) 65 | 66 | if self.skip_wire: 67 | deselect_objects = context.selected_objects 68 | deselect_objects_skip_wire = [obj for obj in deselect_objects if obj.display_type == 'WIRE'] 69 | for obj in deselect_objects_skip_wire: 70 | obj.select_set(False) 71 | 72 | 73 | if self.format == "OBJ": 74 | bpy.ops.wm.obj_export(filepath=export_path, apply_modifiers=True, export_selected_objects=self.selected, global_scale=self.scale) 75 | elif self.format == "FBX": 76 | bpy.ops.export_scene.fbx(filepath=export_path, use_selection=self.selected, global_scale=self.scale) 77 | elif self.format == "GLTF": 78 | bpy.ops.export_scene.gltf(filepath=export_path, use_selection=self.selected) 79 | elif self.format == "USD": 80 | bpy.ops.wm.usd_export(filepath=export_path, selected_objects_only=self.selected) 81 | elif self.format == "ABC": 82 | bpy.ops.wm.alembic_export(filepath=export_path, selected=self.selected, global_scale=self.scale) 83 | return {'FINISHED'} 84 | 85 | 86 | # def register(): 87 | # bpy.utils.register_class(QuickExportPanel) 88 | # def unregister(): 89 | # bpy.utils.unregister_class(QuickExportPanel) 90 | 91 | # class QuickExportPanel(bpy.types.Panel): 92 | # bl_description = "Quick Export Panel" 93 | # bl_label = "Quick Export" 94 | # bl_idname = "KEYOPS_PT_quick_export_panel" 95 | # bl_space_type = 'VIEW_3D' 96 | # bl_region_type = 'UI' 97 | # bl_category = 'Toolkit' 98 | # bl_options = {'DEFAULT_CLOSED'} 99 | 100 | # @classmethod 101 | # def poll(cls, context): 102 | # if context.mode == "OBJECT": 103 | # return True 104 | 105 | # def draw(self, context): 106 | # layout = self.layout 107 | # row = layout.row() 108 | # row.operator("keyops.quick_export", text="Quick Export", icon="TRIA_RIGHT") 109 | # row.scale_y = 1.25 110 | # row = layout.row() 111 | # row.prop(context.scene, "scale", text="Scale") 112 | 113 | # def register(): 114 | # bpy.types.Scene.scale = bpy.props.FloatProperty(name="Scale", default=1.0) 115 | 116 | # def unregister(): 117 | # del bpy.types.Scene.scale 118 | -------------------------------------------------------------------------------- /operators/rebind.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | #combine to one class 4 | 5 | class ContextMenuRebindW(bpy.types.Operator): 6 | bl_idname = "keyops.rebind_w" 7 | bl_label = "Context Menu W" 8 | 9 | def execute(self, context): 10 | for km in bpy.context.window_manager.keyconfigs.user.keymaps: 11 | for kmi in km.keymap_items: 12 | if kmi.type == "RIGHTMOUSE": 13 | 14 | if "Object" in kmi.name and "Context Menu" in kmi.name: 15 | km.keymap_items.new("wm.call_menu", "W", "PRESS", shift=False, ctrl=False, alt=False).properties.name = kmi.properties.name 16 | km.keymap_items.remove(kmi) 17 | elif "Vertex Paint" in kmi.name and "Context Menu" in kmi.name: 18 | km.keymap_items.new("wm.call_menu", "W", "PRESS", shift=False, ctrl=False, alt=False).properties.name = kmi.properties.name 19 | km.keymap_items.remove(kmi) 20 | # elif "Node" in kmi.name and "Context Menu" in kmi.name: 21 | # km.keymap_items.new("wm.call_menu", "W", "PRESS", shift=False, ctrl=False, alt=False).properties.name = kmi.properties.name 22 | # km.keymap_items.remove(kmi) 23 | elif "Curve" in kmi.name and "Context Menu" in kmi.name: 24 | km.keymap_items.new("wm.call_menu", "W", "PRESS", shift=False, ctrl=False, alt=False).properties.name = kmi.properties.name 25 | km.keymap_items.remove(kmi) 26 | elif "Lattice" in kmi.name and "Context Menu" in kmi.name: 27 | km.keymap_items.new("wm.call_menu", "W", "PRESS", shift=False, ctrl=False, alt=False).properties.name = kmi.properties.name 28 | km.keymap_items.remove(kmi) 29 | elif kmi.name == "Call Menu" and kmi.properties.name == "VIEW3D_MT_edit_mesh_context_menu": 30 | km.keymap_items.new("wm.call_menu", "W", "PRESS", shift=False, ctrl=False, alt=False).properties.name = kmi.properties.name 31 | km.keymap_items.remove(kmi) 32 | # elif kmi.name == "Outliner Context Menu" and kmi.properties.name == "OUTLINER_MT_context_menu": 33 | # km.keymap_items.new("wm.call_menu", "W", "PRESS", shift=False, ctrl=False, alt=False).properties.name = kmi.properties.name 34 | # km.keymap_items.remove(kmi) 35 | # elif kmi.name == "Outliner" and "Context Menu" in kmi.name: 36 | # km.keymap_items.new("wm.call_menu", "W", "PRESS", shift=False, ctrl=False, alt=False).properties.name = kmi.properties.name 37 | # km.keymap_items.remove(kmi) 38 | 39 | return {'FINISHED'} 40 | 41 | class ContextMenuRebindRightClick(bpy.types.Operator): 42 | bl_idname = "keyops.rebind_rightclick" 43 | bl_label = "Context Menu Rightclick" 44 | 45 | def execute(self, context): 46 | for keymap in bpy.context.window_manager.keyconfigs.user.keymaps: 47 | for keymap_item in keymap.keymap_items: 48 | if keymap_item.name == "Call Menu" and keymap_item.properties.name == "VIEW3D_MT_edit_mesh_context_menu": 49 | keymap_item.type = "RIGHTMOUSE" 50 | elif "Context Menu" in keymap_item.name: 51 | if "Object" in keymap_item.name and keymap_item.type == "W": 52 | keymap_item.type = "RIGHTMOUSE" 53 | elif "Vertex Paint" in keymap_item.name and keymap_item.type == "W": 54 | keymap_item.type = "RIGHTMOUSE" 55 | #need to rebind switch tool as well! 56 | # elif "Node" in keymap_item.name and keymap_item.type == "W": 57 | # keymap_item.type = "RIGHTMOUSE" 58 | elif "Curve" in keymap_item.name and keymap_item.type == "W": 59 | keymap_item.type = "RIGHTMOUSE" 60 | elif "Lattice" in keymap_item.name and keymap_item.type == "W": 61 | keymap_item.type = "RIGHTMOUSE" 62 | # elif "Outliner Context Menu" in keymap_item.name and keymap_item.type == "W": 63 | # keymap_item.type = "RIGHTMOUSE" 64 | 65 | return {'FINISHED'} 66 | 67 | 68 | class AddObjectPieRebindShiftA(bpy.types.Operator): 69 | bl_idname = "keyops.add_object_pie_rebind" 70 | bl_label = "KeyOps: Add Object Pie Rebind Shift A" 71 | bl_description = "Rebind Add Object Pie to Shift A" 72 | 73 | type: bpy.props.StringProperty(default="") # type: ignore 74 | 75 | def execute(self, context): 76 | if self.type == "Add Object Pie Rebind Shift A": 77 | for keymap in bpy.context.window_manager.keyconfigs.user.keymaps: 78 | for keymap_item in keymap.keymap_items: 79 | if keymap_item.name == "Mesh" and keymap_item.type == "A" and keymap_item.shift: 80 | keymap_item.alt = True 81 | if keymap_item.name == "Add Mesh Pie" and keymap_item.type == "A" and keymap_item.shift and keymap_item.alt: 82 | keymap_item.alt = False 83 | 84 | if keymap.name == "Object Mode": 85 | for keymap_item in keymap.keymap_items: 86 | if keymap_item.name == "Add" and keymap_item.type == "A" and keymap_item.shift: 87 | keymap_item.alt = True 88 | 89 | elif self.type == "Add Object Pie Rebind Shift Alt A": 90 | for keymap in bpy.context.window_manager.keyconfigs.user.keymaps: 91 | for keymap_item in keymap.keymap_items: 92 | if keymap_item.name == "Mesh" and keymap_item.type == "A" and keymap_item.shift and keymap_item.alt: 93 | keymap_item.alt = False 94 | if keymap_item.name == "Add Mesh Pie" and keymap_item.type == "A" and keymap_item.shift: 95 | keymap_item.alt = True 96 | 97 | if keymap.name == "Object Mode": 98 | for keymap_item in keymap.keymap_items: 99 | if keymap_item.name == "Add" and keymap_item.type == "A" and keymap_item.shift and keymap_item.alt: 100 | keymap_item.alt = False 101 | return {'FINISHED'} 102 | 103 | class SpaceToViewPieShift(bpy.types.Operator): 104 | bl_idname = "keyops.space_to_view_camera_pie" 105 | bl_label = "KeyOps: Space to View Camera Pie" 106 | bl_description = "Rebinds View Camera Pie to Space" 107 | 108 | type: bpy.props.StringProperty(default="") # type: ignore 109 | 110 | def execute(self, context): 111 | if self.type == "Space To View Camera Pie Shift": 112 | for keymap in bpy.context.window_manager.keyconfigs.user.keymaps: 113 | for keymap_item in keymap.keymap_items: 114 | if keymap_item.name == "Play Animation" and keymap_item.type == "SPACE" and keymap_item.shift==False: 115 | keymap_item.shift = True 116 | 117 | for keymap in bpy.context.window_manager.keyconfigs.user.keymaps: 118 | for keymap_item in keymap.keymap_items: 119 | if keymap_item.name == "View" and keymap_item.idname == "wm.call_menu_pie": 120 | keymap_item.type = "SPACE" 121 | 122 | elif self.type == "Space To View Camera Pie": 123 | for keymap in bpy.context.window_manager.keyconfigs.user.keymaps: 124 | for keymap_item in keymap.keymap_items: 125 | if keymap_item.name == "Play Animation" and keymap_item.type == "SPACE" and keymap_item.shift: 126 | keymap_item.shift = False 127 | 128 | for keymap in bpy.context.window_manager.keyconfigs.user.keymaps: 129 | for keymap_item in keymap.keymap_items: 130 | if keymap_item.name == "View" and keymap_item.idname == "wm.call_menu_pie": 131 | keymap_item.type = "ACCENT_GRAVE" 132 | return {'FINISHED'} 133 | 134 | 135 | classes = [ContextMenuRebindW, 136 | ContextMenuRebindRightClick, 137 | AddObjectPieRebindShiftA, 138 | SpaceToViewPieShift, 139 | ] 140 | 141 | class Rebind(bpy.types.Operator): 142 | bl_idname = "keyops.rebind" 143 | bl_label = "KeyOps: Rebind" 144 | bl_description = "Move Pivot Key Press" 145 | bl_options = {'INTERNAL'} 146 | 147 | type: bpy.props.StringProperty(default="") # type: ignore 148 | 149 | def execute(self, context): 150 | if self.type == "Default_Pie": 151 | prefs = bpy.context.preferences.view 152 | prefs.pie_animation_timeout = 6 153 | prefs.pie_tap_timeout = 20 154 | prefs.pie_initial_timeout = 0 155 | prefs.pie_menu_radius = 100 156 | prefs.pie_menu_threshold = 12 157 | prefs.pie_menu_confirm = 0 158 | elif self.type == "No_Lag_Pie": 159 | prefs = bpy.context.preferences.view 160 | prefs.pie_animation_timeout = 0 161 | prefs.pie_tap_timeout = 0 162 | prefs.pie_initial_timeout = 0 163 | prefs.pie_menu_radius = 85 164 | prefs.pie_menu_threshold = 11 165 | prefs.pie_menu_confirm = 0 166 | elif self.type == "Remove Select Type Shortcuts": 167 | for keymap in bpy.context.window_manager.keyconfigs.user.keymaps: 168 | for keymap_item in keymap.keymap_items: 169 | if keymap_item.name == "Select Mode" and keymap_item.type in ('ONE', 'TWO', 'THREE') and not all([not keymap_item.ctrl, not keymap_item.alt, not keymap_item.shift]): 170 | keymap_item.active = False 171 | elif self.type == "Add Select Type Shortcuts": 172 | for keymap in bpy.context.window_manager.keyconfigs.user.keymaps: 173 | for keymap_item in keymap.keymap_items: 174 | if keymap_item.name == "Select Mode" and keymap_item.type in ('ONE', 'TWO', 'THREE') and not all([not keymap_item.ctrl, not keymap_item.alt, not keymap_item.shift]): 175 | keymap_item.active = True 176 | 177 | return {'FINISHED'} 178 | 179 | 180 | def register(): 181 | for cls in classes: 182 | bpy.utils.register_class(cls) 183 | 184 | def unregister(): 185 | for cls in reversed(classes): 186 | bpy.utils.unregister_class(cls) 187 | -------------------------------------------------------------------------------- /operators/smart_apply_scale.py: -------------------------------------------------------------------------------- 1 | # Based on Scale with modifiers by Artem_Poletsky https://blenderartists.org/t/scale-with-modifiers-free-addon-for-blender-2-8/1212309 2 | # It has been modified to work with 4.+ and Geomtry Nodes Modifiers as well as some other minor changes 3 | 4 | import bpy 5 | from mathutils import Vector 6 | 7 | MODS = { 8 | 'ARRAY': 'function', 9 | 'BOOLEAN': {'double_threshold'}, 10 | 'BEVEL': 'function', 11 | 'SCREW': {'screw_offset', 'merge_threshold'}, 12 | 'MIRROR': {'merge_threshold'}, 13 | 'SOLIDIFY': {'thickness'}, 14 | 'WIREFRAME': {'thickness'}, 15 | 'DISPLACE': 'function', 16 | 'SHRINKWRAP': {'offset'}, 17 | 'WELD': {'merge_threshold'}, 18 | 'SKIN': 'function', 19 | 'REMESH': {'voxel_size'}, 20 | } 21 | 22 | def apply_scale(obj): 23 | bpy.ops.object.transform_apply(scale=True, location=False, rotation=False) 24 | 25 | 26 | def objectsSelectSet(objects, value): 27 | for o in objects: 28 | o.select_set(value) 29 | 30 | def funcBEVEL(mod, scale, object, operator): 31 | if mod.offset_type != 'PERCENT': 32 | mod.width *= scale 33 | return False 34 | 35 | def funcBEVELget(mod): 36 | return mod.width 37 | 38 | def funcBEVELset(mod, size): 39 | mod.width = size 40 | 41 | def funcARRAY(mod, scale, object, operator): 42 | mod.constant_offset_displace[0] *= scale 43 | mod.constant_offset_displace[1] *= scale 44 | mod.constant_offset_displace[2] *= scale 45 | mod.merge_threshold *= scale 46 | return False 47 | 48 | def funcARRAYget(mod): 49 | return Vector((mod.constant_offset_displace[0], mod.constant_offset_displace[1], mod.constant_offset_displace[2], mod.merge_threshold)) 50 | 51 | def funcARRAYset(mod, size): 52 | mod.constant_offset_displace[0] = size[0] 53 | mod.constant_offset_displace[1] = size[1] 54 | mod.constant_offset_displace[2] = size[2] 55 | mod.merge_threshold = size[3] 56 | 57 | def funcDISPLACE(mod, scale, object, operator): 58 | mod.strength *= scale 59 | if (bool(mod.texture) 60 | & ((mod.texture_coords == "LOCAL") | (mod.texture_coords == "OBJECT")) 61 | & operator.scaleTextures): 62 | if hasattr(mod.texture, 'noise_scale'): 63 | mod.texture.noise_scale *= scale 64 | return "objects have displace texture applied." 65 | else: 66 | return False 67 | 68 | def funcDISPLACEget(mod): 69 | return mod.strength 70 | 71 | def funcDISPLACEset(mod, size): 72 | mod.strength = size 73 | 74 | def funcSKIN(mod, scale, object, operator): 75 | verts = object.data.skin_vertices[0].data 76 | for v in verts: 77 | v.radius[0] *= scale 78 | v.radius[1] *= scale 79 | 80 | def funcSKINget(mod): 81 | # unsupported 82 | return 1 83 | 84 | def funcSKINset(mod, size): 85 | # unsupported 86 | return 87 | 88 | def func_geom_nodes(scale, object): 89 | if object.modifiers: 90 | for mod in object.modifiers: 91 | if mod.type == 'NODES': 92 | if mod.node_group: 93 | node_group = mod.node_group 94 | input_node = next((node for node in node_group.nodes if node.type == 'GROUP_INPUT'),None) 95 | 96 | for node_input in input_node.outputs[:-1]: 97 | if not node_input.type == 'GEOMETRY': 98 | socket_id = node_input.identifier 99 | socket_type = node_input.type 100 | if socket_type == 'VALUE': 101 | if'NodeSocketFloatDistance' in str(node_input): 102 | mod[socket_id] = scale * mod[socket_id] 103 | 104 | elif socket_type == 'VECTOR': 105 | if'NodeSocketVectorTranslation' in str(node_input): 106 | mod[socket_id] = Vector(mod[socket_id]) * scale 107 | return 108 | 109 | def getModifierSize(mod, scale): 110 | attr = MODS[mod.type] 111 | if attr == 'function': 112 | return globals()["func" + mod.type + "get"](mod) * scale 113 | tupl = () 114 | for key in attr: 115 | if not hasattr(mod, key): 116 | continue 117 | tupl += (getattr(mod, key), ) 118 | if len(tupl) == 0: 119 | return 1 120 | if len(tupl) == 1: 121 | return tupl[0] * scale 122 | return Vector(tupl) * scale 123 | 124 | def setModifierSize(mod, size): 125 | attr = MODS[mod.type] 126 | if attr == 'function': 127 | globals()["func" + mod.type + "set"](mod, size) 128 | return 129 | 130 | i = 0 131 | for key in attr: 132 | if not hasattr(mod, key): 133 | continue 134 | if type(size) == Vector: 135 | setattr(mod, key, size[i]) 136 | else: 137 | setattr(mod, key, size) 138 | i+=1 139 | 140 | def get_scale(obj): 141 | s = obj.scale 142 | return (s[0] + s[1] + s[2]) / 3 143 | 144 | def equal_points_len(target, source): 145 | tl = len(target) 146 | sl = len(source) 147 | if tl == sl: 148 | return 149 | while tl > sl: 150 | target.remove(target[1]) 151 | tl = len(target) 152 | while tl < sl: 153 | target.add(0, 0) 154 | tl = len(target) 155 | return 156 | def copy_profile(target, source): 157 | copy_modifier(target, source) 158 | equal_points_len(target.points, source.points) 159 | # len = max(len(source.points), len(target.points)) 160 | for i in range(0, len(source.points)): 161 | p1 = source.points[i] 162 | p2 = target.points[i] 163 | # new_point = target.points.add(p.location[0], p.location[1]) 164 | p2.handle_type_1 = p1.handle_type_1 165 | p2.handle_type_2 = p1.handle_type_2 166 | p2.location = p1.location 167 | # copy_modifier(new_point, p) 168 | target.update() 169 | return 170 | 171 | def copy_modifier(target, source, string=""): 172 | """Copy attributes from source to target that have string in them""" 173 | for attr in dir(source): 174 | if attr in {'custom_profile'}: 175 | copy_profile(getattr(target, attr), getattr(source, attr)) 176 | continue 177 | if attr.find(string) > -1: 178 | try: 179 | setattr(target, attr, getattr(source, attr)) 180 | except: 181 | pass 182 | return 183 | 184 | # class UnifyModifiersSizeOperator(bpy.types.Operator): 185 | # """Unify modifiers""" 186 | # bl_idname = "object.unify_modifiers_operator" 187 | # bl_label = "Unify modifiers" 188 | # bl_options = {'REGISTER', 'UNDO'} 189 | 190 | # use_names: bpy.props.BoolProperty(name="Use names", default=False) 191 | # copy_all: bpy.props.BoolProperty(name="Copy all attributes", default=False) 192 | 193 | # @classmethod 194 | # def poll(cls, context): 195 | # return (context.space_data.type == 'VIEW_3D' 196 | # and len(context.selected_objects) > 0 197 | # and context.view_layer.objects.active 198 | # and context.object.mode == 'OBJECT') 199 | 200 | # def execute(self, context): 201 | # source = context.view_layer.objects.active 202 | # targets = list(context.selected_objects) 203 | # targets.remove(source) 204 | # source_scale = abs(get_scale(source)) 205 | # indices = {} 206 | # for mod in reversed(source.modifiers): 207 | # if not mod.type in MODS: 208 | # continue 209 | # size = getModifierSize(mod, source_scale) 210 | # indices[mod.type] = indices[mod.type] + 1 if mod.type in indices else 0 211 | # if not mod.show_viewport: 212 | # continue 213 | # source_index = indices[mod.type] 214 | # for t in targets: 215 | # target_scale = abs(get_scale(t)) 216 | # target_index = 0 217 | # for m in reversed(t.modifiers): 218 | # # print(source_index, target_index, mod.type, m.type) 219 | # if m.type == mod.type: 220 | # do_set_size = self.use_names and m.name == mod.name 221 | # # print(self.use_names, m.name, mod.name, do_set_size) 222 | # if not do_set_size and not self.use_names: 223 | # do_set_size = target_index == source_index 224 | # if do_set_size and target_scale != 0: 225 | # if self.copy_all: 226 | # copy_modifier(m, mod) 227 | # setModifierSize(m, size / target_scale) 228 | # target_index += 1 229 | # return {'FINISHED'} 230 | 231 | 232 | class SmartApplyScale(bpy.types.Operator): 233 | bl_idname = "object.smart_apply_scale" 234 | bl_label = "Smart Apply Scale" 235 | bl_description = "Apply scale while compensating in the modifiers for the scale difference" 236 | bl_options = {'REGISTER', 'UNDO'} 237 | 238 | deselect : bpy.props.BoolProperty(name="Deselect problem objects", default=False) # type: ignore 239 | scaleTextures: bpy.props.BoolProperty(name="Scale procedural displacement textures", default=False) # type: ignore 240 | makeClonesReal: bpy.props.BoolProperty(name="Make objects single user", default=False) # type: ignore 241 | geomtryNodes: bpy.props.BoolProperty(name="Geometry Nodes", default=True, description="Only works if the input has Distance Subtype (m)") # type: ignore 242 | 243 | def execute(self, context): 244 | objects = context.selected_objects 245 | notEven = [] 246 | clones = [] 247 | funcWarnings = [] 248 | isClonesModified = False 249 | warningMessage = "" 250 | 251 | if not objects: 252 | self.report({'WARNING'}, "No objects selected") 253 | return { 'CANCELLED' } 254 | 255 | for obj in objects: 256 | if not obj.data: 257 | continue 258 | 259 | users = obj.data.users 260 | if obj.data.use_fake_user: 261 | users-=1 262 | if users > 1: 263 | if self.makeClonesReal & (not isClonesModified): 264 | isClonesModified = True 265 | bpy.ops.object.make_single_user(type='SELECTED_OBJECTS', object=True, obdata=True, material=False, animation=False) 266 | warningMessage = "Objects are single user now. " 267 | else: 268 | clones.append(obj) 269 | continue 270 | 271 | s = obj.scale 272 | isEven = s[0] == s[1] == s[2] 273 | if not isEven: 274 | notEven.append(obj) 275 | 276 | scale = abs((s[0] + s[1] + s[2]) / 3) 277 | 278 | for mod in obj.modifiers: 279 | if (mod.type in MODS) & mod.show_viewport: 280 | if MODS[mod.type] == 'function': 281 | result = globals()["func" + mod.type](mod, scale, obj, self) 282 | if result: 283 | funcWarnings.append((obj, result)) 284 | else: 285 | for attrname in MODS[mod.type]: 286 | if not hasattr(mod, attrname): 287 | continue 288 | val = getattr(mod, attrname) 289 | setattr(mod, attrname, val * scale) 290 | if self.geomtryNodes: 291 | func_geom_nodes(scale, obj) 292 | 293 | warningObjects = [] 294 | if len(funcWarnings): 295 | keys = {} 296 | for obj, w in funcWarnings: 297 | warningObjects.append(obj) 298 | 299 | if not w in keys: 300 | keys[w] = 1 301 | else: 302 | keys[w] += 1 303 | 304 | for k in keys: 305 | warningMessage += ("{:d} " + k + " ").format(keys[k]) 306 | 307 | 308 | clonesLen = len(clones) 309 | if clonesLen: 310 | warningMessage += "{:d} objects are multi-user, ignoring ".format(clonesLen) 311 | objectsSelectSet(clones, False) 312 | 313 | notEvenLen = len(notEven) 314 | if notEvenLen: 315 | warningMessage += "Scale of {:d} objects is not even. ".format(notEvenLen) 316 | if warningMessage: 317 | self.report({'WARNING'}, warningMessage + "Some issues are possible ") 318 | 319 | apply_scale(bpy.context.selected_objects) 320 | 321 | objectsSelectSet(clones, True) 322 | if self.deselect: 323 | objectsSelectSet(clones + notEven + warningObjects, False) 324 | 325 | return { 'FINISHED' } 326 | 327 | def register(): 328 | bpy.types.VIEW3D_MT_object_apply.prepend(menu_apply) 329 | # bpy.types.VIEW3D_MT_make_links.append(menu_make_links) 330 | 331 | def unregister(): 332 | bpy.types.VIEW3D_MT_object_apply.remove(menu_apply) 333 | # bpy.types.VIEW3D_MT_object_apply.remove(menu_make_links) 334 | 335 | def menu_apply(self, context): 336 | layout = self.layout 337 | layout.separator() 338 | 339 | layout.operator_context = "INVOKE_DEFAULT" 340 | layout.operator(SmartApplyScale.bl_idname, text=SmartApplyScale.bl_label, icon='FREEZE') 341 | 342 | # def menu_make_links(self, context): 343 | # layout = self.layout 344 | # layout.separator() 345 | 346 | # layout.operator_context = "INVOKE_DEFAULT" 347 | # # layout.operator(UnifyModifiersSizeOperator.bl_idname, text=UnifyModifiersSizeOperator.bl_label) 348 | 349 | 350 | -------------------------------------------------------------------------------- /operators/smart_seam.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from ..utils.pref_utils import get_keyops_prefs 4 | 5 | #finish this in 4.1 or remove it 6 | class SmartSeam(bpy.types.Operator): 7 | bl_idname = "keyops.smart_seam" 8 | bl_label = "KeyOps: Smart Seam" 9 | bl_options = {'REGISTER', 'UNDO'} 10 | 11 | mark_sharp: bpy.props.BoolProperty(name="Mark Sharp", default=False)# type: ignore 12 | 13 | @classmethod 14 | def poll(cls, context): 15 | return context.active_object is not None and context.active_object.type == 'MESH' and bpy.context.scene.tool_settings.mesh_select_mode[1] 16 | 17 | def execute(self, context): 18 | prefs = get_keyops_prefs() 19 | seam_settings = prefs.smart_seam_settings 20 | 21 | if seam_settings == True: 22 | 23 | selected_objects = bpy.context.selected_objects 24 | 25 | all_seams = True 26 | for obj in selected_objects: 27 | if obj.type == 'MESH' and obj.mode == 'EDIT': 28 | bm = bmesh.from_edit_mesh(obj.data) 29 | selected_edges = [e for e in bm.edges if e.select] 30 | if any(not e.seam for e in selected_edges): 31 | all_seams = False 32 | break 33 | 34 | for obj in selected_objects: 35 | if obj.type == 'MESH' and obj.mode == 'EDIT': 36 | bm = bmesh.from_edit_mesh(obj.data) 37 | selected_edges = [e for e in bm.edges if e.select] 38 | 39 | if all_seams: 40 | bpy.ops.mesh.mark_seam(clear=True) 41 | bpy.ops.mesh.mark_sharp(clear=True) 42 | else: 43 | bpy.ops.mesh.mark_seam(clear=False) 44 | if self.mark_sharp: 45 | bpy.ops.mesh.mark_sharp() 46 | 47 | bmesh.update_edit_mesh(obj.data) 48 | return {'FINISHED'} 49 | 50 | else: 51 | bpy.ops.mesh.mark_seam(clear=False) 52 | if self.mark_sharp: 53 | bpy.ops.mesh.mark_sharp() 54 | return {'FINISHED'} 55 | 56 | # def register(): 57 | # bpy.app.handlers.depsgraph_update_post.append(bpy.app.handlers.persistent(update_edge, persistent=True)) 58 | 59 | # def unregister(): 60 | # bpy.app.handlers.depsgraph_update_post.remove(bpy.app.handlers.persistent(update_edge, persistent=True)) 61 | 62 | class RemoveSeam(bpy.types.Operator): 63 | bl_idname = "keyops.remove_seam" 64 | bl_label = "KeyOps: Remove Seam" 65 | bl_options = {'REGISTER', 'UNDO'} 66 | @classmethod 67 | def poll(cls, context): 68 | return context.active_object.type == 'MESH' 69 | 70 | def execute(self, context): 71 | bpy.ops.mesh.mark_seam(clear=True) 72 | bpy.ops.mesh.mark_sharp(clear=True) 73 | return {'FINISHED'} 74 | 75 | # def update_edge_selection(self, scene): 76 | # if scene is None: 77 | # scene = bpy.context.scene 78 | 79 | # has_view_3d_area = False 80 | 81 | # for screen in bpy.data.screens: 82 | # for area in screen.areas: 83 | # if area.type == 'VIEW_3D': 84 | # has_view_3d_area = True 85 | # break 86 | # if has_view_3d_area: 87 | # break 88 | 89 | # if has_view_3d_area and bpy.context.mode == 'EDIT_MESH' and not bpy.context.space_data.overlay.show_retopology: 90 | # obj = bpy.context.active_object 91 | # if obj is not None and obj.type == 'MESH' and bpy.context.preferences.themes.get('Default') is not None: 92 | # theme = bpy.context.preferences.themes['Default'] 93 | # face_select_alpha_def = 0.18039216101169586 94 | # edge_select_yellow = (1.0, 0.890196, 0.0) 95 | # edge_select_def = (1.0, 0.6274510025978088, 0.0) 96 | # edge_select_off = (0,0,0) 97 | 98 | # if bpy.context.tool_settings.mesh_select_mode[0]: 99 | # theme.view_3d.edge_select = edge_select_def 100 | # theme.view_3d.face_select[3] = face_select_alpha_def 101 | 102 | # elif bpy.context.tool_settings.mesh_select_mode[1]: 103 | # theme.view_3d.edge_select = edge_select_yellow 104 | 105 | # elif bpy.context.tool_settings.mesh_select_mode[2]: 106 | # theme.view_3d.edge_select = edge_select_off 107 | # theme.view_3d.edge_select = edge_select_def 108 | # theme.view_3d.face_select[3] = face_select_alpha_def 109 | 110 | # else: 111 | # pass 112 | 113 | # def update_edge(self, context): 114 | # update_edge_selection(context.scene, None) 115 | 116 | -------------------------------------------------------------------------------- /operators/smart_uv_sync.py: -------------------------------------------------------------------------------- 1 | import bmesh 2 | import bpy 3 | from ..utils.pref_utils import get_keyops_prefs 4 | 5 | #Based on the code from the excellent addon UV Toolkit by Alex Dev that is sadly no longer available, please come back Alex! :( 6 | #Only an very old version is still available, but I doubt it still works in new version of Blender https://alexbel.gumroad.com/l/NbMya 7 | #The Toggle UV sync was an serverly underrated operation that basically fixes the uv editor in Blender, and I wanted to highlight its importance and keep it alive since its no longer officaly available anywhere. 8 | #Its the only way to get the UV editor in Blender to not be a horrible slow mess to work in and its a must have for anyone who works with UVs. 9 | 10 | class SmartUVSync(bpy.types.Operator): 11 | bl_idname = "keyops.smart_uv_sync" 12 | bl_label = "KeyOps: Smart UV Sync" 13 | bl_description = "Right Click to Toggle Sync Mode and UV Selection" 14 | bl_options = {'REGISTER'} 15 | 16 | @classmethod 17 | def poll(cls, context): 18 | return context.mode == 'EDIT_MESH' 19 | 20 | def fast_sync(self, context): 21 | if bpy.context.scene.tool_settings.use_uv_select_sync: 22 | bpy.context.scene.tool_settings.use_uv_select_sync = True 23 | else: 24 | bpy.context.scene.tool_settings.use_uv_select_sync = False 25 | bpy.ops.mesh.select_all(action='SELECT') 26 | 27 | def sync_uv_selction_mode(self, context, uv_sync_enable): 28 | scene = context.scene 29 | 30 | vertex = True, False, False 31 | edge = False, True, False 32 | face = False, False, True 33 | 34 | if uv_sync_enable: 35 | uv_select_mode = scene.tool_settings.uv_select_mode 36 | tool_settings = context.tool_settings 37 | 38 | if uv_select_mode == 'VERTEX': 39 | tool_settings.mesh_select_mode = vertex 40 | if uv_select_mode == 'EDGE': 41 | tool_settings.mesh_select_mode = edge 42 | if uv_select_mode == 'FACE': 43 | tool_settings.mesh_select_mode = face 44 | 45 | else: 46 | mesh_select_mode = context.tool_settings.mesh_select_mode[:] 47 | tool_settings = scene.tool_settings 48 | 49 | if mesh_select_mode == vertex: 50 | tool_settings.uv_select_mode = 'VERTEX' 51 | if mesh_select_mode == edge: 52 | tool_settings.uv_select_mode = 'EDGE' 53 | if mesh_select_mode == face: 54 | tool_settings.uv_select_mode = 'FACE' 55 | 56 | def sync_selected_elements(self, context, uv_sync_enable): 57 | for ob in context.objects_in_mode_unique_data: 58 | me = ob.data 59 | bm = bmesh.from_edit_mesh(me) 60 | 61 | uv_layer = bm.loops.layers.uv.verify() 62 | 63 | if uv_sync_enable: 64 | for face in bm.faces: 65 | for loop in face.loops: 66 | loop_uv = loop[uv_layer] 67 | if not loop_uv.select: 68 | face.select = False 69 | 70 | for face in bm.faces: 71 | for loop in face.loops: 72 | loop_uv = loop[uv_layer] 73 | if loop_uv.select: 74 | loop.vert.select = True 75 | 76 | for edge in bm.edges: 77 | vert_count = 0 78 | for vert in edge.verts: 79 | if vert.select: 80 | vert_count += 1 81 | if vert_count == 2: 82 | edge.select = True 83 | 84 | else: 85 | for face in bm.faces: 86 | for loop in face.loops: 87 | loop_uv = loop[uv_layer] 88 | loop_uv.select = False 89 | 90 | mesh_select_mode = context.tool_settings.mesh_select_mode[:] 91 | 92 | if mesh_select_mode[2]: # face 93 | for face in bm.faces: 94 | if face.select: 95 | for loop in face.loops: 96 | loop_uv = loop[uv_layer] 97 | if loop.vert.select: 98 | loop_uv.select = True 99 | # if mesh_select_mode[1]: # edge 100 | # bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') 101 | # print ("edge") 102 | 103 | 104 | else: #vertex 105 | for face in bm.faces: 106 | for loop in face.loops: 107 | loop_uv = loop[uv_layer] 108 | if loop.vert.select: 109 | loop_uv.select = True 110 | 111 | for face in bm.faces: 112 | face.select = True 113 | 114 | bmesh.update_edit_mesh(me) 115 | 116 | def register(): 117 | bpy.utils.register_class(UVEDITORSMARTUVSYNC_PT_Panel) 118 | bpy.types.Scene.smart_uv_sync_enable = bpy.props.BoolProperty(name="Smart UV Sync (Slower)", default=True, description="Right Click to Toggle Sync Mode and UV Selection") 119 | 120 | 121 | def unregister(): 122 | bpy.utils.unregister_class(UVEDITORSMARTUVSYNC_PT_Panel) 123 | del bpy.types.Scene.smart_uv_sync_enable 124 | 125 | 126 | def execute(self, context): 127 | tool_settings = context.tool_settings 128 | uv_sync_enable = not tool_settings.use_uv_select_sync 129 | tool_settings.use_uv_select_sync = uv_sync_enable 130 | 131 | if context.scene.smart_uv_sync_enable: 132 | self.sync_uv_selction_mode(context, uv_sync_enable) 133 | self.sync_selected_elements(context, uv_sync_enable) 134 | 135 | else: 136 | self.fast_sync(context) 137 | 138 | return {'FINISHED'} 139 | 140 | class UVEDITORSMARTUVSYNC_PT_Panel(bpy.types.Panel): 141 | prefs = get_keyops_prefs() 142 | category_name = prefs.smart_uv_sync_panel_name 143 | bl_label = "KeyOps UV" 144 | bl_space_type = 'IMAGE_EDITOR' 145 | bl_region_type = 'UI' 146 | bl_category = category_name 147 | 148 | @classmethod 149 | def poll(cls, context): 150 | return context.mode == 'EDIT_MESH' 151 | 152 | def draw(self, context): 153 | layout = self.layout 154 | row = layout.row() 155 | row.scale_y = 1.5 156 | row.prop(context.scene, "smart_uv_sync_enable", toggle=True, icon='UV_SYNC_SELECT') 157 | -------------------------------------------------------------------------------- /operators/unique_collection_duplicate.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | # todo: 4 | # autofind booleons 5 | # new cutter collection? 6 | # custom name 7 | # auto remove remesh and smooth? 8 | # Auto remove small bevelse? 9 | 10 | 11 | class UniqueCollectionDuplicate(bpy.types.Operator): 12 | bl_label = "Unique Collection Duplicate" 13 | bl_idname = "keyops.unique_collection_duplicate" 14 | bl_description = "Make a copy of the current collection and makes all the booleons unqiue if the booleons are selected" 15 | bl_options = {'REGISTER', 'UNDO'} 16 | 17 | default_name = "" 18 | 19 | @classmethod 20 | def poll(cls, context): 21 | if context.mode == 'OBJECT': 22 | return [obj for obj in context.selected_objects] 23 | 24 | def execute(self, context): 25 | 26 | if bpy.data.collections.get("high"): 27 | self.default_name = "low" 28 | else: 29 | self.default_name = "high" 30 | 31 | bpy.ops.object.duplicate() 32 | bpy.ops.object.move_to_collection(collection_index=0, is_new=True, new_collection_name=self.default_name) 33 | 34 | objects_to_duplicate = [obj for obj in context.selected_objects if obj.data and obj.data.users > 1] 35 | 36 | for obj in objects_to_duplicate: 37 | 38 | obj.data = obj.data.copy() 39 | 40 | booleans = [mod for mod in obj.modifiers if mod.type == 'BOOLEAN' and mod.object and mod.object.data.users > 1] 41 | 42 | for mod in booleans: 43 | mod.object.data = mod.object.data.copy() 44 | 45 | return {'FINISHED'} 46 | -------------------------------------------------------------------------------- /operators/viewport_menu.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | previes_shading_types_settings = [] 4 | 5 | class ViewportMenu(bpy.types.Operator): 6 | bl_idname = "keyops.viewport_menu" 7 | bl_label = "Viewport Overlays Menu" 8 | 9 | type: bpy.props.StringProperty(default='') # type: ignore 10 | 11 | def execute(self, context): 12 | global previes_shading_types_settings 13 | 14 | if self.type == 'toggle_silhouettes': 15 | view3d = context.space_data 16 | 17 | if not view3d.shading.light == 'FLAT': 18 | previes_shading_types_settings = [view3d.shading.show_object_outline, 19 | view3d.shading.show_xray, 20 | view3d.shading.show_backface_culling, 21 | view3d.shading.light, 22 | view3d.shading.background_type, 23 | view3d.overlay.show_overlays, 24 | view3d.shading.type, 25 | view3d.shading.background_color.copy(), 26 | view3d.shading.show_cavity, 27 | view3d.shading.show_shadows, 28 | view3d.shading.show_xray, 29 | view3d.shading.color_type, 30 | view3d.shading.single_color] 31 | # silhouette settings 32 | view3d.shading.light = 'FLAT' 33 | view3d.shading.show_object_outline = False 34 | view3d.shading.background_type = 'VIEWPORT' 35 | view3d.overlay.show_overlays = False 36 | view3d.shading.type = 'SOLID' 37 | view3d.shading.background_color = (0, 0, 0) 38 | view3d.shading.show_cavity = False 39 | view3d.shading.show_shadows = False 40 | view3d.shading.show_xray = False 41 | view3d.shading.color_type = 'SINGLE' 42 | view3d.shading.single_color = (1, 1, 1) 43 | 44 | else: 45 | if previes_shading_types_settings: 46 | # restore previous settings 47 | view3d.shading.show_object_outline = previes_shading_types_settings[0] 48 | view3d.shading.show_xray = previes_shading_types_settings[1] 49 | view3d.shading.show_backface_culling = previes_shading_types_settings[2] 50 | view3d.shading.light = previes_shading_types_settings[3] 51 | view3d.shading.background_type = previes_shading_types_settings[4] 52 | view3d.overlay.show_overlays = previes_shading_types_settings[5] 53 | view3d.shading.type = previes_shading_types_settings[6] 54 | view3d.shading.background_color = previes_shading_types_settings[7] 55 | view3d.shading.show_cavity = previes_shading_types_settings[8] 56 | view3d.shading.show_shadows = previes_shading_types_settings[9] 57 | view3d.shading.show_xray = previes_shading_types_settings[10] 58 | view3d.shading.color_type = previes_shading_types_settings[11] 59 | view3d.shading.single_color = previes_shading_types_settings[12] 60 | 61 | else: 62 | # reset to default settings if no previous settings was found 63 | view3d.shading.show_object_outline = True 64 | view3d.shading.show_xray = False 65 | view3d.shading.show_backface_culling = False 66 | view3d.shading.light = 'STUDIO' 67 | view3d.shading.background_type = 'THEME' 68 | view3d.overlay.show_overlays = True 69 | view3d.shading.type = 'SOLID' 70 | view3d.shading.background_color = (0.05, 0.05, 0.05) 71 | 72 | previes_shading_types_settings = [] 73 | return {'FINISHED'} 74 | 75 | def register(): 76 | bpy.utils.register_class(VIEW3D_MT_viewport_menu) 77 | def unregister(): 78 | bpy.utils.unregister_class(VIEW3D_MT_viewport_menu) 79 | 80 | class VIEW3D_MT_viewport_menu(bpy.types.Menu): 81 | bl_label = "Viewport Overlays" 82 | bl_idname = "VIEW3D_MT_viewport_menu" 83 | 84 | def draw(self, context): 85 | layout = self.layout 86 | view3d = context.space_data 87 | shading = view3d.shading 88 | 89 | split = layout.split(factor=0.5) 90 | 91 | # Left column 92 | col = split.column() 93 | col.prop(view3d.overlay, 'show_wireframes') 94 | col.prop(view3d.overlay, 'show_face_orientation') 95 | if view3d.shading.type == 'SOLID': 96 | col.prop(view3d.shading, 'show_object_outline') 97 | col.prop(view3d.overlay, "show_overlays", text = 'Overlays', emboss=True) 98 | 99 | # Right column 100 | col = split.column() 101 | icon = "SHADING_SOLID" if shading.light != 'FLAT' else "SHADING_RENDERED" 102 | col.operator('keyops.viewport_menu', text='Toggle Silhouette', icon=icon).type = 'toggle_silhouettes' 103 | 104 | if view3d.shading.type != 'WIREFRAME': 105 | col.template_icon_view(view3d.shading, "studio_light", show_labels=True, scale=3.5, scale_popup=4) 106 | # col.label(text='') 107 | 108 | # if view3d.shading.type == 'SOLID': 109 | # col.prop(view3d.shading, "light", text="Test", expand=True) 110 | -------------------------------------------------------------------------------- /operators/zip_release.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import shutil 3 | from pathlib import Path 4 | 5 | 6 | ADDON_DIR_NAME = "Key-Ops-Toolkit" 7 | ITEMS_TO_INCLUDE = ( 8 | "icons", 9 | "menu", 10 | "__init__.py", 11 | "operators", 12 | "blender_manifest.toml", 13 | "utils", 14 | "addon_preferences.py", 15 | "classes_keymap_items.py", 16 | ) 17 | 18 | def parse_args(): 19 | parser = argparse.ArgumentParser() 20 | return parser.parse_args() 21 | 22 | 23 | def get_dir_content_to_ignore(src: str, names: list[str]): 24 | return [name for name in names if name == "__pycache__"] 25 | 26 | 27 | def main(): 28 | root = Path(__file__).resolve().parents[2] 29 | zip_name = f"{ADDON_DIR_NAME}" 30 | temp_dir = root / f"{ADDON_DIR_NAME}_temp" 31 | 32 | if temp_dir.exists(): 33 | shutil.rmtree(temp_dir) 34 | 35 | temp_dir.mkdir() 36 | 37 | for item in ITEMS_TO_INCLUDE: 38 | source = root / ADDON_DIR_NAME / item 39 | dest = temp_dir / item 40 | if source.is_dir(): 41 | shutil.copytree(source, dest, ignore=get_dir_content_to_ignore) 42 | else: 43 | shutil.copy(source, dest) 44 | 45 | shutil.make_archive(root / zip_name, "zip", temp_dir) 46 | 47 | print(f"{zip_name}.zip succesfully created") 48 | 49 | shutil.rmtree(temp_dir) 50 | 51 | 52 | if __name__ == "__main__": 53 | main() 54 | -------------------------------------------------------------------------------- /ui/toolkit_paneL_ui.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from ..utils.pref_utils import get_icon, get_addon_name 3 | from ..operators.poly_count_list import draw_polycount_list_ui 4 | from ..operators.auto_lod import draw_lod_panel 5 | from ..operators.quick_bake_name import draw_quick_bake_name 6 | from ..operators.toggle_retopology import draw_retopology_panel 7 | from ..operators.cad_decimate import draw_cad_decimate_panel 8 | from ..operators.toolkit_panel import draw_edit_mode_panel, draw_modifier_panel 9 | import functools 10 | 11 | show_object_panel = False 12 | 13 | def object_panel(self, context): 14 | layout = self.layout 15 | wm = context.window_manager 16 | 17 | box = layout.box() 18 | box.label(text="Object Operations", icon_value=get_icon("mesh_cube")) 19 | col = box.column(align=False) 20 | 21 | # row = col.row(align=True) 22 | # # add vert, edge, face, click on the menters edit mode 23 | # row.scale_y = 2 24 | # row.scale_x = 2 25 | # row.operator("keyops.toolkit_panel", text="", icon = "VERTEXSEL") 26 | # row.operator("keyops.toolkit_panel", text="", icon = "EDGESEL") 27 | # row.operator("keyops.toolkit_panel", text="", icon = "FACESEL") 28 | 29 | row = col.row(align=True) 30 | row.scale_y = 1.4 31 | row.operator("keyops.toolkit_panel", text="Combine", icon_value=get_icon("union")).type = "Smart_Join_Objects" 32 | row.operator("keyops.toolkit_panel", text="Seperate", icon_value=get_icon("slice")).type = "seprate_objects_by" 33 | 34 | 35 | row = col.row(align=True) 36 | row.scale_y = 1.4 37 | row.operator("keyops.toolkit_panel", text="Apply Modifiers", icon_value=get_icon("mesh")).type = "Instant_Apply_Modifiers" 38 | row.operator("object.smart_apply_scale", text="Freeze Scale", icon = "FREEZE") 39 | row = col.row(align=True) 40 | row.scale_y = 1.4 41 | row.operator("keyops.toolkit_panel", text="Snap to Floor", icon = "VIEW_PERSPECTIVE").type = "snap_to_floor" 42 | row.operator("keyops.toolkit_panel", text="Clear Normals", icon="X").type = "clear_custom_normals" 43 | 44 | row = col.row(align=True) 45 | row.label(text="Duplicate Linked") 46 | row = col.row(align=True) 47 | row.scale_y = 1.4 48 | row.operator("object.duplicate_move_linked", text="Mesh", icon = "LINKED") 49 | row.operator("keyops.toolkit_panel", text="Modifiers", icon = "LINKED").type = "duplicate_linked_modifiers" 50 | 51 | 52 | if context.mode == 'OBJECT': 53 | # box.label(text="Booleans") 54 | row = box.row(align=True) 55 | row = box.row(align=True) 56 | row.label(text="Booleans") 57 | # row = box.row(align=True) 58 | row.scale_y = 1.5 59 | row.scale_x = 4 60 | row.operator("object.add_boolean_modifier_operator", text="", icon_value=get_icon("diffrance")).type = "DIFFERENCE" 61 | row.operator("object.add_boolean_modifier_operator", text="", icon_value=get_icon("union")).type = "UNION" 62 | row.operator("object.add_boolean_modifier_operator", text="", icon_value=get_icon("intersection")).type = "INTERSECT" 63 | row.operator("object.add_boolean_modifier_operator", text="", icon_value=get_icon("slice")).type = "SLICE" 64 | # row.operator("object.add_boolean_modifier_operator", text="", icon_value=get_icon("hole")).type = "SLICE" 65 | 66 | row = box.row() 67 | 68 | # row = box.row() 69 | row.scale_y = 1.1 70 | row.scale_x = 1 71 | row.alignment = 'LEFT' 72 | row.prop(wm, "live_booleans", text="Realtime") 73 | row.operator("object.boolean_scroll", text="Boolean Scroll", icon="MOUSE_MMB_SCROLL") 74 | 75 | # box = layout.box() 76 | # box.label(text="High/Low Collections") 77 | # col = box.column(align=True) 78 | # row = col.row(align=True) 79 | # row.operator("keyops.toolkit_panel", text="Unique Collection Copy", icon="COLLECTION_COLOR_02").type = "unique_collection_duplicat" 80 | # row = col.row(align=True) 81 | # row.operator("keyops.toolkit_panel", text="Toggle").type = "toggle_high_low" 82 | # row.operator("keyops.toolkit_panel", text="high").type = "high" 83 | # row.operator("keyops.toolkit_panel", text="low").type = "low" 84 | 85 | 86 | 87 | def redraw_ui(self, context): 88 | for area in bpy.context.screen.areas: 89 | if area.type == 'VIEW_3D': 90 | for region in area.regions: 91 | if region.type == 'UI' and area.ui_type == 'New Toolkit': 92 | area.tag_redraw() 93 | break 94 | 95 | def update_ui(self, context): 96 | global show_object_panel 97 | show_object_panel = self.show_object_panel 98 | # update the UI when properties change 99 | # This function can be used to refresh the UI or perform other actions 100 | bpy.app.timers.register(redraw_ui, persistent=True, first_interval=0.01) 101 | print("UI updated with new properties") 102 | 103 | 104 | # Add a Blender timer to periodically refresh the UI 105 | 106 | ev = [] 107 | 108 | class ToggleToolkitPanelEnum(bpy.types.Operator): 109 | bl_idname = "keyops.toggle_toolkit_panel_enum" 110 | bl_label = "Toggle Toolkit Panel Enum" 111 | bl_description = "Toggle the visibility of the toolkit panel enum" 112 | 113 | type: bpy.props.StringProperty( 114 | name="ID", 115 | description="ID of the panel to toggle", 116 | default="toolkit_panel_mode" 117 | ) 118 | 119 | def invoke(self, context, event): 120 | if event.shift: 121 | ev.append("shift") 122 | return self.execute(context) 123 | 124 | def execute(self, context): 125 | scene = context.scene 126 | global ev 127 | if "shift" in ev: 128 | scene.toolkit_panel_mode ^= {self.type} 129 | else: 130 | scene.toolkit_panel_mode = {self.type} 131 | 132 | ev = [] 133 | return {'FINISHED'} 134 | 135 | 136 | class NewToolkitPanel(bpy.types.Panel): 137 | bl_description = "KeyOps Toolkit Panel" 138 | bl_label = "Toolkit" 139 | bl_idname = "KEYOPS_PT_new_toolkit_panel" 140 | bl_space_type = 'VIEW_3D' 141 | bl_region_type = 'UI' 142 | bl_category = 'New Toolkit' 143 | 144 | # @classmethod 145 | # def poll(cls, context): 146 | # return context.active_object is not None and context.active_object.mode == 'EDIT' 147 | 148 | def draw_header(self, context): 149 | layout = self.layout 150 | row = layout.row(align=True) 151 | row.scale_x = 0.94 152 | row.label(text="", icon_value=get_icon("K")) 153 | row.label(text="", icon_value=get_icon("ey")) 154 | row.label(text="", icon_value=get_icon("Op")) 155 | row.label(text="", icon_value=get_icon("s")) 156 | 157 | def draw_header_preset(self, context): 158 | layout = self.layout 159 | row = layout.row(align=True) 160 | row.operator("preferences.addon_show", text="", icon='PREFERENCES', emboss=False).module = get_addon_name() 161 | 162 | 163 | def draw(self, context): 164 | global show_object_panel 165 | scene = context.scene 166 | layout = self.layout 167 | # layout.prop_tabs_enum(scene, "toolkit_panel_mode", icon_only=False) 168 | row = layout.row(align=True) 169 | layout = self.layout 170 | row = layout.row(align=True) 171 | row.scale_x = 12 172 | row.scale_y = 1.4 173 | row.alignment = 'EXPAND' 174 | row.prop(context.scene, "toolkit_panel_mode", emboss=True) 175 | row = layout.row(align=True) 176 | row.scale_x = 12 177 | row.scale_y = 1.25 178 | 179 | object = 'OBJECT' in scene.toolkit_panel_mode 180 | modifiers = 'MODIFIERS' in scene.toolkit_panel_mode 181 | lod = 'LOD' in scene.toolkit_panel_mode 182 | cad_decimate = 'CAD_DECIMATE' in scene.toolkit_panel_mode 183 | polycount_list = 'POLYCOUNT_LIST' in scene.toolkit_panel_mode 184 | 185 | # row.operator("keyops.toggle_toolkit_panel_enum", text="", icon_value=get_icon("mesh_cube"), depress=object, emboss=object).type = "OBJECT" 186 | # row.operator("keyops.toggle_toolkit_panel_enum", text="", icon_value=get_icon("modifier"), depress=modifiers, emboss=modifiers).type = "MODIFIERS" 187 | # row.operator("keyops.toggle_toolkit_panel_enum", text="", icon_value=get_icon("mesh_icosphere2"), depress=lod, emboss=lod).type = "LOD" 188 | # row.operator("keyops.toggle_toolkit_panel_enum", text="", icon_value=get_icon("mod_decim"), depress=cad_decimate, emboss=cad_decimate).type = "CAD_DECIMATE" 189 | # row.operator("keyops.toggle_toolkit_panel_enum", text="", icon="SORTSIZE", depress=polycount_list, emboss=polycount_list).type = "POLYCOUNT_LIST" 190 | 191 | if object: 192 | draw_retopology_panel(self, context, draw_header=True) 193 | if context.mode == 'OBJECT': 194 | object_panel(self, context) 195 | draw_quick_bake_name(self, context, draw_header=True) 196 | elif context.mode == 'EDIT_MESH': 197 | draw_edit_mode_panel(self, context, draw_header=True) 198 | if modifiers: 199 | draw_modifier_panel(self, context, draw_header=True) 200 | if lod: 201 | draw_lod_panel(self, context, draw_header=True) 202 | if cad_decimate: 203 | draw_cad_decimate_panel(self, context, draw_header=True) 204 | if polycount_list: 205 | draw_polycount_list_ui(self, context) 206 | 207 | # row = layout.row(align=True) 208 | # row.scale_x = 12 209 | # row.scale_y = 1.25 210 | # object_emboss = show_object_panel 211 | # icoon = get_icon("mesh_cube_selected") 212 | # if not object_emboss: 213 | # icoon = get_icon("mesh_cube") 214 | # print(object_emboss) 215 | 216 | # row.prop(context.scene, "show_object_panel", text="", icon_value=icoon, emboss= object_emboss) 217 | # row.prop(context.scene, "show_modifiers_panel", text="", icon_value=get_icon("modifier"), emboss=scene.show_modifiers_panel) 218 | # row.prop(context.scene, "show_lod_panel", text="", icon_value=get_icon("mesh_icosphere2"), emboss=scene.show_lod_panel) 219 | # row.prop(context.scene, "show_cad_decimate_panel", text="", icon_value=get_icon("mod_decim"), emboss=scene.show_cad_decimate_panel) 220 | 221 | def register(): 222 | 223 | # You can add a toggle to demonstrate dynamic change 224 | bpy.types.Scene.some_toggle = bpy.props.BoolProperty( 225 | name="Toggle Icon Change", 226 | description="Change enum icon dynamically", 227 | default=False 228 | ) 229 | bpy.types.Scene.toolkit_panel_mode = bpy.props.EnumProperty( 230 | name="Toolkit Panel Mode", 231 | description="Manage which panels should show", 232 | items=[ 233 | ('OBJECT', "", "Show Object panel", get_icon("mesh_cube"), 1), 234 | ('MODIFIERS', "", "Show Modifiers panel", get_icon("modifier"), 2), 235 | ('LOD', "", "Show LOD panel", get_icon("mesh_icosphere2"), 4), 236 | ('CAD_DECIMATE', "", "Show CAD Decimate panel", get_icon("mod_decim"), 8), 237 | ("POLYCOUNT_LIST", "", "Show Polycount List panel", get_icon("polycount"), 16), 238 | ], 239 | options={'ENUM_FLAG'}, 240 | default={'OBJECT', 'MODIFIERS'}, 241 | ) 242 | 243 | bpy.types.Scene.show_object_panel = bpy.props.BoolProperty( 244 | name="Show Object Panel", 245 | description="Toggle visibility of the Object panel", 246 | default=True, 247 | update=update_ui 248 | ) 249 | bpy.types.Scene.show_modifiers_panel = bpy.props.BoolProperty( 250 | name="Show Modifiers Panel", 251 | description="Toggle visibility of the Modifiers panel", 252 | default=True, 253 | update=update_ui 254 | ) 255 | bpy.types.Scene.show_lod_panel = bpy.props.BoolProperty( 256 | name="Show LOD Panel", 257 | description="Toggle visibility of the LOD panel", 258 | default=True, 259 | update=update_ui 260 | ) 261 | bpy.types.Scene.show_cad_decimate_panel = bpy.props.BoolProperty( 262 | name="Show CAD Decimate Panel", 263 | description="Toggle visibility of the CAD Decimate panel", 264 | default=True, 265 | update=update_ui 266 | ) 267 | 268 | bpy.utils.register_class(ToggleToolkitPanelEnum) 269 | 270 | def unregister(): 271 | del bpy.types.Scene.some_toggle 272 | del bpy.types.Scene.toolkit_panel_mode 273 | del bpy.types.Scene.show_object_panel 274 | del bpy.types.Scene.show_modifiers_panel 275 | del bpy.types.Scene.show_lod_panel 276 | del bpy.types.Scene.show_cad_decimate_panel 277 | 278 | bpy.utils.unregister_class(ToggleToolkitPanelEnum) 279 | 280 | -------------------------------------------------------------------------------- /utils/mesh_utils.py: -------------------------------------------------------------------------------- 1 | 2 | import bpy 3 | import bmesh 4 | 5 | def modifier_toggle_visability_based(visibility_modifier_dict_list={}): 6 | active_object_name = bpy.context.view_layer.objects.active.name 7 | 8 | if active_object_name not in visibility_modifier_dict_list: 9 | visibility_modifier_dict_list[active_object_name] = [] 10 | 11 | visibility_modifier_list = visibility_modifier_dict_list[active_object_name] 12 | 13 | if len(visibility_modifier_list) <= 0: 14 | ml_act_ob = bpy.context.view_layer.objects.active 15 | for mod in ml_act_ob.modifiers: 16 | if mod.show_viewport: 17 | visibility_modifier_list.append(mod) 18 | mod.show_viewport = False 19 | else: 20 | ml_act_ob = bpy.context.view_layer.objects.active 21 | hidden_modifiers = [] 22 | for mod in ml_act_ob.modifiers: 23 | if mod in visibility_modifier_list: 24 | mod.show_viewport = True 25 | hidden_modifiers.append(mod) 26 | visibility_modifier_list = [mod for mod in visibility_modifier_list if mod not in hidden_modifiers] 27 | visibility_modifier_dict_list[active_object_name] = visibility_modifier_list 28 | 29 | def set_attribute_value_on_selection(obj=None, attr_name="", value=True, domain='POINT', data_type='BOOLEAN'): 30 | mesh = obj.data 31 | 32 | if attr_name and obj: 33 | attribute = mesh.attributes.get(attr_name) 34 | if attribute is None: 35 | attribute = mesh.attributes.new(name=attr_name, domain=domain, type=data_type) 36 | if obj.mode == 'EDIT': 37 | mesh = bpy.context.active_object.data 38 | attribute = mesh.attributes.new(name="new attribute", type="BOOLEAN", domain="POINT") 39 | attribute_values = [i for i in range(len(mesh.vertices))] 40 | 41 | bm = bmesh.from_edit_mesh(mesh) 42 | layer = bm.verts.layers.int.get(attr_name) 43 | 44 | for vert in bm.verts: 45 | print(f"Previous value for {vert} : {vert[layer]}") 46 | vert[layer] = attribute_values[vert.index] 47 | print(f"New value for {vert} : {vert[layer]}") 48 | 49 | bmesh.update_edit_mesh(mesh) 50 | 51 | -------------------------------------------------------------------------------- /utils/pref_utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import rna_keymap_ui 3 | import os 4 | from bpy.utils import previews 5 | from .. import __package__ as base_package 6 | 7 | def get_is_addon_enabled(addon_name): 8 | list_of_addons = bpy.context.preferences.addons.keys() 9 | list_of_addons = [addon.split(".")[-1] for addon in list_of_addons] 10 | return addon_name in list_of_addons 11 | 12 | def get_addon_name(): 13 | return base_package 14 | 15 | def get_keyops_prefs(): 16 | addon_prefs = bpy.context.preferences.addons[base_package] 17 | return addon_prefs.preferences 18 | 19 | def get_addon_path(addon_name): 20 | for addon in bpy.context.preferences.addons: 21 | if str(addon_name) in addon.module: 22 | return addon.module 23 | 24 | def get_addon_preferences(addon_name): 25 | name = get_addon_path(addon_name) 26 | return bpy.context.preferences.addons[str(name)] 27 | 28 | #The following code is based on the MACHIN3 addon: MACHIN3tools 29 | #Check out there awesome addons here: https://machin3.io/ 30 | def draw_keymap(key_config, name, key_list, layout): 31 | drawn = [] 32 | index = 0 33 | 34 | for item in key_list: 35 | is_drawn = False 36 | 37 | if item.get("keymap"): 38 | key_map = key_config.keymaps.get(item.get("keymap")) 39 | 40 | key_map_item = None 41 | if key_map: 42 | id_name = item.get("idname") 43 | for km_item in key_map.keymap_items: 44 | if km_item.idname == id_name: 45 | properties = item.get("properties") 46 | if properties: 47 | if all([getattr(km_item.properties, name, None) == value for name, value in properties]): 48 | key_map_item = km_item 49 | break 50 | else: 51 | key_map_item = km_item 52 | break 53 | if key_map_item: 54 | if index == 0: 55 | box = layout.box() 56 | 57 | if len(key_list) == 1: 58 | label = name.title().replace("_", " ") 59 | else: 60 | if index == 0: 61 | box.label(text=name.title().replace("_", " ")) 62 | 63 | label = item.get("label") 64 | 65 | row = box.split(factor=0.15) 66 | row.label(text=label) 67 | 68 | rna_keymap_ui.draw_kmi(["ADDON", "USER", "DEFAULT"], key_config, key_map, key_map_item, row, 0) 69 | 70 | is_drawn = True 71 | index += 1 72 | 73 | drawn.append(is_drawn) 74 | return drawn 75 | 76 | def register_icons(): 77 | path = os.path.join(os.path.dirname(__file__), "..", "icons") 78 | icons = previews.new() 79 | for i in sorted(os.listdir(path)): 80 | if i.endswith(".png"): 81 | iconname = i[:-4] 82 | filepath = os.path.join(path, i) 83 | icons.load(iconname, filepath, 'IMAGE') 84 | return icons 85 | 86 | def unregister_icons(icons): 87 | previews.remove(icons) 88 | 89 | icons = None 90 | 91 | def get_icon(name): 92 | global icons 93 | 94 | if not icons: 95 | from .. import icons 96 | 97 | return icons[name].icon_id -------------------------------------------------------------------------------- /utils/register_extensions.py: -------------------------------------------------------------------------------- 1 | import bpy.types 2 | from ..classes_keymap_items import classes as classesdict 3 | from ..classes_keymap_items import keymap_items as keysdict 4 | from bpy.utils import register_class, unregister_class 5 | from ..utils.pref_utils import get_keyops_prefs 6 | import importlib 7 | 8 | #The following code is based on the MACHIN3 addon: MACHIN3tools 9 | #Check out there awesome addons here: https://machin3.io/ 10 | 11 | def register_classes(classlists): 12 | classes = [getattr(importlib.import_module("..{}".format(fr), package=__package__), imp[0]) for classlist in classlists for fr, imps in classlist for imp in imps] 13 | 14 | for cls in classes: 15 | register_class(cls) 16 | return classes 17 | 18 | def unregister_classes(classes): 19 | for cls in classes: 20 | unregister_class(cls) 21 | 22 | def get_classes(classlist): 23 | classes = [] 24 | 25 | for fr, imps in classlist: 26 | type = "OT" if "operators" in fr else "MT" 27 | classes.extend(getattr(bpy.types, f"KEYOPS_{type}_{imp[1]}", False) for imp in imps) 28 | return classes 29 | 30 | def register_keymaps(keylists): 31 | keymaps = [] 32 | 33 | for keylist in keylists: 34 | for item in keylist: 35 | keymap = item.get("keymap") 36 | space_type = item.get("space_type", "EMPTY") 37 | 38 | if keymap and (km := bpy.context.window_manager.keyconfigs.addon.keymaps.new(name=keymap, space_type=space_type)): 39 | idname = item.get("idname") 40 | type = item.get("type") 41 | value = item.get("value") 42 | repeat = item.get("repeat", False) 43 | ctrl = item.get("ctrl", False) 44 | shift = item.get("shift", False) 45 | alt = item.get("alt", False) 46 | 47 | if (kmi := km.keymap_items.new(idname, type, value, repeat=repeat, shift=shift, ctrl=ctrl, alt=alt)): 48 | properties = item.get("properties") 49 | 50 | if properties: 51 | for name, value in properties: 52 | setattr(kmi.properties, name, value) 53 | keymaps.append((km, kmi)) 54 | return keymaps 55 | 56 | def unregister_keymaps(keymaps): 57 | for km, kmi in keymaps: 58 | km.keymap_items.remove(kmi) 59 | 60 | def get_keymaps(keylist): 61 | wm = bpy.context.window_manager 62 | kc = wm.keyconfigs.addon 63 | 64 | keymaps = [] 65 | 66 | for item in keylist: 67 | keymap = item.get("keymap") 68 | 69 | if keymap: 70 | km = kc.keymaps.get(keymap) 71 | 72 | if km: 73 | idname = item.get("idname") 74 | 75 | for kmi in km.keymap_items: 76 | if kmi.idname == idname: 77 | properties = item.get("properties") 78 | 79 | if properties: 80 | if all([getattr(kmi.properties, name, None) == value for name, value in properties]): 81 | keymaps.append((km, kmi)) 82 | 83 | else: 84 | keymaps.append((km, kmi)) 85 | return keymaps 86 | 87 | def enable_extension(self, register, extension): 88 | name = extension.replace("_", " ").title() 89 | debug = False 90 | 91 | if register: 92 | classlist, keylist = eval("get_%s()" % extension) 93 | classes = register_classes(classlist) 94 | 95 | from .. import classes as default_classes 96 | 97 | for c in classes: 98 | if c not in default_classes: 99 | default_classes.append(c) 100 | 101 | keymaps = register_keymaps(keylist) 102 | 103 | from .. import keymaps as default_keymaps 104 | for k in keymaps: 105 | if k not in default_keymaps: 106 | default_keymaps.append(k) 107 | 108 | if debug: 109 | if classes: 110 | print("Registered KEYOPStools' %s" % (name)) 111 | 112 | classlist.clear() 113 | keylist.clear() 114 | 115 | else: 116 | keylist = keysdict.get(extension.upper()) 117 | 118 | if keylist: 119 | keymaps = get_keymaps(keylist) 120 | 121 | from .. import keymaps as default_keymaps 122 | for k in keymaps: 123 | if k in default_keymaps: 124 | default_keymaps.remove(k) 125 | 126 | unregister_keymaps(keymaps) 127 | 128 | classlist = classesdict[extension.upper()] 129 | classes = get_classes(classlist) 130 | 131 | from .. import classes as default_classes 132 | 133 | for c in classes: 134 | if c in default_classes: 135 | default_classes.remove(c) 136 | 137 | 138 | unregister_classes(classes) 139 | 140 | if debug: 141 | if classes: 142 | print("Unregistered KEYOPStools' %s" % (name)) 143 | 144 | def operator(operator_name): 145 | def decorator(func): 146 | def wrapper(classlists=[], keylists=[]): 147 | if getattr(get_keyops_prefs(), f"enable_{operator_name.lower().replace(' ', '_')}"): 148 | operator_class = classesdict[operator_name.upper().replace(' ', '_')] 149 | operator_key = keysdict.get(operator_name.upper().replace(' ', '_'), None) 150 | classlists.append(operator_class) 151 | if operator_key: 152 | keylists.append(operator_key) 153 | return classlists, keylists 154 | return wrapper 155 | return decorator 156 | 157 | def get_extension(): 158 | functions = [ 159 | get_auto_delete, 160 | get_toggle_retopology, 161 | get_maya_pivot, 162 | get_maya_navigation, 163 | get_double_click_select_island, 164 | get_uv_tools, 165 | get_add_objects_pie, 166 | get_add_modifier_pie, 167 | get_workspace_pie, 168 | get_cursor_pie, 169 | get_legacy_shortcuts, 170 | get_fast_merge, 171 | get_modi_key, 172 | get_uv_pies, 173 | get_utility_pie, 174 | get_cad_decimate, 175 | get_auto_lod, 176 | get_polycount_list, 177 | get_toolkit_panel, 178 | get_quick_bake_name, 179 | get_quick_export, 180 | get_atri_op, 181 | get_material_index, 182 | get_outliner_options, 183 | get_viewport_menu, 184 | ] 185 | classlists, keylists = [], [] 186 | for func in functions: 187 | classlists, keylists = func(classlists, keylists) 188 | return classlists, keylists 189 | 190 | 191 | @operator("Auto Delete") 192 | def get_auto_delete(): pass 193 | 194 | @operator("Toggle Retopology") 195 | def get_toggle_retopology(): pass 196 | 197 | @operator("Maya Pivot") 198 | def get_maya_pivot(): pass 199 | 200 | @operator("Maya Navigation") 201 | def get_maya_navigation(): pass 202 | 203 | @operator("Double Click Select Island") 204 | def get_double_click_select_island(): pass 205 | 206 | @operator("UV Tools") 207 | def get_uv_tools(): pass 208 | 209 | @operator("Add Objects Pie") 210 | def get_add_objects_pie(): pass 211 | 212 | @operator("Add Modifier Pie") 213 | def get_add_modifier_pie(): pass 214 | 215 | @operator("Workspace Pie") 216 | def get_workspace_pie(): pass 217 | 218 | @operator("Legacy Shortcuts") 219 | def get_legacy_shortcuts(): pass 220 | 221 | @operator("Cursor Pie") 222 | def get_cursor_pie(): pass 223 | 224 | @operator("Fast Merge") 225 | def get_fast_merge(): pass 226 | 227 | @operator("Modi Key") 228 | def get_modi_key(): pass 229 | 230 | @operator("UV Pies") 231 | def get_uv_pies(): pass 232 | 233 | @operator("Utility Pie") 234 | def get_utility_pie(): pass 235 | 236 | @operator("CAD Decimate") 237 | def get_cad_decimate(): pass 238 | 239 | @operator("Auto LOD") 240 | def get_auto_lod(): pass 241 | 242 | @operator("Polycount List") 243 | def get_polycount_list(): pass 244 | 245 | @operator("Toolkit Panel") 246 | def get_toolkit_panel(): pass 247 | 248 | @operator("Quick Bake Name") 249 | def get_quick_bake_name(): pass 250 | 251 | @operator("Quick Export") 252 | def get_quick_export(): pass 253 | 254 | @operator("Atri OP") 255 | def get_atri_op(): pass 256 | 257 | @operator("Material Index") 258 | def get_material_index(): pass 259 | 260 | @operator("Outliner Options") 261 | def get_outliner_options(): pass 262 | 263 | @operator("Viewport Menu") 264 | def get_viewport_menu(): pass -------------------------------------------------------------------------------- /utils/utilities.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | def force_show_obj(context, obj, select=True): 4 | """ 5 | Force show the object in the viewport, even if the collection is hidden. 6 | """ 7 | obj_collection = obj.users_collection[0] 8 | 9 | if obj_collection.hide_viewport == True or context.view_layer.layer_collection.children[obj_collection.name].hide_viewport == True: 10 | obj_collection.hide_viewport = False 11 | context.view_layer.layer_collection.children[obj_collection.name].hide_viewport = False 12 | for obj in obj_collection.objects: 13 | if not obj.hide_get(): 14 | obj.hide_set(True) 15 | 16 | obj.hide_set(False) 17 | obj.hide_viewport = False 18 | if select: 19 | obj.select_set(True) 20 | context.view_layer.objects.active = obj --------------------------------------------------------------------------------