├── .gitignore ├── __init__.py ├── colors.py ├── handlers.py ├── icons ├── append.png ├── artstation.png ├── blenderartists.png ├── cancel.png ├── cancel_grey.png ├── edge.png ├── edit_mesh.png ├── error.png ├── export.png ├── external_data.png ├── face.png ├── fist.png ├── flat.png ├── import.png ├── island.png ├── link.png ├── material.png ├── new.png ├── object.png ├── open.png ├── open_material.png ├── open_next.png ├── open_previous.png ├── open_recent.png ├── open_world.png ├── patreon.png ├── plus.png ├── recover_auto_save.png ├── revert.png ├── save.png ├── save_as.png ├── save_incremental.png ├── separator.png ├── smooth.png ├── twitter.png ├── vertex.png ├── wireframe.png ├── wireframe_xray.png ├── world.png └── youtube.png ├── items.py ├── license ├── msgbus.py ├── operators ├── TODO │ ├── TODO │ ├── cleanout_materials.py │ ├── cleanout_uvs.py │ ├── find_disabled_mods.py │ ├── lockitall.py │ └── mod_machine.py ├── align.py ├── apply.py ├── assetbrowser.py ├── clean_up.py ├── clipping_toggle.py ├── customize.py ├── extrude.py ├── filebrowser.py ├── focus.py ├── group.py ├── material_picker.py ├── mesh_cut.py ├── mirror.py ├── quadsphere.py ├── render.py ├── select.py ├── smart_drive.py ├── smart_edge.py ├── smart_face.py ├── smart_vert.py ├── smooth.py ├── surface_slide.py ├── thread.py └── unity.py ├── preferences.py ├── properties.py ├── registration.py ├── resources ├── matcaps │ ├── matcap_base.exr │ ├── matcap_shiny_red.exr │ ├── matcap_zebra_horizontal.exr │ └── matcap_zebra_vertical.exr ├── theme │ └── m3.xml └── worlds │ ├── Gdansk_shipyard_buildings.exr │ └── tomoco_studio.exr ├── ui ├── UILists.py ├── menus.py ├── operators │ ├── align.py │ ├── call_pie.py │ ├── collection.py │ ├── colorize.py │ ├── cursor.py │ ├── draw.py │ ├── grease_pencil.py │ ├── mesh.py │ ├── mode.py │ ├── open_blend.py │ ├── origin.py │ ├── overlay.py │ ├── save.py │ ├── shading.py │ ├── snapping_preset.py │ ├── tool.py │ ├── transform_preset.py │ ├── uv.py │ ├── viewport.py │ └── workspace.py ├── panels.py └── pies.py └── utils ├── append.py ├── asset.py ├── collection.py ├── developer.py ├── draw.py ├── geometry.py ├── graph.py ├── group.py ├── light.py ├── material.py ├── math.py ├── mesh.py ├── modifier.py ├── object.py ├── property.py ├── raycast.py ├── registration.py ├── scene.py ├── selection.py ├── snap.py ├── system.py ├── tools.py ├── ui.py ├── view.py ├── wm.py └── world.py /.gitignore: -------------------------------------------------------------------------------- 1 | changelog.md 2 | changelog.wiki 3 | 4 | # Byte-compiled / optimized / DLL files 5 | *.swp 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | # ========================= 65 | # Operating System Files 66 | # ========================= 67 | 68 | # OSX 69 | # ========================= 70 | 71 | .DS_Store 72 | .AppleDouble 73 | .LSOverride 74 | 75 | # Thumbnails 76 | ._* 77 | 78 | # Files that might appear in the root of a volume 79 | .DocumentRevisions-V100 80 | .fseventsd 81 | .Spotlight-V100 82 | .TemporaryItems 83 | .Trashes 84 | .VolumeIcon.icns 85 | 86 | # Directories potentially created on remote AFP share 87 | .AppleDB 88 | .AppleDesktop 89 | Network Trash Folder 90 | Temporary Items 91 | .apdisk 92 | 93 | # Windows 94 | # ========================= 95 | 96 | # Windows image file caches 97 | Thumbs.db 98 | ehthumbs.db 99 | 100 | # Folder config file 101 | Desktop.ini 102 | 103 | # Recycle Bin used on file shares 104 | $RECYCLE.BIN/ 105 | 106 | # Windows Installer files 107 | *.cab 108 | *.msi 109 | *.msm 110 | *.msp 111 | 112 | # Windows shortcuts 113 | *.lnk 114 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "MACHIN3tools", 3 | "author": "MACHIN3, TitusLVR", 4 | "version": (1, 3, 0), 5 | "blender": (3, 3, 0), 6 | "location": "", 7 | "description": "Streamlining Blender 3.3+.", 8 | "warning": "", 9 | "doc_url": "https://machin3.io/MACHIN3tools/docs", 10 | "category": "Mesh"} 11 | 12 | 13 | def reload_modules(name): 14 | ''' 15 | This makes sure all modules are reloaded from new files, when the addon is removed and a new version is installed in the same session, 16 | or when Blender's 'Reload Scripts' operator is run manually. 17 | It's important, that utils modules are reloaded first, as operators and menus import from them 18 | ''' 19 | 20 | import os 21 | import importlib 22 | 23 | 24 | # first update the classes and keys dicts, the properties, items, colors 25 | from . import registration, items, colors 26 | 27 | for module in [registration, items, colors]: 28 | print("reloading", module.__name__) 29 | importlib.reload(module) 30 | 31 | # then fetch and reload all utils modules 32 | utils_modules = sorted([name[:-3] for name in os.listdir(os.path.join(__path__[0], "utils")) if name.endswith('.py')]) 33 | 34 | for module in utils_modules: 35 | impline = "from . utils import %s" % (module) 36 | 37 | print("reloading %s" % (".".join([name] + ['utils'] + [module]))) 38 | 39 | exec(impline) 40 | importlib.reload(eval(module)) 41 | 42 | 43 | from . import handlers 44 | print("reloading", handlers.__name__) 45 | importlib.reload(handlers) 46 | 47 | # and based on that, reload the modules containing operator and menu classes 48 | modules = [] 49 | 50 | for label in registration.classes: 51 | entries = registration.classes[label] 52 | for entry in entries: 53 | path = entry[0].split('.') 54 | module = path.pop(-1) 55 | 56 | if (path, module) not in modules: 57 | modules.append((path, module)) 58 | 59 | for path, module in modules: 60 | if path: 61 | impline = "from . %s import %s" % (".".join(path), module) 62 | else: 63 | impline = "from . import %s" % (module) 64 | 65 | print("reloading %s" % (".".join([name] + path + [module]))) 66 | 67 | exec(impline) 68 | importlib.reload(eval(module)) 69 | 70 | 71 | if 'bpy' in locals(): 72 | reload_modules(bl_info['name']) 73 | 74 | import bpy 75 | from bpy.props import PointerProperty, BoolProperty, EnumProperty 76 | from . properties import M3SceneProperties, M3ObjectProperties 77 | from . utils.registration import get_core, get_tools, get_pie_menus 78 | from . utils.registration import register_classes, unregister_classes, register_keymaps, unregister_keymaps, register_icons, unregister_icons, register_msgbus, unregister_msgbus 79 | from . ui.menus import object_context_menu, mesh_context_menu, add_object_buttons, material_pick_button, outliner_group_toggles, extrude_menu, group_origin_adjustment_toggle, render_menu, render_buttons 80 | from . handlers import focus_HUD, surface_slide_HUD, update_group, update_asset, update_msgbus, screencast_HUD, increase_lights_on_render_end, decrease_lights_on_render_start, axes_HUD 81 | 82 | 83 | def register(): 84 | global classes, keymaps, icons, owner 85 | 86 | # CORE 87 | 88 | core_classes = register_classes(get_core()) 89 | 90 | 91 | # PROPERTIES 92 | 93 | bpy.types.Scene.M3 = PointerProperty(type=M3SceneProperties) 94 | bpy.types.Object.M3 = PointerProperty(type=M3ObjectProperties) 95 | 96 | bpy.types.WindowManager.M3_screen_cast = BoolProperty() 97 | bpy.types.WindowManager.M3_asset_catalogs = EnumProperty(items=[]) 98 | 99 | 100 | # TOOLS, PIE MENUS, KEYMAPS, MENUS 101 | 102 | tool_classlists, tool_keylists, tool_count = get_tools() 103 | pie_classlists, pie_keylists, pie_count = get_pie_menus() 104 | 105 | classes = register_classes(tool_classlists + pie_classlists) + core_classes 106 | keymaps = register_keymaps(tool_keylists + pie_keylists) 107 | 108 | bpy.types.VIEW3D_MT_object_context_menu.prepend(object_context_menu) 109 | bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(mesh_context_menu) 110 | 111 | bpy.types.VIEW3D_MT_edit_mesh_extrude.append(extrude_menu) 112 | bpy.types.VIEW3D_MT_mesh_add.prepend(add_object_buttons) 113 | bpy.types.VIEW3D_MT_editor_menus.append(material_pick_button) 114 | bpy.types.OUTLINER_HT_header.prepend(outliner_group_toggles) 115 | 116 | bpy.types.VIEW3D_PT_tools_object_options_transform.append(group_origin_adjustment_toggle) 117 | 118 | bpy.types.TOPBAR_MT_render.append(render_menu) 119 | bpy.types.DATA_PT_context_light.prepend(render_buttons) 120 | 121 | 122 | # ICONS 123 | 124 | icons = register_icons() 125 | 126 | 127 | # MSGBUS 128 | 129 | owner = object() 130 | register_msgbus(owner) 131 | 132 | 133 | # HANDLERS 134 | 135 | bpy.app.handlers.load_post.append(update_msgbus) 136 | 137 | bpy.app.handlers.depsgraph_update_post.append(axes_HUD) 138 | bpy.app.handlers.depsgraph_update_post.append(focus_HUD) 139 | bpy.app.handlers.depsgraph_update_post.append(surface_slide_HUD) 140 | bpy.app.handlers.depsgraph_update_post.append(update_group) 141 | bpy.app.handlers.depsgraph_update_post.append(update_asset) 142 | bpy.app.handlers.depsgraph_update_post.append(screencast_HUD) 143 | 144 | bpy.app.handlers.render_init.append(decrease_lights_on_render_start) 145 | bpy.app.handlers.render_cancel.append(increase_lights_on_render_end) 146 | bpy.app.handlers.render_complete.append(increase_lights_on_render_end) 147 | 148 | 149 | # REGISTRATION OUTPUT 150 | 151 | print(f"Registered {bl_info['name']} {'.'.join([str(i) for i in bl_info['version']])} with {tool_count} {'tool' if tool_count == 1 else 'tools'}, {pie_count} pie {'menu' if pie_count == 1 else 'menus'}") 152 | 153 | 154 | def unregister(): 155 | global classes, keymaps, icons, owner 156 | 157 | # HANDLERS 158 | 159 | bpy.app.handlers.load_post.remove(update_msgbus) 160 | 161 | from . handlers import axesHUD, focusHUD, surfaceslideHUD, screencastHUD 162 | 163 | if axesHUD and "RNA_HANDLE_REMOVED" not in str(axesHUD): 164 | bpy.types.SpaceView3D.draw_handler_remove(axesHUD, 'WINDOW') 165 | 166 | if focusHUD and "RNA_HANDLE_REMOVED" not in str(focusHUD): 167 | bpy.types.SpaceView3D.draw_handler_remove(focusHUD, 'WINDOW') 168 | 169 | if surfaceslideHUD and "RNA_HANDLE_REMOVED" not in str(surfaceslideHUD): 170 | bpy.types.SpaceView3D.draw_handler_remove(surfaceslideHUD, 'WINDOW') 171 | 172 | if screencastHUD and "RNA_HANDLE_REMOVED" not in str(screencastHUD): 173 | bpy.types.SpaceView3D.draw_handler_remove(screencastHUD, 'WINDOW') 174 | 175 | bpy.app.handlers.depsgraph_update_post.remove(axes_HUD) 176 | bpy.app.handlers.depsgraph_update_post.remove(focus_HUD) 177 | bpy.app.handlers.depsgraph_update_post.remove(surface_slide_HUD) 178 | bpy.app.handlers.depsgraph_update_post.remove(update_group) 179 | bpy.app.handlers.depsgraph_update_post.remove(update_asset) 180 | bpy.app.handlers.depsgraph_update_post.remove(screencast_HUD) 181 | 182 | bpy.app.handlers.render_init.remove(decrease_lights_on_render_start) 183 | bpy.app.handlers.render_cancel.remove(increase_lights_on_render_end) 184 | bpy.app.handlers.render_complete.remove(increase_lights_on_render_end) 185 | 186 | 187 | # MSGBUS 188 | 189 | unregister_msgbus(owner) 190 | 191 | 192 | # TOOLS, PIE MENUS, KEYMAPS, MENUS 193 | 194 | bpy.types.VIEW3D_MT_object_context_menu.remove(object_context_menu) 195 | bpy.types.VIEW3D_MT_edit_mesh_context_menu.remove(mesh_context_menu) 196 | 197 | bpy.types.VIEW3D_MT_edit_mesh_extrude.remove(extrude_menu) 198 | bpy.types.VIEW3D_MT_mesh_add.remove(add_object_buttons) 199 | bpy.types.VIEW3D_MT_editor_menus.remove(material_pick_button) 200 | bpy.types.OUTLINER_HT_header.remove(outliner_group_toggles) 201 | 202 | bpy.types.VIEW3D_PT_tools_object_options_transform.remove(group_origin_adjustment_toggle) 203 | 204 | bpy.types.TOPBAR_MT_render.remove(render_menu) 205 | bpy.types.DATA_PT_context_light.remove(render_buttons) 206 | 207 | unregister_keymaps(keymaps) 208 | unregister_classes(classes) 209 | 210 | 211 | # PROPERTIES 212 | 213 | del bpy.types.Scene.M3 214 | del bpy.types.Object.M3 215 | 216 | del bpy.types.WindowManager.M3_screen_cast 217 | del bpy.types.WindowManager.M3_asset_catalogs 218 | 219 | 220 | # ICONS 221 | 222 | unregister_icons(icons) 223 | 224 | print(f"Unregistered {bl_info['name']} {'.'.join([str(i) for i in bl_info['version']])}.") 225 | -------------------------------------------------------------------------------- /colors.py: -------------------------------------------------------------------------------- 1 | black = (0, 0, 0) 2 | white = (1, 1, 1) 3 | grey = (0.5, 0.5, 0.5) 4 | red = (1, 0.25, 0.25) 5 | green = (0.25, 1, 0.25) 6 | blue = (0.2, 0.6, 1) 7 | yellow = (1, 0.9, 0.2) 8 | normal = (0.5, 0.5, 1) 9 | orange = (1, 0.6, 0.2) 10 | light_red = (1, 0.65, 0.65) 11 | light_green = (0.75, 1, 0.75) 12 | 13 | group_colors = [(0.65, 0.2, 0.04), 14 | (0.76, 0.47, 0.01), 15 | (0.3, 0.96, 0.21), 16 | (0.04, 0.48, 0.17), 17 | (0.05, 0.54, 0.95), 18 | (0.06, 0.21, 0.94), 19 | (0.23, 0.15, 0.66), 20 | (0.65, 0.12, 0.12), 21 | (0.11, 0.24, 0.09), 22 | (0.12, 0.25, 0.24), 23 | (0.05, 0.19, 0.26), 24 | (0.11, 0.16, 0.31), 25 | (0.05, 0.06, 0.11)] 26 | -------------------------------------------------------------------------------- /icons/append.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/append.png -------------------------------------------------------------------------------- /icons/artstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/artstation.png -------------------------------------------------------------------------------- /icons/blenderartists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/blenderartists.png -------------------------------------------------------------------------------- /icons/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/cancel.png -------------------------------------------------------------------------------- /icons/cancel_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/cancel_grey.png -------------------------------------------------------------------------------- /icons/edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/edge.png -------------------------------------------------------------------------------- /icons/edit_mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/edit_mesh.png -------------------------------------------------------------------------------- /icons/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/error.png -------------------------------------------------------------------------------- /icons/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/export.png -------------------------------------------------------------------------------- /icons/external_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/external_data.png -------------------------------------------------------------------------------- /icons/face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/face.png -------------------------------------------------------------------------------- /icons/fist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/fist.png -------------------------------------------------------------------------------- /icons/flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/flat.png -------------------------------------------------------------------------------- /icons/import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/import.png -------------------------------------------------------------------------------- /icons/island.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/island.png -------------------------------------------------------------------------------- /icons/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/link.png -------------------------------------------------------------------------------- /icons/material.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/material.png -------------------------------------------------------------------------------- /icons/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/new.png -------------------------------------------------------------------------------- /icons/object.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/object.png -------------------------------------------------------------------------------- /icons/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/open.png -------------------------------------------------------------------------------- /icons/open_material.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/open_material.png -------------------------------------------------------------------------------- /icons/open_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/open_next.png -------------------------------------------------------------------------------- /icons/open_previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/open_previous.png -------------------------------------------------------------------------------- /icons/open_recent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/open_recent.png -------------------------------------------------------------------------------- /icons/open_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/open_world.png -------------------------------------------------------------------------------- /icons/patreon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/patreon.png -------------------------------------------------------------------------------- /icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/plus.png -------------------------------------------------------------------------------- /icons/recover_auto_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/recover_auto_save.png -------------------------------------------------------------------------------- /icons/revert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/revert.png -------------------------------------------------------------------------------- /icons/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/save.png -------------------------------------------------------------------------------- /icons/save_as.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/save_as.png -------------------------------------------------------------------------------- /icons/save_incremental.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/save_incremental.png -------------------------------------------------------------------------------- /icons/separator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/separator.png -------------------------------------------------------------------------------- /icons/smooth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/smooth.png -------------------------------------------------------------------------------- /icons/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/twitter.png -------------------------------------------------------------------------------- /icons/vertex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/vertex.png -------------------------------------------------------------------------------- /icons/wireframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/wireframe.png -------------------------------------------------------------------------------- /icons/wireframe_xray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/wireframe_xray.png -------------------------------------------------------------------------------- /icons/world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/world.png -------------------------------------------------------------------------------- /icons/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/icons/youtube.png -------------------------------------------------------------------------------- /items.py: -------------------------------------------------------------------------------- 1 | from mathutils import Vector 2 | 3 | axis_items = [('X', 'X', ''), 4 | ('Y', 'Y', ''), 5 | ('Z', 'Z', '')] 6 | 7 | axis_index_mapping = {'X': 0, 8 | 'Y': 1, 9 | 'Z': 2} 10 | 11 | axis_vector_mappings = {'X': Vector((1, 0, 0)), 12 | 'Y': Vector((0, 1, 0)), 13 | 'Z': Vector((0, 0, 1))} 14 | 15 | uv_axis_items = [('U', 'U', ''), 16 | ('V', 'V', '')] 17 | 18 | 19 | # keys 20 | 21 | ctrl = ['LEFT_CTRL', 'RIGHT_CTRL'] 22 | alt = ['LEFT_ALT', 'RIGHT_ALT'] 23 | shift = ['LEFT_SHIFT', 'RIGHT_SHIFT'] 24 | 25 | 26 | # PREFERENCES 27 | 28 | preferences_tabs = [("GENERAL", "General", ""), 29 | ("KEYMAPS", "Keymaps", ""), 30 | ("ABOUT", "About", "")] 31 | 32 | matcap_background_type_items = [("THEME", "Theme", ""), 33 | ("WORLD", "World", ""), 34 | ("VIEWPORT", "Viewport", "")] 35 | 36 | 37 | # OPERATORS 38 | 39 | 40 | smartvert_mode_items = [("MERGE", "Merge", ""), 41 | ("CONNECT", "Connect Paths", "")] 42 | 43 | 44 | smartvert_merge_type_items = [("LAST", "Last", ""), 45 | ("CENTER", "Center", ""), 46 | ("PATHS", "Paths", "")] 47 | 48 | smartvert_path_type_items = [("TOPO", "Topo", ""), 49 | ("LENGTH", "Length", "")] 50 | 51 | 52 | smartedge_sharp_mode_items = [('SHARPEN', 'Sharpen', ''), 53 | ('CHAMFER', 'Chamfer', ''), 54 | ('KOREAN', 'Korean Bevel', '')] 55 | 56 | smartedge_select_mode_items = [('BOUNDS', 'Bounds/Region', ''), 57 | ('ADJACENT', 'Adjacent', '')] 58 | 59 | focus_method_items = [('VIEW_SELECTED', 'View Selected', ''), 60 | ('LOCAL_VIEW', 'Local View', '')] 61 | 62 | focus_levels_items = [('SINGLE', 'Single', ''), 63 | ('MULTIPLE', 'Multiple', '')] 64 | 65 | align_mode_items = [('VIEW', 'View', ''), 66 | ('AXES', 'Axes', '')] 67 | 68 | align_type_items = [('MIN', 'Min', ''), 69 | ('MAX', 'Max', ''), 70 | ('AVERAGE', 'Average', ''), 71 | ('ZERO', 'Zero', ''), 72 | ('CURSOR', 'Cursor', '')] 73 | 74 | align_direction_items = [('LEFT', 'Left', ''), 75 | ('RIGHT', 'Right', ''), 76 | ('TOP', 'Top', ''), 77 | ('BOTTOM', 'Bottom', ''), 78 | ('HORIZONTAL', 'Horizontal', ''), 79 | ('VERTICAL', 'Vertical', '')] 80 | 81 | align_space_items = [('LOCAL', 'Local', ''), 82 | ('WORLD', 'World', ''), 83 | ('CURSOR', 'Cursor', '')] 84 | 85 | obj_align_mode_items = [('ORIGIN', 'Origin', ''), 86 | ('CURSOR', 'Cursor', ''), 87 | ('ACTIVE', 'Active', ''), 88 | ('FLOOR', 'Floor', '')] 89 | 90 | cleanup_select_items = [("NON-MANIFOLD", "Non-Manifold", ""), 91 | ("NON-PLANAR", "Non-Planar", ""), 92 | ("TRIS", "Tris", ""), 93 | ("NGONS", "Ngons", "")] 94 | 95 | driver_limit_items = [('NONE', 'None', ''), 96 | ('START', 'Start', ''), 97 | ('END', 'End', ''), 98 | ('BOTH', 'Both', '')] 99 | 100 | driver_transform_items = [('LOCATION', 'Location', ''), 101 | ('ROTATION_EULER', 'Rotation', '')] 102 | 103 | driver_space_items = [('AUTO', 'Auto', 'Choose Local or World space based on whether driver object is parented'), 104 | ('LOCAL_SPACE', 'Local', ''), 105 | ('WORLD_SPACE', 'World', '')] 106 | 107 | axis_mapping_dict = {'X': 0, 'Y': 1, 'Z': 2} 108 | 109 | uv_align_axis_mapping_dict = {'U': 0, 'V': 1} 110 | 111 | bridge_interpolation_items = [('LINEAR', 'Linear', ''), 112 | ('PATH', 'Path', ''), 113 | ('SURFACE', 'Surface', '')] 114 | 115 | view_axis_items = [("FRONT", "Front", ""), 116 | ("BACK", "Back", ""), 117 | ("LEFT", "Left", ""), 118 | ("RIGHT", "Right", ""), 119 | ("TOP", "Top", ""), 120 | ("BOTTOM", "Bottom", "")] 121 | 122 | group_location_items = [('AVERAGE', 'Average', ''), 123 | ('ACTIVE', 'Active', ''), 124 | ('CURSOR', 'Cursor', ''), 125 | ('WORLD', 'World', '')] 126 | 127 | cursor_spin_angle_preset_items = [('None', 'None', ''), 128 | ('30', '30', ''), 129 | ('45', '45', ''), 130 | ('60', '60', ''), 131 | ('90', '90', ''), 132 | ('135', '135', ''), 133 | ('180', '180', '')] 134 | 135 | 136 | create_assembly_asset_empty_location_items = [('AVG', 'Average', ''), 137 | ('AVGFLOOR', 'Average Floor', ''), 138 | ('WORLDORIGIN', 'World Origin', '')] 139 | 140 | create_assembly_asset_empty_collection_items = [('SCENECOL', 'Add to Scene Collection', ''), 141 | ('OBJCOLS', 'Add to Object Collections', '')] 142 | 143 | 144 | shade_mode_items = [('SMOOTH', 'Smooth', ''), 145 | ('FLAT', 'Flat', '')] 146 | 147 | 148 | 149 | # PIES 150 | 151 | eevee_preset_items = [('NONE', 'None', ''), 152 | ('LOW', 'Low', 'Use Scene Lights, Ambient Occlusion and Screen Space Reflections'), 153 | ('HIGH', 'High', 'Use Bloom and Screen Space Refractions'), 154 | ('ULTRA', 'Ultra', 'Use Scene World and Volumetrics.\nCreate Principled Volume node if necessary')] 155 | 156 | render_engine_items = [('BLENDER_EEVEE', 'Eevee', ''), 157 | ('CYCLES', 'Cycles', '')] 158 | 159 | 160 | shading_light_items = [('STUDIO', 'Studio', ''), 161 | ('MATCAP', 'Matcap', ''), 162 | ('FLAT', 'Flat', '')] 163 | 164 | 165 | cycles_device_items = [('CPU', 'CPU', ''), 166 | ('GPU', 'GPU', '')] 167 | 168 | 169 | bc_orientation_items = [('LOCAL', 'Local', ''), 170 | ('NEAREST', 'Nearest', ''), 171 | ('LONGEST', 'Longest', '')] 172 | 173 | 174 | tool_name_mapping_dict = {'BC': 'BoxCutter', 175 | 'Hops': 'HardOps', 176 | 'builtin.select_box': 'Select Box', 177 | 'machin3.tool_hyper_cursor': 'Hyper Cursor', 178 | 'machin3.tool_hyper_cursor_simple': 'Simple Hyper Cursor'} 179 | 180 | 181 | # MODIFIERS 182 | 183 | mirror_props = ['type', 184 | 'merge_threshold', 185 | 'mirror_object', 186 | 'mirror_offset_u', 187 | 'mirror_offset_v', 188 | 'offset_u', 189 | 'offset_v', 190 | 'show_expanded', 191 | 'show_in_editmode', 192 | 'show_on_cage', 193 | 'show_render', 194 | 'show_viewport', 195 | 'use_axis', 196 | 'use_bisect_axis', 197 | 'use_bisect_flip_axis', 198 | 'use_clip', 199 | 'use_mirror_merge', 200 | 'use_mirror_u', 201 | 'use_mirror_v', 202 | 'use_mirror_vertex_groups'] 203 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016-2021 MACHIN3, machin3.io, support@machin3.io 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 | -------------------------------------------------------------------------------- /msgbus.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . utils import registration as r 3 | from . utils.group import update_group_name 4 | 5 | 6 | def group_name_change(): 7 | active = bpy.context.active_object 8 | 9 | if active and active.M3.is_group_empty and r.get_prefs().group_auto_name: 10 | update_group_name(active) 11 | 12 | 13 | def group_color_change(): 14 | active = bpy.context.active_object 15 | 16 | if active and active.M3.is_group_empty: 17 | objects = [obj for obj in active.children if obj.M3.is_group_object and not obj.M3.is_group_empty] 18 | 19 | for obj in objects: 20 | obj.color = active.color 21 | -------------------------------------------------------------------------------- /operators/TODO/TODO: -------------------------------------------------------------------------------- 1 | create batch tool, that can: 2 | * cleanout_materials 3 | * cleanout uvs 4 | * find disabled mods 5 | * hide/unhide meshes 6 | * lock/unlock objects 7 | * batch mod stuff 8 | -------------------------------------------------------------------------------- /operators/TODO/cleanout_materials.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import BoolProperty 3 | from .. import M3utils as m3 4 | 5 | 6 | class CleanoutMaterials(bpy.types.Operator): 7 | bl_idname = "machin3.cleanout_materials" 8 | bl_label = "MACHIN3: Cleanout Materials" 9 | bl_options = {'REGISTER', 'UNDO'} 10 | 11 | changedrawtype = BoolProperty(name="Change Drawtype", default=True) 12 | changeshading = BoolProperty(name="Change to Material Shading", default=True) 13 | 14 | def draw(self, context): 15 | layout = self.layout 16 | 17 | column = layout.column() 18 | 19 | column.prop(self, "changedrawtype") 20 | column.prop(self, "changeshading") 21 | 22 | def execute(self, context): 23 | 24 | self.cleanout_materials() 25 | 26 | return {'FINISHED'} 27 | 28 | def cleanout_materials(self): 29 | global changedrawtype 30 | didunhide = False 31 | 32 | sel = m3.selected_objects() 33 | if len(sel) == 0: 34 | print("\nNothing selected, going over all the scene's Mesh objects.") 35 | scope = bpy.data.objects 36 | else: 37 | scope = sel 38 | 39 | for obj in scope: 40 | if obj.type == "MESH": 41 | slots = obj.material_slots 42 | if len(slots) > 0: 43 | m3.make_active(obj, silent=True) 44 | print("%s:" % (obj.name)) 45 | 46 | # Make drawtype textured, so they will display materials or lack of them properly in the viewport, once you are in material shading 47 | if self.changedrawtype: 48 | self.change_drawtype(obj) 49 | 50 | if obj.hide: 51 | print(" > is hidden - unhiding.") 52 | obj.hide = False 53 | didunhide = True 54 | 55 | for slot in slots: 56 | try: 57 | bpy.ops.object.material_slot_remove() 58 | print("\t removed slot: '%s'" % (slot.name)) 59 | except: 60 | print(m3.red("Something went wrong removing '%s's' slot '%s'.") % (obj.name, slot.name)) 61 | 62 | if didunhide: 63 | print(" > hiding '%s' again." % (obj.name)) 64 | obj.hide = True 65 | 66 | didunhide = False 67 | else: 68 | print("'%s' has no material slots." % (obj.name)) 69 | 70 | # this will still leave the materials in the scene however 71 | # the following will remove the materials from the scene 72 | # it is of utmost importance to only run this, once the magterials are no longer assgigned to objects, 73 | # otherwise selecting an object/openin the material panel will crash blender 74 | 75 | if len(sel) == 0: 76 | print(20 * "-") 77 | print("\nPurging materials from scene.") 78 | 79 | # Clear the user count of a datablock so its not saved, on reload the data will be removed 80 | # This function is for advanced use only, misuse can crash blender since the user count is used to prevent data being removed when it is used. 81 | 82 | for material in bpy.data.materials: 83 | print("Removing material: '%s'." % (material.name)) 84 | material.user_clear() 85 | bpy.data.materials.remove(material) 86 | 87 | if self.changeshading: 88 | bpy.context.space_data.viewport_shade = "MATERIAL" 89 | 90 | def change_drawtype(self, object): 91 | try: 92 | drawtype = bpy.context.object.draw_type 93 | if drawtype == "SOLID": 94 | print('\t changing drawtype to "TEXTURED"') 95 | bpy.context.object.draw_type = "TEXTURED" 96 | except: 97 | print(m3.red("Something went wrong changing '%s's' drawtype to 'TEXTURED'.") % (object.name)) 98 | -------------------------------------------------------------------------------- /operators/TODO/cleanout_uvs.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .. import M3utils as m3 3 | 4 | 5 | class CleanoutUVs(bpy.types.Operator): 6 | bl_idname = "machin3.cleanout_uvs" 7 | bl_label = "MACHIN3: Cleanout UVs" 8 | bl_options = {'REGISTER', 'UNDO'} 9 | 10 | def execute(self, context): 11 | selection = m3.selected_objects() 12 | 13 | for obj in selection: 14 | if obj.type == "MESH": 15 | m3.make_active(obj) 16 | 17 | uvs = obj.data.uv_textures 18 | 19 | while uvs: 20 | print(" > removing UVs: %s" % (uvs[0].name)) 21 | uvs.remove(uvs[0]) 22 | 23 | return {'FINISHED'} 24 | -------------------------------------------------------------------------------- /operators/TODO/find_disabled_mods.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .. import M3utils as m3 3 | 4 | 5 | class FindDisabledMods(bpy.types.Operator): 6 | bl_idname = "machin3.find_disabled_mods" 7 | bl_label = "MACHIN3: Find Disabled Mods Modes" 8 | bl_options = {'REGISTER', 'UNDO'} 9 | 10 | def execute(self, context): 11 | m3.clear() 12 | 13 | selection = m3.selected_objects() 14 | bpy.ops.ed.undo_push(message="testing") 15 | 16 | disabled = [] 17 | for obj in selection: 18 | m3.make_active(obj) 19 | 20 | for mod in obj.modifiers: 21 | modname = mod.name 22 | try: 23 | bpy.ops.object.modifier_apply(apply_as='DATA', modifier=mod.name) 24 | except: 25 | if obj not in disabled: 26 | disabled.append(obj.name) 27 | print(obj.name) 28 | print(" » " + modname, "is DISABLED!\n") 29 | 30 | # undo applying all those mods, as it's only done to trigger exceptions and find disabled mods 31 | bpy.ops.ed.undo() 32 | 33 | # for some reason we need to do it through the obj name, we cant directly references the objects(probably as their ids have changed when doing the undo) 34 | for obj in selection: 35 | if obj.name in disabled: 36 | print(obj.name) 37 | else: 38 | o = bpy.data.objects[obj.name] 39 | o.select = False 40 | 41 | return {'FINISHED'} 42 | -------------------------------------------------------------------------------- /operators/TODO/lockitall.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import BoolProperty 3 | from .. import M3utils as m3 4 | 5 | 6 | lockchoice = [("LOCK", "Lock", ""), 7 | ("UNLOCK", "Unlock", "")] 8 | 9 | 10 | class LockItAll(bpy.types.Operator): 11 | bl_idname = "machin3.lock_it_all" 12 | bl_label = "MACHIN3: Lock It All" 13 | bl_options = {'REGISTER', 'UNDO'} 14 | 15 | location = BoolProperty(name="Location", default=True) 16 | rotation = BoolProperty(name="Rotation", default=True) 17 | scale = BoolProperty(name="Scale", default=True) 18 | 19 | lockorunlock = bpy.props.EnumProperty(name="Lock or Unlock", items=lockchoice, default="LOCK") 20 | 21 | def draw(self, context): 22 | layout = self.layout 23 | 24 | column = layout.column() 25 | 26 | row = column.row() 27 | row.prop(self, "lockorunlock", expand=True) 28 | 29 | column.separator() 30 | 31 | column.prop(self, "location", toggle=True) 32 | column.prop(self, "rotation", toggle=True) 33 | column.prop(self, "scale", toggle=True) 34 | 35 | def execute(self, context): 36 | selection = m3.selected_objects() 37 | 38 | if self.lockorunlock == "LOCK": 39 | lock = True 40 | else: 41 | lock = False 42 | 43 | for obj in selection: 44 | if self.location: 45 | for i in range(len(obj.lock_location)): 46 | obj.lock_location[i] = lock 47 | print("Locked '%s's location[%d]." % (obj.name, i)) 48 | if any([self.rotation, self.scale]): 49 | print("---") 50 | if self.rotation: 51 | for i in range(len(obj.lock_rotation)): 52 | obj.lock_rotation[i] = lock 53 | print("Locked '%s's rotation[%d]." % (obj.name, i)) 54 | if self.scale: 55 | print("---") 56 | if self.scale: 57 | for i in range(len(obj.lock_scale)): 58 | obj.lock_scale[i] = lock 59 | print("Locked '%s's scale[%d]." % (obj.name, i)) 60 | if any([self.location, self.rotation, self.scale]): 61 | print("_" * 40) 62 | 63 | return {'FINISHED'} 64 | -------------------------------------------------------------------------------- /operators/TODO/mod_machine.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import BoolProperty, EnumProperty 3 | from .. import M3utils as m3 4 | 5 | 6 | applyorshow = [("APPLY", "Apply", ""), 7 | ("REMOVE", "Remove", ""), 8 | ("SHOW", "Show", ""), 9 | ("HIDE", "Hide", "")] 10 | 11 | 12 | class ModMachine(bpy.types.Operator): 13 | bl_idname = "machin3.mod_machine" 14 | bl_label = "MACHIN3: Mod Machine" 15 | bl_options = {'REGISTER', 'UNDO'} 16 | 17 | applyorshow = EnumProperty(name="Apply or Show", items=applyorshow, default="APPLY") 18 | 19 | applyall = BoolProperty(name="All (Overwrite)", default=False) 20 | applymirror = BoolProperty(name="Mirror", default=False) 21 | applybevel = BoolProperty(name="Bevel", default=False) 22 | applydisplace = BoolProperty(name="Displace", default=False) 23 | applyboolean = BoolProperty(name="Boolean", default=False) 24 | applydatatransfer = BoolProperty(name="Data Transfer", default=False) 25 | applyshrinkwrap = BoolProperty(name="Shrink Wrap", default=False) 26 | 27 | def draw(self, context): 28 | layout = self.layout 29 | 30 | column = layout.column() 31 | 32 | row = column.row() 33 | row.prop(self, "applyorshow", expand=True) 34 | 35 | column.prop(self, "applyall", toggle=True) 36 | column.separator() 37 | 38 | row = column.row(align=True) 39 | row.prop(self, "applymirror", toggle=True) 40 | row.prop(self, "applybevel", toggle=True) 41 | row.prop(self, "applydisplace", toggle=True) 42 | 43 | row = column.row(align=True) 44 | row.prop(self, "applyboolean", toggle=True) 45 | row.prop(self, "applydatatransfer", toggle=True) 46 | row.prop(self, "applyshrinkwrap", toggle=True) 47 | 48 | def execute(self, context): 49 | bpy.ops.ed.undo_push(message="MACHIN3: Pre-Mod-Machine-State") # without pushing the state, there you might loose the selections, when redoing the op and switching the type of mod 50 | 51 | self.selection = m3.selected_objects() 52 | active = m3.get_active() 53 | 54 | applylist = [] 55 | 56 | if self.applymirror: 57 | applylist.append("MIRROR") 58 | if self.applybevel: 59 | applylist.append("BEVEL") 60 | if self.applydisplace: 61 | applylist.append("DISPLACE") 62 | if self.applyboolean: 63 | applylist.append("BOOLEAN") 64 | if self.applydatatransfer: 65 | applylist.append("DATA_TRANSFER") 66 | if self.applyshrinkwrap: 67 | applylist.append("SHRINKWRAP") 68 | 69 | for obj in self.selection: 70 | if obj.type == "MESH": 71 | m3.make_active(obj) 72 | 73 | for mod in obj.modifiers: 74 | if self.applyall: 75 | try: 76 | if self.applyorshow == "APPLY": 77 | bpy.ops.object.modifier_apply(apply_as='DATA', modifier=mod.name) 78 | print("Applied '%s's '%s' modifier" % (obj.name, mod.name)) 79 | elif self.applyorshow == "REMOVE": 80 | bpy.ops.object.modifier_remove(modifier=mod.name) 81 | print("Removed '%s's '%s' modifier" % (obj.name, mod.name)) 82 | 83 | elif self.applyorshow == "SHOW": 84 | mod.show_viewport = True 85 | print("'%s's '%s' modifier is now visible" % (obj.name, mod.name)) 86 | elif self.applyorshow == "HIDE": 87 | mod.show_viewport = False 88 | print("'%s's '%s' modifier is now hidden" % (obj.name, mod.name)) 89 | except: 90 | # print(m3.red("Failed to apply modifier") % (obj.name, mod.name)) 91 | pass # seeing some occasioanl utf8 errors, when accessing obj.name or mod.name (even if it isnt writting to the terminal!), likely related to this bug in blender: https://developer.blender.org/T48042 92 | else: 93 | if mod.type in applylist: 94 | try: 95 | if self.applyorshow == "APPLY": 96 | bpy.ops.object.modifier_apply(apply_as='DATA', modifier=mod.name) 97 | print("Applied '%s's '%s' modifier" % (obj.name, mod.name)) 98 | elif self.applyorshow == "REMOVE": 99 | bpy.ops.object.modifier_remove(modifier=mod.name) 100 | print("Removed '%s's '%s' modifier" % (obj.name, mod.name)) 101 | elif self.applyorshow == "SHOW": 102 | mod.show_viewport = True 103 | print("'%s's '%s' modifier is now visible" % (obj.name, mod.name)) 104 | elif self.applyorshow == "HIDE": 105 | mod.show_viewport = False 106 | print("'%s's '%s' modifier is now hidden" % (obj.name, mod.name)) 107 | except: 108 | # print(m3.red("Failed to apply modifier") % (obj.name, mod.name)) 109 | pass 110 | m3.make_active(active) 111 | 112 | return {'FINISHED'} 113 | -------------------------------------------------------------------------------- /operators/apply.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import BoolProperty 3 | from mathutils import Vector, Quaternion 4 | from .. utils.registration import get_addon 5 | from .. utils.math import flatten_matrix, get_loc_matrix, get_rot_matrix, get_sca_matrix 6 | 7 | 8 | # TODO: updare child parent inverse mx? 9 | # TODO: is this tool still relevant? does blender do this properly on its own now? incl the bevel mod? 10 | # TODO: what about the displace mod? 11 | 12 | 13 | class Apply(bpy.types.Operator): 14 | bl_idname = "machin3.apply_transformations" 15 | bl_label = "MACHIN3: Apply Transformations" 16 | bl_description = "Apply Transformations while keeping the bevel width as well as the child transformations unchanged." 17 | bl_options = {'REGISTER', 'UNDO'} 18 | 19 | scale: BoolProperty(name="Scale", default=True) 20 | rotation: BoolProperty(name="Rotation", default=False) 21 | 22 | def draw(self, context): 23 | layout = self.layout 24 | 25 | column = layout.column() 26 | 27 | row = column.row(align=True) 28 | row.prop(self, "scale", toggle=True) 29 | row.prop(self, "rotation", toggle=True) 30 | 31 | @classmethod 32 | def poll(cls, context): 33 | return context.mode == 'OBJECT' and context.selected_objects 34 | 35 | def execute(self, context): 36 | if any([self.rotation, self.scale]): 37 | decalmachine, _, _, _ = get_addon("DECALmachine") 38 | 39 | # only apply the scale to objects, that arent't parented themselves 40 | apply_objs = [obj for obj in context.selected_objects if not obj.parent] 41 | 42 | for obj in apply_objs: 43 | 44 | # fetch children and their current world mx 45 | children = [(child, child.matrix_world) for child in obj.children] 46 | 47 | mx = obj.matrix_world 48 | loc, rot, sca = mx.decompose() 49 | 50 | # apply the current transformations on the mesh level 51 | if self.rotation and self.scale: 52 | meshmx = get_rot_matrix(rot) @ get_sca_matrix(sca) 53 | elif self.rotation: 54 | meshmx = get_rot_matrix(rot) 55 | elif self.scale: 56 | meshmx = get_sca_matrix(sca) 57 | 58 | obj.data.transform(meshmx) 59 | 60 | # zero out the transformations on the object level 61 | if self.rotation and self.scale: 62 | applymx = get_loc_matrix(loc) @ get_rot_matrix(Quaternion()) @ get_sca_matrix(Vector.Fill(3, 1)) 63 | elif self.rotation: 64 | applymx = get_loc_matrix(loc) @ get_rot_matrix(Quaternion()) @ get_sca_matrix(sca) 65 | elif self.scale: 66 | applymx = get_loc_matrix(loc) @ get_rot_matrix(rot) @ get_sca_matrix(Vector.Fill(3, 1)) 67 | 68 | obj.matrix_world = applymx 69 | 70 | 71 | # adjust the bevel width values accordingly 72 | if self.scale: 73 | mods = [mod for mod in obj.modifiers if mod.type == "BEVEL"] 74 | 75 | for mod in mods: 76 | vwidth = get_sca_matrix(sca) @ Vector((0, 0, mod.width)) 77 | mod.width = vwidth[2] 78 | 79 | 80 | # reset the children to their original state again 81 | for obj, mxw in children: 82 | obj.matrix_world = mxw 83 | 84 | # update decal backups's backup matrices as well, we can just reuse the bmesh mx here 85 | if decalmachine and obj.DM.decalbackup: 86 | backup = obj.DM.decalbackup 87 | backup.DM.backupmx = flatten_matrix(meshmx @ backup.DM.backupmx) 88 | 89 | return {'FINISHED'} 90 | -------------------------------------------------------------------------------- /operators/clipping_toggle.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import FloatProperty, BoolProperty, EnumProperty 3 | from .. utils.property import step_enum 4 | from .. utils.registration import get_prefs 5 | from .. colors import white 6 | 7 | 8 | state_items = [("MIN", "Minimum", ""), 9 | ("MED", "Medium", ""), 10 | ("MAX", "Maximum", "")] 11 | 12 | 13 | class ClippingToggle(bpy.types.Operator): 14 | bl_idname = "machin3.clipping_toggle" 15 | bl_label = "MACHIN3: Clipping Toggle" 16 | bl_options = {'REGISTER', 'UNDO'} 17 | 18 | def update_clip_start_maximum(self, context): 19 | if self.avoid_item_update: 20 | self.avoid_item_update = False 21 | return 22 | 23 | bpy.context.space_data.clip_start = self.maximum 24 | self.avoid_state_update = True 25 | self.state = "MAX" 26 | self.avoid_execute = True 27 | 28 | def update_clip_start_medium(self, context): 29 | if self.avoid_item_update: 30 | self.avoid_item_update = False 31 | return 32 | 33 | bpy.context.space_data.clip_start = self.medium 34 | self.avoid_state_update = True 35 | self.state = "MED" 36 | self.avoid_execute = True 37 | 38 | def update_clip_start_minimum(self, context): 39 | if self.avoid_item_update: 40 | self.avoid_item_update = False 41 | return 42 | 43 | bpy.context.space_data.clip_start = self.minimum 44 | self.avoid_state_update = True 45 | self.state = "MIN" 46 | self.avoid_execute = True 47 | 48 | def update_state(self, context): 49 | if self.avoid_execute: 50 | self.avoid_execute = False 51 | return 52 | 53 | if self.avoid_state_update: 54 | self.avoid_state_update = False 55 | return 56 | 57 | view = bpy.context.space_data 58 | 59 | if self.state == "MIN": 60 | view.clip_start = self.minimum 61 | 62 | elif self.state == "MED": 63 | view.clip_start = self.medium 64 | 65 | elif self.state == "MAX": 66 | view.clip_start = self.maximum 67 | 68 | self.avoid_execute = True 69 | 70 | def update_reset(self, context): 71 | if not self.reset: 72 | return 73 | 74 | self.avoid_item_update = True 75 | self.maximum = 1 76 | self.avoid_item_update = True 77 | self.medium = 0.1 78 | self.avoid_item_update = True 79 | self.minimum = 0.001 80 | 81 | view = bpy.context.space_data 82 | 83 | if self.state == "MIN": 84 | view.clip_start = self.minimum 85 | 86 | elif self.state == "MED": 87 | view.clip_start = self.medium 88 | 89 | elif self.state == "MAX": 90 | view.clip_start = self.maximum 91 | 92 | self.reset = False 93 | self.avoid_execute = True 94 | 95 | maximum: FloatProperty(name="Maximum", default=1, min=0, precision=2, step=10, update=update_clip_start_maximum) 96 | medium: FloatProperty(name="Medium", default=0.01, min=0, precision=3, step=1, update=update_clip_start_medium) 97 | minimum: FloatProperty(name="Minimum", default=0.001, min=0, precision=5, step=0.001, update=update_clip_start_minimum) 98 | 99 | state: EnumProperty(name="Current State", items=state_items, default="MED", update=update_state) 100 | reset: BoolProperty(default=False, update=update_reset) 101 | 102 | avoid_execute: BoolProperty(default=False) 103 | avoid_state_update: BoolProperty(default=False) 104 | avoid_item_update: BoolProperty(default=False) 105 | 106 | def draw(self, context): 107 | layout = self.layout 108 | col = layout.column() 109 | 110 | view = bpy.context.space_data 111 | 112 | row = col.row(align=True) 113 | row.prop(self, "state", expand=True) 114 | row.prop(self, "reset", text="", icon="BLANK1", emboss=False) 115 | 116 | row = col.row(align=True) 117 | row.prop(self, "minimum", text="") 118 | row.prop(self, "medium", text="") 119 | row.prop(self, "maximum", text="") 120 | row.prop(self, "reset", text="", icon="LOOP_BACK") 121 | 122 | row = col.row(align=True) 123 | row.label(text="Current") 124 | row.label(text=str(round(view.clip_start, 6))) 125 | 126 | def execute(self, context): 127 | if self.avoid_execute: 128 | self.avoid_execute = False 129 | 130 | else: 131 | self.avoid_execute = True 132 | self.state = step_enum(self.state, state_items, 1, loop=True) 133 | 134 | view = bpy.context.space_data 135 | 136 | scale = context.preferences.view.ui_scale * get_prefs().HUD_scale 137 | center_offset = 100 * scale 138 | 139 | coords = ((context.region.width / 2) - (center_offset if self.state == 'MIN' else 0 if self.state == 'MED' else - center_offset), 100) 140 | 141 | if self.state == "MIN": 142 | view.clip_start = self.minimum 143 | bpy.ops.machin3.draw_label(text=f'Minimum: {round(self.minimum, 6)}', coords=coords, color=white, time=get_prefs().HUD_fade_clipping_toggle) 144 | 145 | elif self.state == "MED": 146 | view.clip_start = self.medium 147 | bpy.ops.machin3.draw_label(text=f'Medium: {round(self.medium, 6)}', coords=coords, color=white, time=get_prefs().HUD_fade_clipping_toggle) 148 | 149 | elif self.state == "MAX": 150 | view.clip_start = self.maximum 151 | bpy.ops.machin3.draw_label(text=f'Maximum: {round(self.maximum, 6)}', coords=coords, color=white, time=get_prefs().HUD_fade_clipping_toggle) 152 | 153 | return {'FINISHED'} 154 | -------------------------------------------------------------------------------- /operators/extrude.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from bpy.props import FloatProperty, IntProperty, EnumProperty, BoolProperty 4 | from math import radians 5 | from mathutils.geometry import intersect_point_line 6 | from .. utils.math import average_locations, average_normals 7 | from .. utils.draw import draw_point, draw_vector 8 | from .. items import axis_vector_mappings, axis_items, cursor_spin_angle_preset_items 9 | from .. colors import yellow, blue, red 10 | 11 | 12 | class PunchItALittle(bpy.types.Operator): 13 | bl_idname = "machin3.punch_it_a_little" 14 | bl_label = "MACHIN3: Punch It (a little)" 15 | bl_description = "Manifold Extruding that works, somewhat" 16 | bl_options = {'REGISTER', 'UNDO'} 17 | 18 | amount: FloatProperty(name="Amount", description="Extrusion Depth", default=0.1, min=0, precision=4, step=0.1) 19 | 20 | def execute(self, context): 21 | if self.amount: 22 | active = context.active_object 23 | 24 | bpy.ops.mesh.duplicate() 25 | 26 | bm = bmesh.from_edit_mesh(active.data) 27 | bm.normal_update() 28 | 29 | original_verts = [v for v in bm.verts if v.select] 30 | original_faces = [f for f in bm.faces if f.select] 31 | # print(original_faces) 32 | 33 | geo = bmesh.ops.extrude_face_region(bm, geom=original_faces, use_normal_flip=False) 34 | extruded_verts = [v for v in geo['geom'] if isinstance(v, bmesh.types.BMVert)] 35 | 36 | # move out the original faces 37 | normal = original_faces[0].normal 38 | 39 | for v in original_verts: 40 | v.co += normal * self.amount 41 | 42 | # select te extruded verts then flush the selection to select the entire extruded part 43 | for v in extruded_verts: 44 | v.select_set(True) 45 | 46 | bm.select_flush(True) 47 | 48 | # from the selectino get all the faces of the extuded part (incl. the side faces) 49 | all_faces = [f for f in bm.faces if f.select] 50 | 51 | # then recalc the normals in preparatino for the boolean 52 | bmesh.ops.recalc_face_normals(bm, faces=all_faces) 53 | 54 | bmesh.update_edit_mesh(active.data) 55 | 56 | bpy.ops.mesh.intersect_boolean(use_self=True) 57 | return {'FINISHED'} 58 | 59 | 60 | class CursorSpin(bpy.types.Operator): 61 | bl_idname = "machin3.cursor_spin" 62 | bl_label = "MACHIN3: Cursor Spin" 63 | bl_description = "Cursor Spin" 64 | bl_options = {'REGISTER', 'UNDO'} 65 | 66 | def update_angle(self, context): 67 | if self.angle_preset != 'None' and self.angle != int(self.angle_preset): 68 | self.avoid_update = True 69 | self.angle_preset = 'None' 70 | 71 | def update_angle_preset(self, context): 72 | if self.avoid_update: 73 | self.avoid_update = False 74 | return 75 | 76 | if self.angle_preset != 'None': 77 | self.angle = int(self.angle_preset) 78 | 79 | def update_offset_reset(self, context): 80 | if self.avoid_update: 81 | self.avoid_update = False 82 | return 83 | 84 | self.offset = 0 85 | 86 | self.avoid_update = True 87 | self.offset_reset = False 88 | 89 | angle: FloatProperty(name='Angle', default=45, min=0, update=update_angle) 90 | angle_preset: EnumProperty(name='Angle Preset', items=cursor_spin_angle_preset_items, default='45', update=update_angle_preset) 91 | angle_invert: BoolProperty(name='Invert', default=False) 92 | 93 | steps: IntProperty(name='Steps', default=4, min=1) 94 | adaptive: BoolProperty(name="Adaptive Steps", default=True) 95 | adaptive_factor: FloatProperty(name="Adaptive Factor", default=0.1, step=0.05) 96 | 97 | axis: EnumProperty(name='Axis', description='Cursor Axis', items=axis_items, default='Y') 98 | 99 | offset: FloatProperty(name='Offset', default=0, step=0.1) 100 | offset_reset: BoolProperty(name='Offset Reset', default=False, update=update_offset_reset) 101 | 102 | # hidden 103 | avoid_update: BoolProperty() 104 | 105 | def draw(self, context): 106 | layout = self.layout 107 | 108 | column = layout.column(align=True) 109 | 110 | row = column.row(align=True) 111 | r = row.row(align=True) 112 | r.scale_y = 1.2 113 | r.prop(self, 'angle_preset', expand=True) 114 | row.prop(self, 'angle_invert', toggle=True) 115 | 116 | row = column.split(factor=0.4, align=True) 117 | row.prop(self, 'angle') 118 | 119 | r = row.split(factor=0.6, align=True) 120 | 121 | if self.adaptive: 122 | r.prop(self, 'adaptive_factor', text='Factor') 123 | else: 124 | r.prop(self, 'steps') 125 | 126 | r.prop(self, 'adaptive', text='Adaptive', toggle=True) 127 | 128 | column.separator() 129 | 130 | row = column.split(factor=0.5, align=True) 131 | 132 | r = row.row(align=True) 133 | r.prop(self, 'axis', expand=True) 134 | 135 | r = row.row(align=True) 136 | r.prop(self, 'offset') 137 | r.prop(self, 'offset_reset', text='', icon='LOOP_BACK', toggle=True) 138 | 139 | def execute(self, context): 140 | debug = False 141 | # debug = True 142 | 143 | if self.angle: 144 | cmx = context.scene.cursor.matrix 145 | mx = context.active_object.matrix_world 146 | 147 | # get angle 148 | angle = radians(-self.angle if self.angle_invert else self.angle) 149 | 150 | # get axis 151 | axis = cmx.to_quaternion() @ axis_vector_mappings[self.axis] 152 | 153 | # get center from cursor 154 | center = cmx.to_translation() 155 | 156 | # but also attempt to get center from cursor + offset however, depends on whether there is a valid selection 157 | # blender's spin up doesn't poll for a selection, it just runs without doing anything, so do the same here 158 | bm = bmesh.from_edit_mesh(context.active_object.data) 159 | verts = [v for v in bm.verts if v.select] 160 | 161 | if verts: 162 | center_sel = mx @ average_locations([v.co for v in verts]) 163 | if debug: 164 | draw_point(center_sel, modal=False) 165 | 166 | i = intersect_point_line(center_sel, center, center + axis) 167 | 168 | if i: 169 | closest_on_axis = i[0] 170 | if debug: 171 | draw_point(closest_on_axis, color=yellow, modal=False) 172 | 173 | offset_vector = (closest_on_axis - center_sel).normalized() 174 | if debug: 175 | draw_vector(offset_vector, closest_on_axis, color=yellow, modal=False) 176 | 177 | center = center + offset_vector * self.offset 178 | if debug: 179 | draw_point(center, color=blue, modal=False) 180 | 181 | # based on the avg face normal and the offset vector, set the angle to posiitve or negative accordingly 182 | faces = [f for f in bm.faces if f.select] 183 | 184 | avg_normal = average_normals([f.normal for f in faces]) 185 | 186 | # cross = avg_normal.cross(offset_vector) 187 | cross = offset_vector.cross(avg_normal) 188 | if debug: 189 | draw_vector(cross, origin=center, color=red, modal=False) 190 | 191 | # compare the cross vector to the axis 192 | dot = cross.dot(axis) 193 | 194 | # the two vectors should point in the same direction, if they aren't then the extrusion will go inwards, even with a positive angle 195 | # so invert the angle in that case 196 | if dot < 0: 197 | angle = -angle 198 | 199 | if debug: 200 | context.area.tag_redraw() 201 | 202 | # get steps adaptively 203 | if self.adaptive: 204 | steps = max([int(self.angle * self.adaptive_factor), 1]) 205 | 206 | # get them directly 207 | else: 208 | steps = self.steps 209 | 210 | bpy.ops.mesh.spin(angle=angle, steps=steps, center=center, axis=axis) 211 | return {'FINISHED'} 212 | -------------------------------------------------------------------------------- /operators/filebrowser.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import StringProperty, BoolProperty 3 | import os 4 | from .. utils.system import abspath, open_folder 5 | from .. utils.property import step_list 6 | 7 | 8 | class Open(bpy.types.Operator): 9 | bl_idname = "machin3.filebrowser_open" 10 | bl_label = "MACHIN3: Open in System's filebrowser" 11 | bl_description = "Open the current location in the System's own filebrowser\nALT: Open .blend file" 12 | 13 | path: StringProperty(name="Path") 14 | blend_file: BoolProperty(name="Open .blend file") 15 | 16 | @classmethod 17 | def poll(cls, context): 18 | return context.area.type == 'FILE_BROWSER' 19 | 20 | def execute(self, context): 21 | params = context.space_data.params 22 | directory = abspath(params.directory.decode()) 23 | 24 | if self.blend_file: 25 | active_file = context.active_file 26 | 27 | if active_file.asset_data: 28 | bpy.ops.asset.open_containing_blend_file() 29 | 30 | else: 31 | path = os.path.join(directory, active_file.relative_path) 32 | bpy.ops.machin3.open_library_blend(blendpath=path) 33 | 34 | else: 35 | open_folder(directory) 36 | 37 | return {'FINISHED'} 38 | 39 | 40 | class Toggle(bpy.types.Operator): 41 | bl_idname = "machin3.filebrowser_toggle" 42 | bl_label = "MACHIN3: Toggle Filebrowser" 43 | bl_description = "" 44 | 45 | type: StringProperty() 46 | 47 | @classmethod 48 | def poll(cls, context): 49 | return context.area.type == 'FILE_BROWSER' 50 | 51 | def execute(self, context): 52 | if self.type == 'DISPLAY_TYPE': 53 | if context.area.ui_type == 'FILES': 54 | if context.space_data.params.display_type == 'LIST_VERTICAL': 55 | context.space_data.params.display_type = 'THUMBNAIL' 56 | 57 | else: 58 | context.space_data.params.display_type = 'LIST_VERTICAL' 59 | 60 | elif context.area.ui_type == 'ASSETS': 61 | if context.space_data.params.asset_library_ref == 'Library': 62 | context.space_data.params.asset_library_ref = 'LOCAL' 63 | elif context.space_data.params.asset_library_ref == 'LOCAL': 64 | context.space_data.params.asset_library_ref = 'Library' 65 | 66 | elif self.type == 'SORT': 67 | if context.area.ui_type == 'FILES': 68 | if context.space_data.params.sort_method == 'FILE_SORT_ALPHA': 69 | context.space_data.params.sort_method = 'FILE_SORT_TIME' 70 | 71 | else: 72 | context.space_data.params.sort_method = 'FILE_SORT_ALPHA' 73 | 74 | elif context.area.ui_type == 'ASSETS': 75 | import_types = ['LINK', 'APPEND', 'APPEND_REUSE'] 76 | 77 | bpy.context.space_data.params.import_type = step_list(context.space_data.params.import_type, import_types, 1) 78 | 79 | elif self.type == 'HIDDEN': 80 | if context.area.ui_type == 'FILES': 81 | context.space_data.params.show_hidden = not context.space_data.params.show_hidden 82 | 83 | return {'FINISHED'} 84 | 85 | 86 | class CycleThumbs(bpy.types.Operator): 87 | bl_idname = "machin3.filebrowser_cycle_thumbnail_size" 88 | bl_label = "MACHIN3: Cycle Thumbnail Size" 89 | bl_description = "" 90 | bl_options = {'REGISTER', 'UNDO'} 91 | 92 | reverse: BoolProperty(name="Reverse Cycle Diretion") 93 | 94 | @classmethod 95 | def poll(cls, context): 96 | return context.area.type == 'FILE_BROWSER' and context.space_data.params.display_type == 'THUMBNAIL' 97 | 98 | def execute(self, context): 99 | sizes = ['TINY', 'SMALL', 'NORMAL', 'LARGE'] 100 | size = bpy.context.space_data.params.display_size 101 | bpy.context.space_data.params.display_size = step_list(size, sizes, -1 if self.reverse else 1, loop=True) 102 | 103 | return {'FINISHED'} 104 | -------------------------------------------------------------------------------- /operators/material_picker.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import BoolProperty 3 | from bl_ui.space_statusbar import STATUSBAR_HT_header as statusbar 4 | from mathutils import Vector 5 | from .. utils.raycast import cast_obj_ray_from_mouse, cast_bvh_ray_from_mouse 6 | from .. utils.draw import draw_label 7 | from .. utils.registration import get_prefs 8 | from .. items import alt 9 | 10 | 11 | def draw_material_pick_status(self, context): 12 | layout = self.layout 13 | 14 | row = layout.row(align=True) 15 | row.label(text=f"Material Picker") 16 | 17 | row.label(text="", icon='MOUSE_LMB') 18 | row.label(text="Pick Material") 19 | 20 | row.label(text="", icon='MOUSE_MMB') 21 | row.label(text="Viewport") 22 | 23 | row.label(text="", icon='MOUSE_RMB') 24 | row.label(text="Cancel") 25 | 26 | row.separator(factor=10) 27 | 28 | row.label(text="", icon='EVENT_ALT') 29 | row.label(text="Assign Material") 30 | 31 | 32 | class MaterialPicker(bpy.types.Operator): 33 | bl_idname = "machin3.material_picker" 34 | bl_label = "MACHIN3: Material Picker" 35 | bl_description = "Pick a Material from the 3D View\nALT: Assign it to the Selection too" 36 | bl_options = {'REGISTER', 'UNDO'} 37 | 38 | @classmethod 39 | def poll(cls, context): 40 | return context.area.type == 'VIEW_3D' 41 | 42 | def draw_HUD(self, args): 43 | context, event = args 44 | 45 | draw_label(context, title="Assign" if self.is_assign else "Pick", coords=self.mousepos + Vector((20, 10)), center=False) 46 | 47 | def modal(self, context, event): 48 | context.area.tag_redraw() 49 | 50 | self.mousepos = Vector((event.mouse_region_x, event.mouse_region_y)) 51 | self.is_assign = event.alt 52 | 53 | if event.type == 'LEFTMOUSE': 54 | if context.mode == 'OBJECT': 55 | hitobj, hitobj_eval, _, _, hitindex, _ = cast_obj_ray_from_mouse(self.mousepos, depsgraph=self.dg, debug=False) 56 | 57 | elif context.mode == 'EDIT_MESH': 58 | hitobj, _, _, hitindex, _, _ = cast_bvh_ray_from_mouse(self.mousepos, candidates=[obj for obj in context.visible_objects if obj.mode == 'EDIT']) 59 | 60 | if hitobj: 61 | if context.mode == 'OBJECT': 62 | matindex = hitobj_eval.data.polygons[hitindex].material_index 63 | elif context.mode == 'EDIT_MESH': 64 | matindex = hitobj.data.polygons[hitindex].material_index 65 | 66 | context.view_layer.objects.active = hitobj 67 | hitobj.active_material_index = matindex 68 | 69 | if hitobj.material_slots and hitobj.material_slots[matindex].material: 70 | mat = hitobj.material_slots[matindex].material 71 | 72 | if self.is_assign: 73 | sel = [obj for obj in context.selected_objects if obj != hitobj and obj.data] 74 | 75 | for obj in sel: 76 | if not obj.material_slots: 77 | obj.data.materials.append(mat) 78 | 79 | else: 80 | obj.material_slots[obj.active_material_index].material = mat 81 | 82 | 83 | bpy.ops.machin3.draw_label(text=mat.name, coords=self.mousepos, alpha=1, time=get_prefs().HUD_fade_material_picker) 84 | 85 | else: 86 | bpy.ops.machin3.draw_label(text="Empty", coords=self.mousepos, color=(0.5, 0.5, 0.5), alpha=1, time=get_prefs().HUD_fade_material_picker + 0.2) 87 | 88 | else: 89 | bpy.ops.machin3.draw_label(text="None", coords=self.mousepos, color=(1, 0, 0), alpha=1, time=get_prefs().HUD_fade_material_picker + 0.2) 90 | 91 | self.finish(context) 92 | return {'FINISHED'} 93 | 94 | elif event.type == 'MIDDLEMOUSE': 95 | return {'PASS_THROUGH'} 96 | 97 | elif event.type in ['RIGHTMOUSE', 'ESC']: 98 | self.finish(context) 99 | return {'CANCELLED'} 100 | 101 | return {'RUNNING_MODAL'} 102 | 103 | def finish(self, context): 104 | bpy.types.SpaceView3D.draw_handler_remove(self.HUD, 'WINDOW') 105 | 106 | context.window.cursor_set("DEFAULT") 107 | 108 | statusbar.draw = self.bar_orig 109 | 110 | if context.visible_objects: 111 | context.visible_objects[0].select_set(context.visible_objects[0].select_get()) 112 | 113 | def invoke(self, context, event): 114 | 115 | # init 116 | self.is_assign = False 117 | 118 | context.window.cursor_set("EYEDROPPER") 119 | self.mousepos = Vector((event.mouse_region_x, event.mouse_region_y)) 120 | 121 | self.dg = context.evaluated_depsgraph_get() 122 | 123 | self.bar_orig = statusbar.draw 124 | statusbar.draw = draw_material_pick_status 125 | 126 | if context.visible_objects: 127 | context.visible_objects[0].select_set(context.visible_objects[0].select_get()) 128 | 129 | # handlers 130 | args = (context, event) 131 | self.HUD = bpy.types.SpaceView3D.draw_handler_add(self.draw_HUD, (args, ), 'WINDOW', 'POST_PIXEL') 132 | 133 | context.window_manager.modal_handler_add(self) 134 | return {'RUNNING_MODAL'} 135 | -------------------------------------------------------------------------------- /operators/mesh_cut.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import BoolProperty 3 | import bmesh 4 | from math import degrees 5 | from .. utils.mesh import unhide_deselect, join 6 | from .. utils.object import flatten 7 | from .. utils.ui import popup_message 8 | 9 | 10 | class MeshCut(bpy.types.Operator): 11 | bl_idname = "machin3.mesh_cut" 12 | bl_label = "MACHIN3: Mesh Cut" 13 | bl_description = "Cut a Mesh Object, using another Object.\nALT: Flatten Target Object's Modifier Stack\nSHIFT: Mark Seams" 14 | bl_options = {'REGISTER', 'UNDO'} 15 | 16 | flatten_target: BoolProperty(name="Flatte Target's Modifier Stack", default=False) 17 | mark_seams: BoolProperty(name="Mark Seams", default=False) 18 | 19 | @classmethod 20 | def poll(cls, context): 21 | if context.mode == 'OBJECT': 22 | return context.active_object and context.active_object.type == 'MESH' 23 | 24 | def draw(self, context): 25 | layout = self.layout 26 | column = layout.column() 27 | 28 | row = column.row(align=True) 29 | row.prop(self, 'flatten_target', text="Flatten Target", toggle=True) 30 | row.prop(self, 'mark_seams', toggle=True) 31 | 32 | def invoke(self, context, event): 33 | self.flatten_target = event.alt 34 | self.mark_seams = event.shift 35 | return self.execute(context) 36 | 37 | def execute(self, context): 38 | target = context.active_object 39 | cutters = [obj for obj in context.selected_objects if obj != target] 40 | 41 | if cutters: 42 | cutter = cutters[0] 43 | 44 | # unhide both 45 | unhide_deselect(target.data) 46 | unhide_deselect(cutter.data) 47 | 48 | # get depsgraph 49 | dg = context.evaluated_depsgraph_get() 50 | 51 | # always flatten the cutter 52 | flatten(cutter, dg) 53 | 54 | # optionally flatten the target 55 | if self.flatten_target: 56 | flatten(target, dg) 57 | 58 | # clear cutter materials 59 | cutter.data.materials.clear() 60 | 61 | # join target and cutter 62 | join(target, [cutter], select=[1]) 63 | 64 | # knife intersect, and separete the pieces, which will allow us to find and mark the new edges 65 | bpy.ops.object.mode_set(mode='EDIT') 66 | bpy.ops.mesh.intersect(separate_mode='ALL') 67 | bpy.ops.object.mode_set(mode='OBJECT') 68 | 69 | # remove cutter 70 | bm = bmesh.new() 71 | bm.from_mesh(target.data) 72 | bm.normal_update() 73 | bm.verts.ensure_lookup_table() 74 | 75 | select_layer = bm.faces.layers.int.get('Machin3FaceSelect') 76 | 77 | meshcut_layer = bm.edges.layers.int.get('Machin3EdgeMeshCut') 78 | 79 | if not meshcut_layer: 80 | meshcut_layer = bm.edges.layers.int.new('Machin3EdgeMeshCut') 81 | 82 | cutter_faces = [f for f in bm.faces if f[select_layer] > 0] 83 | bmesh.ops.delete(bm, geom=cutter_faces, context='FACES') 84 | 85 | # mark mesh cut edges 86 | non_manifold = [e for e in bm.edges if not e.is_manifold] 87 | 88 | # mark them and collect the verts as well 89 | verts = set() 90 | 91 | for e in non_manifold: 92 | e[meshcut_layer] = 1 93 | 94 | if self.mark_seams: 95 | e.seam = True 96 | 97 | verts.update(e.verts) 98 | 99 | # merge the open, non-manifold seam 100 | bmesh.ops.remove_doubles(bm, verts=list({v for e in non_manifold for v in e.verts}), dist=0.0001) 101 | 102 | # fetch the still valid verts and collect the straight 2-edged ones 103 | straight_edged = [] 104 | 105 | for v in verts: 106 | if v.is_valid and len(v.link_edges) == 2: 107 | e1 = v.link_edges[0] 108 | e2 = v.link_edges[1] 109 | 110 | vector1 = e1.other_vert(v).co - v.co 111 | vector2 = e2.other_vert(v).co - v.co 112 | 113 | angle = degrees(vector1.angle(vector2)) 114 | 115 | if 179 <= angle <= 181: 116 | straight_edged.append(v) 117 | 118 | # dissolve them 119 | bmesh.ops.dissolve_verts(bm, verts=straight_edged) 120 | 121 | # remove face selection layer, it's no longer needed now, keep the meshcut layer though 122 | bm.faces.layers.int.remove(select_layer) 123 | 124 | bm.to_mesh(target.data) 125 | bm.free() 126 | 127 | return {'FINISHED'} 128 | else: 129 | popup_message("Select one object first, then select the object to be cut last!", title="Illegal Sellection") 130 | return {'CANCELLED'} 131 | -------------------------------------------------------------------------------- /operators/quadsphere.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import IntProperty, BoolProperty 3 | from bpy_extras.object_utils import AddObjectHelper 4 | from math import radians 5 | 6 | 7 | class QuadSphere(bpy.types.Operator): 8 | bl_idname = "machin3.quadsphere" 9 | bl_label = "MACHIN3: Quadsphere" 10 | bl_description = "Creates a Quadsphere" 11 | bl_options = {'REGISTER', 'UNDO'} 12 | 13 | subdivisions: IntProperty(name='Subdivisions', default=4, min=1, max=8) 14 | shade_smooth: BoolProperty(name="Shade Smooth", default=True) 15 | 16 | align_rotation: BoolProperty(name="Align Rotation", default=True) 17 | 18 | def draw(self, context): 19 | layout = self.layout 20 | 21 | column = layout.column() 22 | 23 | row = column.row(align=True) 24 | row.prop(self, "subdivisions") 25 | row.prop(self, "shade_smooth", toggle=True) 26 | row.prop(self, "align_rotation", toggle=True) 27 | 28 | @classmethod 29 | def poll(cls, context): 30 | return context.mode in ['OBJECT', 'EDIT_MESH'] 31 | 32 | def execute(self, context): 33 | bpy.ops.mesh.primitive_cube_add(align='CURSOR' if self.align_rotation else 'WORLD') 34 | 35 | mode = bpy.context.mode 36 | 37 | if mode == 'OBJECT': 38 | bpy.ops.object.mode_set(mode='EDIT') 39 | 40 | if self.shade_smooth: 41 | bpy.ops.mesh.faces_shade_smooth() 42 | 43 | for sub in range(self.subdivisions): 44 | bpy.ops.mesh.subdivide(number_cuts=1, smoothness=1) 45 | bpy.ops.transform.tosphere(value=1) 46 | 47 | if mode == 'OBJECT': 48 | bpy.ops.object.mode_set(mode='OBJECT') 49 | 50 | quadsphere = context.active_object 51 | quadsphere.data.auto_smooth_angle = radians(60) 52 | 53 | # clear uvs 54 | mesh = quadsphere.data 55 | 56 | while mesh.uv_layers: 57 | mesh.uv_layers.remove(mesh.uv_layers[0]) 58 | 59 | return {'FINISHED'} 60 | -------------------------------------------------------------------------------- /operators/select.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import EnumProperty, BoolProperty 3 | from mathutils import Vector 4 | # from .. items import axis_items 5 | 6 | 7 | axis_items = [("0", "X", ""), 8 | ("1", "Y", ""), 9 | ("2", "Z", "")] 10 | 11 | # TODO: use the axis_items in items.py? 12 | 13 | 14 | class SelectCenterObjects(bpy.types.Operator): 15 | bl_idname = "machin3.select_center_objects" 16 | bl_label = "MACHIN3: Select Center Objects" 17 | bl_description = "Selects Objects in the Center, objects, that have verts on both sides of the X, Y or Z axis." 18 | bl_options = {'REGISTER', 'UNDO'} 19 | 20 | axis: EnumProperty(name="Axis", items=axis_items, default="0") 21 | 22 | def draw(self, context): 23 | layout = self.layout 24 | 25 | column = layout.column() 26 | 27 | row = column.row() 28 | row.prop(self, "axis", expand=True) 29 | 30 | @classmethod 31 | def poll(cls, context): 32 | return context.mode == 'OBJECT' 33 | 34 | def execute(self, context): 35 | visible = [obj for obj in context.visible_objects if obj.type == "MESH"] 36 | 37 | if visible: 38 | 39 | bpy.ops.object.select_all(action='DESELECT') 40 | 41 | for obj in visible: 42 | mx = obj.matrix_world 43 | 44 | coords = [(mx @ Vector(co))[int(self.axis)] for co in obj.bound_box] 45 | 46 | if min(coords) < 0 and max(coords) > 0: 47 | obj.select_set(True) 48 | 49 | return {'FINISHED'} 50 | 51 | 52 | class SelectWireObjects(bpy.types.Operator): 53 | bl_idname = "machin3.select_wire_objects" 54 | bl_label = "MACHIN3: Select Wire Objects" 55 | bl_description = "Select Objects set to WIRE display type\nALT: Hide Objects\nCLTR: Include Empties" 56 | bl_options = {'REGISTER', 'UNDO'} 57 | 58 | @classmethod 59 | def poll(cls, context): 60 | if context.mode == 'OBJECT': 61 | return [obj for obj in context.visible_objects if obj.display_type in ['WIRE', 'BOUNDS'] or obj.type == 'EMPTY'] 62 | 63 | def invoke(self, context, event): 64 | bpy.ops.object.select_all(action='DESELECT') 65 | 66 | # fix objects without proper display_type 67 | for obj in context.visible_objects: 68 | if obj.display_type == '': 69 | obj.display_type = 'WIRE' 70 | 71 | 72 | # get all wire objects, optionally including empties 73 | if event.ctrl: 74 | objects = [obj for obj in context.visible_objects if obj.display_type in ['WIRE', 'BOUNDS'] or obj.type == 'EMPTY'] 75 | else: 76 | objects = [obj for obj in context.visible_objects if obj.display_type in ['WIRE', 'BOUNDS']] 77 | 78 | for obj in objects: 79 | if event.alt: 80 | obj.hide_set(True) 81 | else: 82 | obj.select_set(True) 83 | 84 | return {'FINISHED'} 85 | 86 | 87 | class SelectHierarchy(bpy.types.Operator): 88 | bl_idname = "machin3.select_hierarchy" 89 | bl_label = "MACHIN3: Select Hierarchy" 90 | bl_description = "Select Hierarchy Down" 91 | bl_options = {'REGISTER', 'UNDO'} 92 | 93 | include_parent: BoolProperty(name="Include Parent", description="Include the Parent in the Selection", default=False) 94 | 95 | recursive: BoolProperty(name="Select Recursive Children", description="Select Children Recursively", default=True) 96 | unhide: BoolProperty(name="Select Hidden Children", description="Unhide and Select Hidden Children", default=False) 97 | 98 | def draw(self, context): 99 | layout = self.layout 100 | 101 | column = layout.column(align=True) 102 | 103 | row = column.row(align=True) 104 | row.prop(self, 'include_parent', toggle=True) 105 | 106 | row = column.row(align=True) 107 | row.prop(self, 'recursive', text="Recursive", toggle=True) 108 | row.prop(self, 'unhide', text="Unhide", toggle=True) 109 | 110 | def invoke(self, context, event): 111 | # self.include_parent = event.shift 112 | 113 | # self.recursive = event.ctrl 114 | # self.unhide = event.alt 115 | return self.execute(context) 116 | 117 | def execute(self, context): 118 | view = context.space_data 119 | sel = context.selected_objects 120 | 121 | for obj in sel: 122 | if self.recursive: 123 | children = [(c, c.visible_get()) for c in obj.children_recursive if c.name in context.view_layer.objects] 124 | else: 125 | children = [(c, c.visible_get()) for c in obj.children if c.name in context.view_layer.objects] 126 | 127 | for c, vis in children: 128 | # unhide 129 | if self.unhide and not vis: 130 | if view.local_view and not c.local_view_get(view): 131 | c.local_view_set(view, True) 132 | 133 | c.hide_set(False) 134 | 135 | # select 136 | c.select_set(True) 137 | 138 | # set parent selection state 139 | obj.select_set(self.include_parent) 140 | 141 | 142 | return {'FINISHED'} 143 | -------------------------------------------------------------------------------- /operators/smart_drive.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import StringProperty 3 | from math import radians, degrees 4 | from .. items import axis_mapping_dict 5 | 6 | 7 | class SmartDrive(bpy.types.Operator): 8 | bl_idname = 'machin3.smart_drive' 9 | bl_label = 'MACHIN3: Smart Drive' 10 | bl_description = 'Drive one Object using another' 11 | bl_options = {'REGISTER', 'UNDO'} 12 | 13 | @classmethod 14 | def poll(cls, context): 15 | m3 = context.scene.M3 16 | 17 | driver_start = m3.driver_start 18 | driver_end = m3.driver_end 19 | 20 | driven_start = m3.driven_start 21 | driven_end = m3.driven_end 22 | 23 | if driver_start != driver_end and driven_start != driven_end and context.active_object: 24 | driven = context.active_object 25 | sel = [obj for obj in context.selected_objects if obj != driven] 26 | 27 | return len(sel) == 1 28 | 29 | def execute(self, context): 30 | m3 = context.scene.M3 31 | 32 | driver_start = m3.driver_start 33 | driver_end = m3.driver_end 34 | driver_transform = m3.driver_transform 35 | driver_axis = m3.driver_axis 36 | driver_space = m3.driver_space 37 | 38 | driven_start = m3.driven_start 39 | driven_end = m3.driven_end 40 | driven_transform = m3.driven_transform 41 | driven_axis = m3.driven_axis 42 | driven_limit = m3.driven_limit 43 | 44 | path = driven_transform.lower() 45 | index = axis_mapping_dict[driven_axis] 46 | 47 | driven = context.active_object 48 | driver = [obj for obj in context.selected_objects if obj != driven][0] 49 | 50 | # remove any existing driver at this path 51 | if driven.animation_data: 52 | for d in driven.animation_data.drivers: 53 | if d.data_path == path and d.array_index == index: 54 | driven.animation_data.drivers.remove(d) 55 | 56 | # add new SCRIPTED_EXPRESSION driver 57 | fcurve = driven.driver_add(path, index) 58 | 59 | drv = fcurve.driver 60 | drv.type = 'SCRIPTED' 61 | 62 | # add control variable, controlled by driver object 63 | var = drv.variables.new() 64 | var.name = path[:3] 65 | var.type = 'TRANSFORMS' 66 | 67 | target = var.targets[0] 68 | target.id = driver 69 | target.transform_type = '%s_%s' % (driver_transform[:3], driver_axis) 70 | 71 | if driver_space == 'AUTO': 72 | target.transform_space = 'LOCAL_SPACE' if driver.parent else 'WORLD_SPACE' 73 | else: 74 | target.transform_space = driver_space 75 | 76 | if driver_transform == 'ROTATION_EULER': 77 | driver_start = radians(driver_start) 78 | driver_end = radians(driver_end) 79 | 80 | if driven_transform == 'ROTATION_EULER': 81 | driven_start = radians(driven_start) 82 | driven_end = radians(driven_end) 83 | 84 | # set expression 85 | drv.expression = self.get_expression(driver_start, driver_end, driven_start, driven_end, driven_limit, var.name) 86 | 87 | return {'FINISHED'} 88 | 89 | def get_expression(self, driver_start, driver_end, driven_start, driven_end, driven_limit, varname): 90 | ''' 91 | create expression based on driver and driven values and limit 92 | ''' 93 | 94 | range_driver = abs(driver_end - driver_start) 95 | range_driven = abs(driven_end - driven_start) 96 | 97 | # driver end value is bigger than end value! 98 | if driver_end > driver_start: 99 | expr = f'((({varname} - {driver_start}) / {range_driver}) * {range_driven})' 100 | 101 | # driven start value is bigger than end value! 102 | else: 103 | expr = f'((({driver_start} - {varname}) / {range_driver}) * {range_driven})' 104 | 105 | # driven end value is bigger than end value! 106 | if driven_end > driven_start: 107 | expr = f'{expr} + {driven_start}' 108 | 109 | # limit the start value 110 | if driven_limit == 'START': 111 | expr = f'max({driven_start}, {expr})' 112 | 113 | # limit the end value 114 | elif driven_limit == 'END': 115 | expr = f'min({driven_end}, {expr})' 116 | 117 | # limit start and end 118 | elif driven_limit == 'BOTH': 119 | expr = f'max({driven_start}, min({driven_end}, {expr}))' 120 | 121 | 122 | # driven start value is bigger than end value! 123 | else: 124 | expr = f'{driven_start} - {expr}' 125 | 126 | # limit the start value 127 | if driven_limit == 'START': 128 | expr = f'min({driven_start}, {expr})' 129 | 130 | # limit the end value 131 | elif driven_limit == 'END': 132 | expr = f'max({driven_end}, {expr})' 133 | 134 | # limit start and end 135 | elif driven_limit == 'BOTH': 136 | expr = f'min({driven_start}, max({driven_end}, {expr}))' 137 | 138 | return expr 139 | 140 | 141 | class SwitchValues(bpy.types.Operator): 142 | bl_idname = 'machin3.switch_driver_values' 143 | bl_label = 'MACHIN3: Switch Driver Values' 144 | bl_options = {'REGISTER', 'UNDO'} 145 | 146 | mode: StringProperty(name='Mode', default='DRIVER') 147 | 148 | @classmethod 149 | def description(cls, context, properties): 150 | return 'Switch Start and End %s Values' % (properties.mode.capitalize()) 151 | 152 | def execute(self, context): 153 | m3 = context.scene.M3 154 | 155 | if self.mode == 'DRIVER': 156 | m3.driver_start, m3.driver_end = m3.driver_end, m3.driver_start 157 | 158 | elif self.mode == 'DRIVEN': 159 | m3.driven_start, m3.driven_end = m3.driven_end, m3.driven_start 160 | 161 | return {'FINISHED'} 162 | 163 | 164 | class SetValue(bpy.types.Operator): 165 | bl_idname = 'machin3.set_driver_value' 166 | bl_label = 'MACHIN3: set_driver_value' 167 | bl_options = {'REGISTER', 'UNDO'} 168 | 169 | mode: StringProperty(name='Mode', default='DRIVER') 170 | value: StringProperty(name='Value', default='START') 171 | 172 | 173 | @classmethod 174 | def description(cls, context, properties): 175 | return 'Set %s %s Value' % (properties.mode.capitalize(), properties.value.capitalize()) 176 | 177 | @classmethod 178 | def poll(cls, context): 179 | return context.active_object and context.selected_objects == [context.active_object] 180 | 181 | def execute(self, context): 182 | active = context.active_object 183 | 184 | value = self.value.lower() 185 | mode = self.mode.lower() 186 | 187 | m3 = context.scene.M3 188 | 189 | axis = getattr(m3, f'{mode}_axis') 190 | transform = getattr(m3, f'{mode}_transform').lower() 191 | 192 | val = getattr(active, transform)[axis_mapping_dict[axis]] 193 | 194 | if transform == 'rotation_euler': 195 | val = degrees(val) 196 | 197 | setattr(m3, f'{mode}_{value}', val) 198 | 199 | return {'FINISHED'} 200 | -------------------------------------------------------------------------------- /operators/smooth.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import BoolProperty, StringProperty 3 | import bmesh 4 | from math import degrees, radians 5 | 6 | 7 | class ToggleSmooth(bpy.types.Operator): 8 | bl_idname = "machin3.toggle_smooth" 9 | bl_label = "MACHIN3: Toggle Smooth" 10 | bl_description = "Toggle Smothing for Korean Bevel and SubD Objects" 11 | bl_options = {'REGISTER', 'UNDO'} 12 | 13 | toggle_subd_overlays: BoolProperty(name="Toggle Overlays", default=False) 14 | toggle_korean_bevel_overlays: BoolProperty(name="Toggle Overlays", default=True) 15 | 16 | mode: StringProperty(name="Smooth Mode", default='SUBD') 17 | 18 | @classmethod 19 | def poll(cls, context): 20 | if context.mode == 'EDIT_MESH': 21 | bm = bmesh.from_edit_mesh(context.active_object.data) 22 | return bm.faces 23 | elif context.mode == 'OBJECT': 24 | return [obj for obj in context.selected_objects if obj.type == 'MESH' and obj.data.polygons] 25 | 26 | def draw(self, context): 27 | layout = self.layout 28 | 29 | column = layout.column() 30 | row = column.split(factor=0.3) 31 | 32 | if self.mode == 'SUBD': 33 | row.label(text='SubD') 34 | row.prop(self, 'toggle_subd_overlays', toggle=True) 35 | else: 36 | row.label(text='Korean Bevel') 37 | row.prop(self, 'toggle_korean_bevel_overlays', toggle=True) 38 | 39 | def execute(self, context): 40 | if context.mode == 'EDIT_MESH': 41 | active = context.active_object 42 | subds = [mod for mod in active.modifiers if mod.type == 'SUBSURF'] 43 | 44 | if subds: 45 | # print("SubD Workflow") 46 | self.toggle_subd(context, active, subds) 47 | 48 | else: 49 | # print("Korean Bevel Workflow") 50 | self.toggle_korean_bevel(context, active) 51 | 52 | else: 53 | objects = [obj for obj in context.selected_objects if obj.type == 'MESH'] 54 | 55 | toggle_type = 'TOGGLE' 56 | 57 | for obj in objects: 58 | subds = [mod for mod in obj.modifiers if mod.type == 'SUBSURF'] 59 | 60 | if subds: 61 | # print("SubD Workflow") 62 | toggle_type = self.toggle_subd(context, obj, subds, toggle_type=toggle_type) 63 | 64 | else: 65 | # print("Korean Bevel Workflow") 66 | toggle_type = self.toggle_korean_bevel(context, obj, toggle_type=toggle_type) 67 | 68 | return {'FINISHED'} 69 | 70 | def toggle_subd(self, context, obj, subds, toggle_type='TOGGLE'): 71 | self.mode = 'SUBD' 72 | 73 | if obj.mode == 'EDIT': 74 | bm = bmesh.from_edit_mesh(obj.data) 75 | bm.normal_update() 76 | bm.faces.ensure_lookup_table() 77 | 78 | else: 79 | bm = bmesh.new() 80 | bm.from_mesh(obj.data) 81 | bm.normal_update() 82 | bm.faces.ensure_lookup_table() 83 | 84 | overlay = context.space_data.overlay 85 | 86 | for subd in subds: 87 | if not subd.show_on_cage: 88 | subd.show_on_cage = True 89 | 90 | # ENABLE 91 | 92 | if not (subds[0].show_in_editmode and subds[0].show_viewport): 93 | if toggle_type in ['TOGGLE', 'ENABLE']: 94 | 95 | # enable face smoothing if necessary 96 | if not bm.faces[0].smooth: 97 | for f in bm.faces: 98 | f.smooth = True 99 | 100 | if obj.mode == 'EDIT': 101 | bmesh.update_edit_mesh(obj.data) 102 | else: 103 | bm.to_mesh(obj.data) 104 | bm.free() 105 | 106 | obj.M3.has_smoothed = True 107 | 108 | for subd in subds: 109 | subd.show_in_editmode = True 110 | subd.show_viewport = True 111 | 112 | # disable overlays, prevent doing it multiple times when batch smoothing 113 | if self.toggle_subd_overlays and toggle_type == 'TOGGLE': 114 | overlay.show_overlays = False 115 | return 'ENABLE' 116 | 117 | 118 | # DISABLE 119 | 120 | else: 121 | if toggle_type in ['TOGGLE', 'DISABLE']: 122 | 123 | # disable face smoothing if it was enabled before 124 | if obj.M3.has_smoothed: 125 | for f in bm.faces: 126 | f.smooth = False 127 | 128 | if obj.mode == 'EDIT': 129 | bmesh.update_edit_mesh(obj.data) 130 | 131 | else: 132 | bm.to_mesh(obj.data) 133 | bm.free() 134 | 135 | obj.M3.has_smoothed = False 136 | 137 | 138 | for subd in subds: 139 | subd.show_in_editmode = False 140 | subd.show_viewport = False 141 | 142 | # re-enable overlays, prevent doing it multiple times when batch smoothing 143 | if toggle_type == 'TOGGLE': 144 | overlay.show_overlays = True 145 | return 'DISABLE' 146 | 147 | print(f" INFO: SubD Smoothing is {'enabled' if toggle_type == 'ENABLE' else 'disabled'} already for {obj.name}") 148 | return toggle_type 149 | 150 | def toggle_korean_bevel(self, context, obj, toggle_type='TOGGLE'): 151 | self.mode = 'KOREAN' 152 | 153 | overlay = context.space_data.overlay 154 | 155 | # enabled auto_smooth if it isn't already 156 | if not obj.data.use_auto_smooth: 157 | obj.data.use_auto_smooth = True 158 | 159 | # get the currentl auto smooth angle 160 | angle = obj.data.auto_smooth_angle 161 | 162 | if obj.mode == 'EDIT': 163 | bm = bmesh.from_edit_mesh(obj.data) 164 | bm.normal_update() 165 | bm.faces.ensure_lookup_table() 166 | 167 | else: 168 | bm = bmesh.new() 169 | bm.from_mesh(obj.data) 170 | bm.normal_update() 171 | bm.faces.ensure_lookup_table() 172 | 173 | 174 | # ENABLE 175 | 176 | if degrees(angle) < 180: 177 | if toggle_type in ['TOGGLE', 'ENABLE']: 178 | obj.M3.smooth_angle = angle 179 | 180 | # change the auto-smooth angle 181 | obj.data.auto_smooth_angle = radians(180) 182 | 183 | # enable face smoothing if necessary 184 | if not bm.faces[0].smooth: 185 | for f in bm.faces: 186 | f.smooth = True 187 | 188 | if obj.mode == 'EDIT': 189 | bmesh.update_edit_mesh(obj.data) 190 | else: 191 | bm.to_mesh(obj.data) 192 | bm.free() 193 | 194 | obj.M3.has_smoothed = True 195 | 196 | # disable overlays 197 | if self.toggle_korean_bevel_overlays and toggle_type == 'TOGGLE': 198 | overlay.show_overlays = False 199 | return 'ENABLE' 200 | 201 | 202 | # DISABLE 203 | 204 | else: 205 | if toggle_type in ['TOGGLE', 'DISABLE']: 206 | 207 | # change the auto-smooth angle 208 | obj.data.auto_smooth_angle = obj.M3.smooth_angle 209 | 210 | # disable face smoothing if it was enabled before 211 | if obj.M3.has_smoothed: 212 | for f in bm.faces: 213 | f.smooth = False 214 | 215 | if obj.mode == 'EDIT': 216 | bmesh.update_edit_mesh(obj.data) 217 | 218 | else: 219 | bm.to_mesh(obj.data) 220 | bm.free() 221 | 222 | obj.M3.has_smoothed = False 223 | 224 | # re-enable overlays 225 | if toggle_type == 'TOGGLE': 226 | overlay.show_overlays = True 227 | return 'DISABLE' 228 | 229 | print(f" INFO: Korean Bevel Smoothing is {'enabled' if toggle_type == 'ENABLE' else 'disabled'} already for {obj.name}") 230 | return toggle_type 231 | -------------------------------------------------------------------------------- /operators/surface_slide.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .. utils.modifier import add_shrinkwrap 3 | from .. utils.object import parent 4 | 5 | 6 | # TODO: add to context menu? only if modes pie is not active? 7 | 8 | 9 | class SurfaceSlide(bpy.types.Operator): 10 | bl_idname = "machin3.surface_slide" 11 | bl_label = "MACHIN3: Surface Slide" 12 | bl_description = "Start Surface Sliding: modifify the topology while keeping the inital form intact" 13 | bl_options = {'REGISTER', 'UNDO'} 14 | 15 | @classmethod 16 | def poll(cls, context): 17 | if context.mode == 'EDIT_MESH': 18 | return not [mod for mod in context.active_object.modifiers if mod.type == 'SHRINKWRAP' and 'SurfaceSlide' in mod.name] 19 | 20 | def execute(self, context): 21 | active = context.active_object 22 | active.update_from_editmode() 23 | 24 | surface = bpy.data.objects.new(name=f"{active.name}_SURFACE", object_data=active.data.copy()) 25 | surface.data.name = '%s_SURFACE' % (active.data.name) 26 | surface.use_fake_user = True 27 | surface.matrix_world = active.matrix_world 28 | 29 | # add shrinkwrap mod 30 | shrinkwrap = add_shrinkwrap(active, surface) 31 | shrinkwrap.name = 'SurfaceSlide' 32 | 33 | # move it to the beginning of the stack 34 | if active.modifiers[0] != shrinkwrap: 35 | bpy.ops.object.modifier_move_to_index(modifier=shrinkwrap.name, index=0) 36 | 37 | # parent surface to active, so you can actually move the active and surface slide will keep working as expected 38 | parent(surface, active) 39 | 40 | return {'FINISHED'} 41 | 42 | 43 | class FinishSurfaceSlide(bpy.types.Operator): 44 | bl_idname = "machin3.finish_surface_slide" 45 | bl_label = "MACHIN3: Finish Surface Slide" 46 | bl_description = "Stop Surface Sliding" 47 | bl_options = {'REGISTER', 'UNDO'} 48 | 49 | @classmethod 50 | def poll(cls, context): 51 | active = context.active_object if context.active_object else None 52 | if active: 53 | return [mod for mod in context.active_object.modifiers if mod.type == 'SHRINKWRAP' and 'SurfaceSlide' in mod.name] 54 | 55 | def execute(self, context): 56 | active = context.active_object 57 | 58 | # get shrinkwrap mod and target surface 59 | surfaceslide = self.get_surface_slide(active) 60 | surface = surfaceslide.target 61 | 62 | # remember if in edit mode 63 | editmode = context.mode == 'EDIT_MESH' 64 | 65 | # mods can only be applied in object mode, so switch to it if necessary 66 | if editmode: 67 | bpy.ops.object.mode_set(mode='OBJECT') 68 | 69 | # apply shrinkwrap mod 70 | bpy.ops.object.modifier_apply(modifier=surfaceslide.name) 71 | 72 | # go back into edit mode 73 | if editmode: 74 | bpy.ops.object.mode_set(mode='EDIT') 75 | 76 | # remove the target surface 77 | if surface: 78 | bpy.data.meshes.remove(surface.data, do_unlink=True) 79 | 80 | return {'FINISHED'} 81 | 82 | def get_surface_slide(self, obj): 83 | surfaceslide = obj.modifiers.get('SurfaceSlide') 84 | 85 | if not surfaceslide: 86 | surfaceslide = [mod for mod in obj.modifiers if mod.type == 'SHRINKWRAP' and 'SurfaceSlide' in mod.name][0] 87 | 88 | return surfaceslide 89 | -------------------------------------------------------------------------------- /operators/thread.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import IntProperty, FloatProperty, BoolProperty 3 | import bmesh 4 | from mathutils import Vector, Matrix 5 | from .. utils.selection import get_boundary_edges, get_edges_vert_sequences 6 | from .. utils.math import average_locations 7 | from .. utils.geometry import calculate_thread 8 | 9 | 10 | class Thread(bpy.types.Operator): 11 | bl_idname = "machin3.add_thread" 12 | bl_label = "MACHIN3: Add Thread" 13 | bl_description = "" 14 | bl_options = {'REGISTER', 'UNDO', 'PRESET'} 15 | 16 | radius: FloatProperty(name="Radius", min=0, default=1) 17 | segments: IntProperty(name="Segments", min=5, default=32) 18 | loops: IntProperty(name="Threads", min=1, default=4) 19 | 20 | depth: FloatProperty(name="Depth", description="Depth in Percentage of minor Diameter", min=0, max=100, default=5, subtype='PERCENTAGE') 21 | fade: FloatProperty(name="Fade", description="Percentage of Segments fading into inner Diameter", min=1, max=50, default=15, subtype='PERCENTAGE') 22 | 23 | h1: FloatProperty(name="Bottom Flank", min=0, default=0.2, step=0.1) 24 | h2: FloatProperty(name="Crest", min=0, default=0.05, step=0.1) 25 | h3: FloatProperty(name="Top Flank", min=0, default=0.2, step=0.1) 26 | h4: FloatProperty(name="Root", min=0, default=0.05, step=0.1) 27 | 28 | flip: BoolProperty(name="Flip", default=False) 29 | 30 | @classmethod 31 | def poll(cls, context): 32 | return context.mode == 'EDIT_MESH' 33 | 34 | def draw(self, context): 35 | layout = self.layout 36 | 37 | column = layout.column(align=True) 38 | column.separator() 39 | 40 | row = column.row(align=True) 41 | row.prop(self, 'loops') 42 | row.prop(self, 'depth') 43 | row.prop(self, 'fade') 44 | 45 | row = column.row(align=True) 46 | row.prop(self, 'h1', text='') 47 | row.prop(self, 'h3', text='') 48 | row.prop(self, 'h2', text='') 49 | row.prop(self, 'h4', text='') 50 | 51 | r = row.row(align=True) 52 | r.active = True if self.h4 else False 53 | r.prop(self, 'flip', toggle=True) 54 | 55 | def execute(self, context): 56 | active = context.active_object 57 | 58 | bm = bmesh.from_edit_mesh(active.data) 59 | bm.normal_update() 60 | 61 | selverts = [v for v in bm.verts if v.select] 62 | selfaces = [f for f in bm.faces if f.select] 63 | 64 | if selfaces: 65 | boundary = get_boundary_edges(selfaces) 66 | sequences = get_edges_vert_sequences(selverts, boundary, debug=False) 67 | 68 | # if there are 2 sequences 69 | if len(sequences) == 2: 70 | seq1, seq2 = sequences 71 | 72 | verts1, cyclic1 = seq1 73 | verts2, cyclic2 = seq2 74 | 75 | if self.flip: 76 | verts1, verts2 = verts2, verts1 77 | cyclic1, cyclic2 = cyclic2, cyclic1 78 | 79 | 80 | # if they are both cyclic and have the same amount of verts,and at least 5 81 | if cyclic1 == cyclic2 and cyclic1 is True and len(verts1) == len(verts2) and len(verts1) >= 5: 82 | smooth = selfaces[0].smooth 83 | 84 | if smooth: 85 | active.data.use_auto_smooth = True 86 | 87 | # deselect verts 88 | for v in verts1 + verts2: 89 | v.select_set(False) 90 | 91 | bm.select_flush(False) 92 | 93 | # set amount of segments 94 | self.segments = len(verts1) 95 | 96 | # get selection mid points 97 | center1 = average_locations([v.co for v in verts1]) 98 | center2 = average_locations([v.co for v in verts2]) 99 | 100 | # get the radii, and set the radius as an average 101 | radius1 = (center1 - verts1[0].co).length 102 | radius2 = (center2 - verts2[0].co).length 103 | self.radius = (radius1 + radius2) / 2 104 | 105 | # get depth as a percantage in relation to the radius 106 | depth = self.depth / 100 * self.radius 107 | 108 | # create point coordinates and face indices 109 | thread, bottom, top, height = calculate_thread(segments=self.segments, loops=self.loops, radius=self.radius, depth=depth, h1=self.h1, h2=self.h2, h3=self.h3, h4=self.h4, fade=self.fade / 100) 110 | 111 | if height != 0: 112 | 113 | # build the faces from those coords and indices 114 | verts, faces = self.build_faces(bm, thread, bottom, top, smooth=smooth) 115 | 116 | # scale the thread geometry to fit the selection height 117 | selheight = (center1 - center2).length 118 | bmesh.ops.scale(bm, vec=Vector((1, 1, selheight / height)), space=Matrix(), verts=verts) 119 | 120 | # move the thread geometry into alignment with the first selection center 121 | bmesh.ops.translate(bm, vec=center1, space=Matrix(), verts=verts) 122 | 123 | # then rotate it into alignment too, this is done in two steps, first the up vectors are aligned 124 | selup = (center2 - center1).normalized() 125 | 126 | selrot = Vector((0, 0, 1)).rotation_difference(selup) 127 | bmesh.ops.rotate(bm, cent=center1, matrix=selrot.to_matrix(), verts=verts, space=Matrix()) 128 | 129 | # then the first verts are aligned too, get the first vert from the active face if its part of the selection 130 | if bm.faces.active and bm.faces.active in selfaces: 131 | active_loops = [loop for v in bm.faces.active.verts if v in verts1 for loop in v.link_loops if loop.face == bm.faces.active] 132 | 133 | if active_loops[0].link_loop_next.vert == active_loops[1].vert: 134 | v1 = active_loops[1].vert 135 | else: 136 | v1 = active_loops[0].vert 137 | else: 138 | v1 = verts1[0] 139 | 140 | threadvec = verts[0].co - center1 141 | selvec = v1.co - center1 142 | 143 | matchrot = threadvec.rotation_difference(selvec).normalized() 144 | bmesh.ops.rotate(bm, cent=center1, matrix=matchrot.to_matrix(), verts=verts, space=Matrix()) 145 | 146 | # remove doubles 147 | bmesh.ops.remove_doubles(bm, verts=verts + verts1 + verts2, dist=0.00001) 148 | 149 | # remove the initially selected faces 150 | bmesh.ops.delete(bm, geom=selfaces, context='FACES') 151 | 152 | # recalculate the normals, usefull when doing inverted thread 153 | bmesh.ops.recalc_face_normals(bm, faces=[f for f in faces if f.is_valid]) 154 | 155 | bmesh.update_edit_mesh(active.data) 156 | return {'FINISHED'} 157 | return {'CANCELLED'} 158 | 159 | def build_faces(self, bm, thread, bottom, top, smooth=False): 160 | verts = [] 161 | 162 | for co in thread[0]: 163 | v = bm.verts.new(co) 164 | verts.append(v) 165 | 166 | faces = [] 167 | 168 | for ids in thread[1]: 169 | f = bm.faces.new([verts[idx] for idx in ids]) 170 | f.smooth = smooth 171 | faces.append(f) 172 | 173 | if smooth: 174 | f.edges[0].smooth = False 175 | f.edges[-2].smooth = False 176 | 177 | bottom_verts = [] 178 | 179 | for co in bottom[0]: 180 | v = bm.verts.new(co) 181 | bottom_verts.append(v) 182 | 183 | bottom_faces = [] 184 | 185 | for ids in bottom[1]: 186 | f = bm.faces.new([bottom_verts[idx] for idx in ids]) 187 | f.smooth = smooth 188 | bottom_faces.append(f) 189 | 190 | if smooth: 191 | if len(ids) == 4: 192 | f.edges[-2].smooth = False 193 | f.edges[0].smooth = False 194 | else: 195 | f.edges[-1].smooth = False 196 | f.edges[1].smooth = False 197 | 198 | 199 | top_verts = [] 200 | 201 | for co in top[0]: 202 | v = bm.verts.new(co) 203 | top_verts.append(v) 204 | 205 | top_faces = [] 206 | 207 | for ids in top[1]: 208 | f = bm.faces.new([top_verts[idx] for idx in ids]) 209 | f.smooth = smooth 210 | top_faces.append(f) 211 | 212 | if smooth: 213 | if len(ids) == 4: 214 | f.edges[-2].smooth = False 215 | f.edges[0].smooth = False 216 | else: 217 | f.edges[-1].smooth = False 218 | f.edges[1].smooth = False 219 | 220 | return [v for v in verts + bottom_verts + top_verts if v.is_valid], faces + bottom_faces + top_faces 221 | -------------------------------------------------------------------------------- /resources/matcaps/matcap_base.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/resources/matcaps/matcap_base.exr -------------------------------------------------------------------------------- /resources/matcaps/matcap_shiny_red.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/resources/matcaps/matcap_shiny_red.exr -------------------------------------------------------------------------------- /resources/matcaps/matcap_zebra_horizontal.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/resources/matcaps/matcap_zebra_horizontal.exr -------------------------------------------------------------------------------- /resources/matcaps/matcap_zebra_vertical.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/resources/matcaps/matcap_zebra_vertical.exr -------------------------------------------------------------------------------- /resources/worlds/Gdansk_shipyard_buildings.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/resources/worlds/Gdansk_shipyard_buildings.exr -------------------------------------------------------------------------------- /resources/worlds/tomoco_studio.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapHookCoding/MACHIN3tools/2c030bf1bb68496578878c7bd4c7a590aab76f9d/resources/worlds/tomoco_studio.exr -------------------------------------------------------------------------------- /ui/UILists.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class AppendMatsUIList(bpy.types.UIList): 5 | bl_idname = "MACHIN3_UL_append_mats" 6 | 7 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 8 | row = layout.split(factor=0.7) 9 | row.label(text=item.name) 10 | -------------------------------------------------------------------------------- /ui/operators/call_pie.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import StringProperty 3 | 4 | 5 | class CallMACHIN3toolsPie(bpy.types.Operator): 6 | bl_idname = "machin3.call_machin3tools_pie" 7 | bl_label = "MACHIN3: Call MACHIN3tools Pie" 8 | bl_options = {'REGISTER', 'UNDO'} 9 | 10 | idname: StringProperty() 11 | 12 | def invoke(self, context, event): 13 | if context.space_data.type == 'VIEW_3D': 14 | 15 | # SHADING PIE 16 | 17 | if self.idname == 'shading_pie': 18 | engine = context.scene.render.engine 19 | device = context.scene.cycles.device 20 | shading = context.space_data.shading 21 | 22 | # sync render engine settings 23 | if engine != context.scene.M3.render_engine and engine in ['BLENDER_EEVEE', 'CYCLES']: 24 | context.scene.M3.avoid_update = True 25 | context.scene.M3.render_engine = engine 26 | 27 | # sync cyclces device settings 28 | if engine == 'CYCLES' and device != context.scene.M3.cycles_device: 29 | context.scene.M3.avoid_update = True 30 | context.scene.M3.cycles_device = device 31 | 32 | # sync shading.light 33 | if shading.light != context.scene.M3.shading_light: 34 | context.scene.M3.avoid_update = True 35 | context.scene.M3.shading_light = shading.light 36 | 37 | context.scene.M3.avoid_update = True 38 | context.scene.M3.use_flat_shadows = shading.show_shadows 39 | 40 | 41 | bpy.ops.wm.call_menu_pie(name='MACHIN3_MT_%s' % (self.idname)) 42 | 43 | # TOOLS PIE 44 | 45 | elif self.idname == 'tools_pie': 46 | if context.mode in ['OBJECT', 'EDIT_MESH']: 47 | bpy.ops.wm.call_menu_pie(name='MACHIN3_MT_%s' % (self.idname)) 48 | 49 | else: 50 | return {'PASS_THROUGH'} 51 | 52 | return {'FINISHED'} 53 | -------------------------------------------------------------------------------- /ui/operators/collection.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import StringProperty, BoolProperty 3 | from ... utils.collection import get_groups_collection, get_scene_collections 4 | 5 | 6 | # TODO: store selected objects in blend file an immedeatly relink it into the current scene, call it StoreCollection() or SaveCollection() 7 | 8 | 9 | class CreateCollection(bpy.types.Operator): 10 | bl_idname = "machin3.create_collection" 11 | bl_label = "MACHIN3: Create Collection" 12 | bl_description = "description" 13 | bl_options = {'REGISTER', 'UNDO'} 14 | 15 | def update_name(self, context): 16 | name = self.name.strip() 17 | col = bpy.data.collections.get(name) 18 | 19 | if col: 20 | self.isduplicate = True 21 | else: 22 | self.isduplicate = False 23 | 24 | 25 | name: StringProperty("Collection Name", default="", update=update_name) 26 | 27 | # hiddem 28 | isduplicate: BoolProperty("is duplicate name") 29 | 30 | 31 | def draw(self, context): 32 | layout = self.layout 33 | 34 | column = layout.column() 35 | 36 | column.prop(self, "name", text="Name") 37 | if self.isduplicate: 38 | column.label(text="Collection '%s' exists already" % (self.name.strip()), icon='ERROR') 39 | 40 | def invoke(self, context, event): 41 | wm = context.window_manager 42 | 43 | return wm.invoke_props_dialog(self, width=300) 44 | 45 | def execute(self, context): 46 | name = self.name.strip() 47 | 48 | # create collection 49 | col = bpy.data.collections.new(name=name) 50 | 51 | # link it to the active collection 52 | acol = context.view_layer.active_layer_collection.collection 53 | acol.children.link(col) 54 | 55 | # reset the name prop 56 | self.name = '' 57 | 58 | return {'FINISHED'} 59 | 60 | 61 | class RemoveFromCollection(bpy.types.Operator): 62 | bl_idname = "machin3.remove_from_collection" 63 | bl_label = "MACHIN3: Remove from Collection" 64 | bl_description = "Remove Selection from a Collection" 65 | bl_options = {'REGISTER', 'UNDO'} 66 | 67 | @classmethod 68 | def poll(cls, context): 69 | view = context.space_data 70 | return view.type == 'VIEW_3D' and context.selected_objects 71 | 72 | def execute(self, context): 73 | 74 | # this op needs to have an active object for some reason, otherwise the collection list will stay empty 75 | if context.active_object not in context.selected_objects: 76 | context.view_layer.objects.active = context.selected_objects[0] 77 | 78 | bpy.ops.collection.objects_remove('INVOKE_DEFAULT') 79 | 80 | return {'FINISHED'} 81 | 82 | 83 | class AddToCollection(bpy.types.Operator): 84 | bl_idname = "machin3.add_to_collection" 85 | bl_label = "MACHIN3: Add to Collection" 86 | bl_description = "Add Selection to a Collection" 87 | bl_options = {'REGISTER', 'UNDO'} 88 | 89 | @classmethod 90 | def poll(cls, context): 91 | view = context.space_data 92 | 93 | if view.type == 'VIEW_3D' and context.selected_objects: 94 | 95 | # if in local view, then you have to have the outliner visible, so can't be in fullscreen 96 | # this is because for whatever reason we need to override the context for the move_to_collection() op to work in local_view 97 | if view.local_view: 98 | for area in context.screen.areas: 99 | if area.type == 'OUTLINER': 100 | return True 101 | else: 102 | return True 103 | 104 | def execute(self, context): 105 | view = context.space_data 106 | 107 | # use a context override when in local view 108 | if view.local_view: 109 | for area in context.screen.areas: 110 | if area.type == 'OUTLINER': 111 | override = {'area': area} 112 | break 113 | 114 | bpy.ops.object.link_to_collection(override, 'INVOKE_DEFAULT') 115 | 116 | else: 117 | bpy.ops.object.link_to_collection('INVOKE_DEFAULT') 118 | 119 | return {'FINISHED'} 120 | 121 | 122 | class MoveToCollection(bpy.types.Operator): 123 | bl_idname = "machin3.move_to_collection" 124 | bl_label = "MACHIN3: Move to Collection" 125 | bl_description = "Move Selection to a Collection" 126 | bl_options = {'REGISTER', 'UNDO'} 127 | 128 | @classmethod 129 | def poll(cls, context): 130 | view = context.space_data 131 | 132 | if view.type == 'VIEW_3D' and context.selected_objects: 133 | 134 | # if in local view, then you have to have the outlienr visible, so can't be in fullscreen 135 | # this is because for whatever reason we need to override the context for the move_to_collection() op to work in local_view 136 | if view.local_view: 137 | for area in context.screen.areas: 138 | if area.type == 'OUTLINER': 139 | return True 140 | else: 141 | return True 142 | 143 | def execute(self, context): 144 | view = context.space_data 145 | 146 | # use a context override when in local view 147 | if view.local_view: 148 | for area in context.screen.areas: 149 | if area.type == 'OUTLINER': 150 | override = {'area': area} 151 | break 152 | 153 | bpy.ops.object.move_to_collection(override, 'INVOKE_DEFAULT') 154 | 155 | else: 156 | bpy.ops.object.move_to_collection('INVOKE_DEFAULT') 157 | 158 | return {'FINISHED'} 159 | 160 | 161 | class Purge(bpy.types.Operator): 162 | bl_idname = "machin3.purge_collections" 163 | bl_label = "MACHIN3: Purge Collections" 164 | bl_description = "Remove empty Collections" 165 | bl_options = {'REGISTER', 'UNDO'} 166 | 167 | def execute(self, context): 168 | for col in get_scene_collections(context.scene): 169 | if not any([col.children, col.objects]): 170 | print("Removing collection '%s'." % (col.name)) 171 | bpy.data.collections.remove(col, do_unlink=True) 172 | 173 | return {'FINISHED'} 174 | 175 | 176 | class Select(bpy.types.Operator): 177 | bl_idname = "machin3.select_collection" 178 | bl_label = "MACHIN3: (De)Select Collection" 179 | bl_description = "Select Collection Objects\nSHIFT: Select all Collection Objects\nALT: Deselect Collection Objects\nSHIFT+ALT: Deselect all Collection Objects\nCTRL: Toggle Viewport Selection of Collection Objects" 180 | bl_options = {'REGISTER'} 181 | 182 | name: StringProperty() 183 | force_all: BoolProperty() 184 | 185 | def invoke(self, context, event): 186 | col = bpy.data.collections.get(self.name, context.scene.collection) 187 | 188 | objects = col.all_objects if event.shift or self.force_all else col.objects 189 | 190 | if objects: 191 | hideselect = objects[0].hide_select 192 | 193 | if col: 194 | for obj in objects: 195 | # deselect 196 | if event.alt: 197 | obj.select_set(False) 198 | 199 | # toggle hide_select (but only for objects not all_objects) 200 | elif event.ctrl: 201 | if obj.name in col.objects: 202 | obj.hide_select = not hideselect 203 | 204 | # seleect 205 | else: 206 | obj.select_set(True) 207 | 208 | self.force_all = False 209 | return {'FINISHED'} 210 | -------------------------------------------------------------------------------- /ui/operators/draw.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import FloatProperty, StringProperty, FloatVectorProperty, BoolProperty 3 | from mathutils import Vector 4 | from ... utils.draw import draw_label 5 | from ... utils.registration import get_prefs 6 | 7 | 8 | class DrawLabel(bpy.types.Operator): 9 | bl_idname = "machin3.draw_label" 10 | bl_label = "MACHIN3: draw_label" 11 | bl_description = "" 12 | bl_options = {'INTERNAL'} 13 | 14 | text: StringProperty(name="Text to draw the HUD", default='Text') 15 | coords: FloatVectorProperty(name='Screen Coordinates', size=2, default=(100, 100)) 16 | center: BoolProperty(name='Center', default=True) 17 | color: FloatVectorProperty(name='Screen Coordinates', size=3, default=(1, 1, 1)) 18 | 19 | time: FloatProperty(name="", default=1, min=0.1) 20 | alpha: FloatProperty(name="Alpha", default=0.5, min=0.1, max=1) 21 | 22 | @classmethod 23 | def poll(cls, context): 24 | return context.space_data.type == 'VIEW_3D' 25 | 26 | def draw_HUD(self, context): 27 | alpha = self.countdown / self.time * self.alpha 28 | draw_label(context, title=self.text, coords=self.coords, center=self.center, color=self.color, alpha=alpha) 29 | 30 | 31 | def modal(self, context, event): 32 | context.area.tag_redraw() 33 | 34 | # FINISH when countdown is 0 35 | 36 | if self.countdown < 0: 37 | # print("Countdown of %d seconds finished" % (self.time)) 38 | 39 | # remove time handler 40 | context.window_manager.event_timer_remove(self.TIMER) 41 | 42 | # remove draw handler 43 | bpy.types.SpaceView3D.draw_handler_remove(self.HUD, 'WINDOW') 44 | return {'FINISHED'} 45 | 46 | # COUNT DOWN 47 | 48 | if event.type == 'TIMER': 49 | self.countdown -= 0.1 50 | 51 | return {'PASS_THROUGH'} 52 | 53 | def execute(self, context): 54 | self.HUD = bpy.types.SpaceView3D.draw_handler_add(self.draw_HUD, (context, ), 'WINDOW', 'POST_PIXEL') 55 | 56 | # time handler 57 | self.TIMER = context.window_manager.event_timer_add(0.1, window=context.window) 58 | 59 | # initalize time from prefs 60 | self.countdown = self.time 61 | 62 | context.window_manager.modal_handler_add(self) 63 | return {'RUNNING_MODAL'} 64 | 65 | 66 | 67 | class DrawLabels(bpy.types.Operator): 68 | bl_idname = "machin3.draw_labels" 69 | bl_label = "MACHIN3: draw_labels" 70 | bl_description = "" 71 | bl_options = {'INTERNAL'} 72 | 73 | text: StringProperty(name="Text to draw the HUD", default='Text') 74 | text2: StringProperty(name="Second Text to draw the HUD", default='Text') 75 | 76 | coords: FloatVectorProperty(name='Screen Coordinates', size=2, default=(100, 100)) 77 | 78 | center: BoolProperty(name='Center', default=True) 79 | color: FloatVectorProperty(name='Screen Coordinates', size=3, default=(1, 1, 1)) 80 | color2: FloatVectorProperty(name='Screen Coordinates', size=3, default=(1, 1, 1)) 81 | 82 | time: FloatProperty(name="", default=1, min=0.1) 83 | alpha: FloatProperty(name="Alpha", default=0.5, min=0.1, max=1) 84 | 85 | @classmethod 86 | def poll(cls, context): 87 | return context.space_data.type == 'VIEW_3D' 88 | 89 | def draw_HUD(self, context): 90 | alpha = self.countdown / self.time * self.alpha 91 | scale = context.preferences.view.ui_scale * get_prefs().HUD_scale 92 | 93 | draw_label(context, title=self.text, coords=self.coords, center=self.center, color=self.color, alpha=alpha) 94 | 95 | if self.text2: 96 | draw_label(context, title=self.text2, coords=Vector(self.coords) + Vector((0, scale * -15)), center=self.center, color=self.color2, alpha=alpha * 2) 97 | 98 | 99 | def modal(self, context, event): 100 | context.area.tag_redraw() 101 | 102 | # FINISH when countdown is 0 103 | 104 | if self.countdown < 0: 105 | # print("Countdown of %d seconds finished" % (self.time)) 106 | 107 | # remove time handler 108 | context.window_manager.event_timer_remove(self.TIMER) 109 | 110 | # remove draw handler 111 | bpy.types.SpaceView3D.draw_handler_remove(self.HUD, 'WINDOW') 112 | return {'FINISHED'} 113 | 114 | # COUNT DOWN 115 | 116 | if event.type == 'TIMER': 117 | self.countdown -= 0.1 118 | 119 | return {'PASS_THROUGH'} 120 | 121 | def execute(self, context): 122 | self.HUD = bpy.types.SpaceView3D.draw_handler_add(self.draw_HUD, (context, ), 'WINDOW', 'POST_PIXEL') 123 | 124 | # time handler 125 | self.TIMER = context.window_manager.event_timer_add(0.1, window=context.window) 126 | 127 | # initalize time from prefs 128 | self.countdown = self.time 129 | 130 | context.window_manager.modal_handler_add(self) 131 | return {'RUNNING_MODAL'} 132 | -------------------------------------------------------------------------------- /ui/operators/grease_pencil.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from ... utils.raycast import get_closest 3 | 4 | 5 | class ShrinkwrapGreasePencil(bpy.types.Operator): 6 | bl_idname = "machin3.shrinkwrap_grease_pencil" 7 | bl_label = "MACHIN3: ShrinkWrap Grease Pencil" 8 | bl_description = "Shrinkwrap current Grease Pencil Layer to closest mesh surface based on Surface Offset value" 9 | bl_options = {'REGISTER', 'UNDO'} 10 | 11 | @classmethod 12 | def poll(cls, context): 13 | active = context.active_object 14 | if active and active.type == 'GPENCIL': 15 | return active.data.layers.active 16 | 17 | def execute(self, context): 18 | dg = context.evaluated_depsgraph_get() 19 | 20 | gp = context.active_object 21 | mx = gp.matrix_world 22 | offset = gp.data.zdepth_offset 23 | 24 | layer = gp.data.layers.active 25 | frame = layer.active_frame 26 | 27 | for stroke in frame.strokes: 28 | for idx, point in enumerate(stroke.points): 29 | closest, _, co, no, _, _ = get_closest(mx @ point.co, depsgraph=dg, debug=False) 30 | 31 | if closest: 32 | point.co = mx.inverted_safe() @ (co + no * offset) 33 | 34 | return {'FINISHED'} 35 | -------------------------------------------------------------------------------- /ui/operators/mode.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import StringProperty 3 | from ... utils.registration import get_prefs 4 | from ... utils.view import set_xray, reset_xray, update_local_view 5 | from ... utils.object import parent 6 | from ... utils.tools import get_active_tool, get_tools_from_context 7 | 8 | 9 | user_cavity = True 10 | 11 | 12 | class EditMode(bpy.types.Operator): 13 | bl_idname = "machin3.edit_mode" 14 | bl_label = "Edit Mode" 15 | bl_options = {'REGISTER', 'UNDO'} 16 | 17 | # hidden (screen cast) 18 | toggled_object = False 19 | 20 | @classmethod 21 | def description(cls, context, properties): 22 | return f"Switch to {'Object' if context.mode == 'EDIT_MESH' else 'Edit'} Mode" 23 | 24 | def execute(self, context): 25 | global user_cavity 26 | 27 | shading = context.space_data.shading 28 | toggle_cavity = get_prefs().toggle_cavity 29 | toggle_xray = get_prefs().toggle_xray 30 | sync_tools = get_prefs().sync_tools 31 | 32 | if sync_tools: 33 | active_tool = get_active_tool(context).idname 34 | 35 | if context.mode == "OBJECT": 36 | if toggle_xray: 37 | set_xray(context) 38 | 39 | bpy.ops.object.mode_set(mode="EDIT") 40 | 41 | if toggle_cavity: 42 | user_cavity = shading.show_cavity 43 | shading.show_cavity = False 44 | 45 | if sync_tools and active_tool in get_tools_from_context(context): 46 | bpy.ops.wm.tool_set_by_id(name=active_tool) 47 | 48 | self.toggled_object = False 49 | 50 | 51 | elif context.mode == "EDIT_MESH": 52 | if toggle_xray: 53 | reset_xray(context) 54 | 55 | bpy.ops.object.mode_set(mode="OBJECT") 56 | 57 | if toggle_cavity and user_cavity: 58 | shading.show_cavity = True 59 | user_cavity = True 60 | 61 | if sync_tools and active_tool in get_tools_from_context(context): 62 | bpy.ops.wm.tool_set_by_id(name=active_tool) 63 | 64 | self.toggled_object = True 65 | 66 | return {'FINISHED'} 67 | 68 | 69 | class MeshMode(bpy.types.Operator): 70 | bl_idname = "machin3.mesh_mode" 71 | bl_label = "Mesh Mode" 72 | bl_options = {'REGISTER', 'UNDO'} 73 | 74 | mode: StringProperty() 75 | 76 | @classmethod 77 | def description(cls, context, properties): 78 | return "%s Select\nCTRL + Click: Expand Selection" % (properties.mode.capitalize()) 79 | 80 | def invoke(self, context, event): 81 | global user_cavity 82 | 83 | shading = context.space_data.shading 84 | toggle_cavity = get_prefs().toggle_cavity 85 | toggle_xray = get_prefs().toggle_xray 86 | 87 | if context.mode == "OBJECT": 88 | if toggle_xray: 89 | set_xray(context) 90 | 91 | active_tool = get_active_tool(context).idname if get_prefs().sync_tools else None 92 | 93 | bpy.ops.object.mode_set(mode="EDIT") 94 | 95 | if toggle_cavity: 96 | user_cavity = shading.show_cavity 97 | shading.show_cavity = False 98 | 99 | if active_tool and active_tool in get_tools_from_context(context): 100 | bpy.ops.wm.tool_set_by_id(name=active_tool) 101 | 102 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=event.ctrl, type=self.mode) 103 | return {'FINISHED'} 104 | 105 | 106 | class ImageMode(bpy.types.Operator): 107 | bl_idname = "machin3.image_mode" 108 | bl_label = "MACHIN3: Image Mode" 109 | bl_options = {'REGISTER'} 110 | 111 | mode: StringProperty() 112 | 113 | def execute(self, context): 114 | view = context.space_data 115 | active = context.active_object 116 | 117 | toolsettings = context.scene.tool_settings 118 | view.mode = self.mode 119 | 120 | if self.mode == "UV" and active: 121 | if active.mode == "OBJECT": 122 | uvs = active.data.uv_layers 123 | 124 | # create new uv layer 125 | if not uvs: 126 | uvs.new() 127 | 128 | bpy.ops.object.mode_set(mode="EDIT") 129 | 130 | if not toolsettings.use_uv_select_sync: 131 | bpy.ops.mesh.select_all(action="SELECT") 132 | 133 | return {'FINISHED'} 134 | 135 | 136 | class UVMode(bpy.types.Operator): 137 | bl_idname = "machin3.uv_mode" 138 | bl_label = "MACHIN3: UV Mode" 139 | bl_options = {'REGISTER'} 140 | 141 | mode: StringProperty() 142 | 143 | def execute(self, context): 144 | toolsettings = context.scene.tool_settings 145 | view = context.space_data 146 | 147 | if view.mode != "UV": 148 | view.mode = "UV" 149 | 150 | if toolsettings.use_uv_select_sync: 151 | bpy.ops.mesh.select_mode(type=self.mode.replace("VERTEX", "VERT")) 152 | 153 | else: 154 | toolsettings.uv_select_mode = self.mode 155 | 156 | return {'FINISHED'} 157 | 158 | 159 | class SurfaceDrawMode(bpy.types.Operator): 160 | bl_idname = "machin3.surface_draw_mode" 161 | bl_label = "MACHIN3: Surface Draw Mode" 162 | bl_description = "Surface Draw, create parented, empty GreasePencil object and enter DRAW mode.\nSHIFT: Select the Line tool." 163 | bl_options = {'REGISTER', 'UNDO'} 164 | 165 | def invoke(self, context, event): 166 | # forcing object mode at the beginning, avoids issues when calling this tool from PAINT_WEIGHT mode 167 | bpy.ops.object.mode_set(mode='OBJECT') 168 | 169 | scene = context.scene 170 | ts = scene.tool_settings 171 | mcol = context.collection 172 | view = context.space_data 173 | active = context.active_object 174 | 175 | existing_gps = [obj for obj in active.children if obj.type == "GPENCIL"] 176 | 177 | if existing_gps: 178 | gp = existing_gps[0] 179 | 180 | else: 181 | name = "%s_SurfaceDrawing" % (active.name) 182 | gp = bpy.data.objects.new(name, bpy.data.grease_pencils.new(name)) 183 | 184 | mcol.objects.link(gp) 185 | 186 | gp.matrix_world = active.matrix_world 187 | parent(gp, active) 188 | 189 | update_local_view(view, [(gp, True)]) 190 | 191 | # create a new layer and set it to multply, multiply actuall results in a dark gpencil in sold shading, otherwise it would be bright, even with a black material 192 | layer = gp.data.layers.new(name="SurfaceLayer") 193 | layer.blend_mode = 'MULTIPLY' 194 | 195 | # create a frame on the layer if there isn't anyone, otherwise you won't be able to draw 196 | if not layer.frames: 197 | layer.frames.new(0) 198 | 199 | context.view_layer.objects.active = gp 200 | active.select_set(False) 201 | gp.select_set(True) 202 | 203 | # set object color to black 204 | gp.color = (0, 0, 0, 1) 205 | 206 | # create black gp mat and append it, look for existing one to avoid duplicates 207 | blacks = [mat for mat in bpy.data.materials if mat.name == 'Black' and mat.is_grease_pencil] 208 | mat = blacks[0] if blacks else bpy.data.materials.new(name='Black') 209 | 210 | bpy.data.materials.create_gpencil_data(mat) 211 | gp.data.materials.append(mat) 212 | 213 | # go into gp draw mode 214 | bpy.ops.object.mode_set(mode='PAINT_GPENCIL') 215 | 216 | # setup surface drawing 217 | ts.gpencil_stroke_placement_view3d = 'SURFACE' 218 | 219 | # note that dis value is not absulate and oddly depends on the view to object distance 220 | gp.data.zdepth_offset = 0.01 221 | 222 | # set the strength to 1, vs the defautl 0.6, making strokes transparent 223 | ts.gpencil_paint.brush.gpencil_settings.pen_strength = 1 224 | 225 | if not view.show_region_toolbar: 226 | view.show_region_toolbar = True 227 | 228 | # add opacity and thickness mods 229 | opacity = gp.grease_pencil_modifiers.new(name="Opacity", type="GP_OPACITY") 230 | opacity.show_expanded = False 231 | thickness = gp.grease_pencil_modifiers.new(name="Thickness", type="GP_THICK") 232 | thickness.show_expanded = False 233 | 234 | # optionally select the line tool 235 | if event.shift: 236 | bpy.ops.wm.tool_set_by_id(name="builtin.line") 237 | 238 | # by default pick the brush 239 | else: 240 | bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw") 241 | 242 | return {'FINISHED'} 243 | -------------------------------------------------------------------------------- /ui/operators/open_blend.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import StringProperty 3 | import subprocess 4 | 5 | 6 | class OpenLibraryBlend(bpy.types.Operator): 7 | bl_idname = "machin3.open_library_blend" 8 | bl_label = "MACHIN3: Open Library Blend" 9 | bl_description = "Open new Blender instance, loading the library sourced in the selected object or collection instance." 10 | bl_options = {'REGISTER'} 11 | 12 | blendpath: StringProperty() 13 | library: StringProperty() 14 | 15 | def execute(self, context): 16 | blenderbinpath = bpy.app.binary_path 17 | 18 | cmd = [blenderbinpath, self.blendpath] 19 | blender = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 20 | 21 | # it it loeaded successfullly reload the library to update any changes that were done 22 | if blender and self.library: 23 | lib = bpy.data.libraries.get(self.library) 24 | if lib: 25 | lib.reload() 26 | 27 | return {'FINISHED'} 28 | -------------------------------------------------------------------------------- /ui/operators/overlay.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | axis_x = True 4 | axis_y = True 5 | axis_z = False 6 | 7 | 8 | class ToggleGrid(bpy.types.Operator): 9 | bl_idname = "machin3.toggle_grid" 10 | bl_label = "Toggle Grid" 11 | bl_description = "Toggle Grid, distinguish between the grid in regular views and orthographic side views" 12 | bl_options = {'REGISTER'} 13 | 14 | def execute(self, context): 15 | global axis_x, axis_y, axis_z 16 | 17 | view = context.space_data 18 | overlay = view.overlay 19 | perspective_type = view.region_3d.view_perspective 20 | 21 | mode = "GRID" if perspective_type == "ORTHO" and view.region_3d.is_orthographic_side_view else "FLOOR" 22 | 23 | if mode == "FLOOR": 24 | if overlay.show_floor: 25 | # get axes states 26 | axis_x = overlay.show_axis_x 27 | axis_y = overlay.show_axis_y 28 | axis_z = overlay.show_axis_z 29 | 30 | # turn grid OFF 31 | overlay.show_floor = False 32 | 33 | # turn axes OFF 34 | overlay.show_axis_x = False 35 | overlay.show_axis_y = False 36 | overlay.show_axis_z = False 37 | 38 | else: 39 | # turn grid ON 40 | overlay.show_floor = True 41 | 42 | # turn axes ON (according to previous states) 43 | overlay.show_axis_x = axis_x 44 | overlay.show_axis_y = axis_y 45 | overlay.show_axis_z = axis_z 46 | 47 | elif mode == "GRID": 48 | overlay.show_ortho_grid = not overlay.show_ortho_grid 49 | 50 | return {'FINISHED'} 51 | 52 | 53 | class ToggleWireframe(bpy.types.Operator): 54 | bl_idname = "machin3.toggle_wireframe" 55 | bl_label = "Toggle Wireframe" 56 | bl_options = {'REGISTER'} 57 | 58 | @classmethod 59 | def description(cls, context, properties): 60 | if context.mode == 'OBJECT': 61 | return "Toggle Wireframe display for the selected objects\nNothing Selected: Toggle Wireframe Overlay, affecting all objects" 62 | elif context.mode == 'EDIT_MESH': 63 | return "Toggle X-Ray, resembling how edit mode wireframes worked in Blender 2.79" 64 | 65 | def execute(self, context): 66 | overlay = context.space_data.overlay 67 | 68 | if context.mode == "OBJECT": 69 | sel = context.selected_objects 70 | 71 | if sel: 72 | for obj in sel: 73 | obj.show_wire = not obj.show_wire 74 | obj.show_all_edges = obj.show_wire 75 | else: 76 | overlay.show_wireframes = not overlay.show_wireframes 77 | 78 | 79 | elif context.mode == "EDIT_MESH": 80 | context.scene.M3.show_edit_mesh_wire = not context.scene.M3.show_edit_mesh_wire 81 | 82 | return {'FINISHED'} 83 | -------------------------------------------------------------------------------- /ui/operators/snapping_preset.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import StringProperty, BoolProperty 3 | 4 | 5 | class SetSnappingPreset(bpy.types.Operator): 6 | bl_idname = "machin3.set_snapping_preset" 7 | bl_label = "MACHIN3: Set Snapping Preset" 8 | bl_description = "Set Snapping Preset" 9 | bl_options = {'REGISTER', 'UNDO'} 10 | 11 | element: StringProperty(name="Snap Element") 12 | target: StringProperty(name="Snap Target") 13 | align_rotation: BoolProperty(name="Align Rotation") 14 | 15 | def draw(self, context): 16 | layout = self.layout 17 | column = layout.column() 18 | 19 | @classmethod 20 | def description(cls, context, properties): 21 | 22 | if properties.element == 'VERTEX': 23 | return "Snap to Vertices" 24 | 25 | elif properties.element == 'EDGE': 26 | return "Snap to Edges" 27 | 28 | elif properties.element == 'FACE' and properties.align_rotation: 29 | return "Snap to Faces and Align the Rotation" 30 | 31 | elif properties.element == 'INCREMENT': 32 | return "Snap to Absolute Grid Points" 33 | 34 | elif properties.element == 'VOLUME': 35 | return "Snap to Volumes" 36 | 37 | @classmethod 38 | def poll(cls, context): 39 | return context.space_data.type == 'VIEW_3D' 40 | 41 | def execute(self, context): 42 | ts = context.scene.tool_settings 43 | 44 | ts.snap_elements = {self.element} 45 | 46 | if self.element == 'INCREMENT': 47 | ts.use_snap_grid_absolute = True 48 | 49 | elif self.element == 'VOLUME': 50 | pass 51 | 52 | else: 53 | ts.snap_target = self.target 54 | ts.use_snap_align_rotation = self.align_rotation 55 | 56 | return {'FINISHED'} 57 | -------------------------------------------------------------------------------- /ui/operators/tool.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import StringProperty, FloatProperty 3 | from ... utils.tools import get_tools_from_context, get_tool_options, get_active_tool 4 | from ... utils.registration import get_addon_prefs, get_addon, get_prefs 5 | from ... utils.tools import prettify_tool_name 6 | from ... colors import white 7 | 8 | 9 | class SetToolByName(bpy.types.Operator): 10 | bl_idname = "machin3.set_tool_by_name" 11 | bl_label = "MACHIN3: Set Tool by Name" 12 | bl_description = "Set Tool by Name" 13 | bl_options = {'REGISTER', 'UNDO'} 14 | 15 | name: StringProperty(name="Tool name/ID") 16 | alpha: FloatProperty(name="Alpha", default=0.5, min=0.1, max=1) 17 | 18 | def draw(self, context): 19 | layout = self.layout 20 | column = layout.column() 21 | 22 | column.label(text=f"Tool: {self.name}") 23 | 24 | def execute(self, context): 25 | 26 | # re-enable the cursor if switching away from the simple hyper cursor 27 | active_tool = get_active_tool(context).idname 28 | 29 | if active_tool == 'machin3.tool_hyper_cursor_simple': 30 | context.space_data.overlay.show_cursor = True 31 | 32 | # switch to the passed in tool 33 | bpy.ops.wm.tool_set_by_id(name=self.name) 34 | 35 | # ensure hyper cursor gizmos are actually shown 36 | if 'machin3.tool_hyper_cursor' in self.name: 37 | context.scene.HC.show_gizmos = True 38 | 39 | # draw a prettified version of the new tool in a fading HUD 40 | name = prettify_tool_name(self.name) 41 | 42 | coords = (context.region.width / 2, 100) 43 | bpy.ops.machin3.draw_label(text=name, coords=coords, color=white, time=get_prefs().HUD_fade_tools_pie) 44 | 45 | # return {'RUNNING_MODAL'} 46 | return {'FINISHED'} 47 | 48 | 49 | 50 | boxcutter = None 51 | 52 | 53 | class SetBCPreset(bpy.types.Operator): 54 | bl_idname = "machin3.set_boxcutter_preset" 55 | bl_label = "MACHIN3: Set BoxCutter Preset" 56 | bl_description = "Quickly enable/switch BC tool in/to various modes" 57 | bl_options = {'REGISTER', 'UNDO'} 58 | 59 | mode: StringProperty() 60 | shape_type: StringProperty() 61 | set_origin: StringProperty(default='MOUSE') 62 | 63 | @classmethod 64 | def poll(cls, context): 65 | global boxcutter 66 | 67 | if boxcutter is None: 68 | _, boxcutter, _, _ = get_addon("BoxCutter") 69 | 70 | return boxcutter in get_tools_from_context(context) 71 | 72 | def execute(self, context): 73 | global boxcutter 74 | 75 | if boxcutter is None: 76 | _, boxcutter, _, _ = get_addon("BoxCutter") 77 | 78 | tools = get_tools_from_context(context) 79 | bcprefs = get_addon_prefs('BoxCutter') 80 | 81 | # ensure the BC tool is active 82 | if not tools[boxcutter]['active']: 83 | bpy.ops.wm.tool_set_by_id(name=boxcutter) 84 | 85 | options = get_tool_options(context, boxcutter, 'bc.shape_draw') 86 | 87 | if options: 88 | options.mode = self.mode 89 | options.shape_type = self.shape_type 90 | 91 | bcprefs.behavior.set_origin = self.set_origin 92 | bcprefs.snap.enable = True 93 | 94 | return {'FINISHED'} 95 | -------------------------------------------------------------------------------- /ui/operators/transform_preset.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import StringProperty 3 | 4 | 5 | class SetTransformPreset(bpy.types.Operator): 6 | bl_idname = "machin3.set_transform_preset" 7 | bl_label = "MACHIN3: Set Transform Preset" 8 | bl_description = "Set Transform Pivot and Orientation at the same time." 9 | bl_options = {'REGISTER', 'UNDO'} 10 | 11 | pivot: StringProperty(name="Transform Pivot") 12 | orientation: StringProperty(name="Transform Orientation") 13 | 14 | def draw(self, context): 15 | layout = self.layout 16 | column = layout.column() 17 | 18 | @classmethod 19 | def poll(cls, context): 20 | return context.space_data.type == 'VIEW_3D' 21 | 22 | def execute(self, context): 23 | context.scene.tool_settings.transform_pivot_point = self.pivot 24 | context.scene.transform_orientation_slots[0].type = self.orientation 25 | 26 | return {'FINISHED'} 27 | -------------------------------------------------------------------------------- /ui/operators/uv.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import EnumProperty, BoolProperty 3 | import bmesh 4 | from ... items import uv_axis_items, uv_align_axis_mapping_dict, align_type_items, align_direction_items 5 | 6 | 7 | class AlignUV(bpy.types.Operator): 8 | bl_idname = "machin3.align_uv" 9 | bl_label = "MACHIN3: Align (UV)" 10 | bl_options = {'REGISTER', 'UNDO'} 11 | bl_description = "Align verts based on min/max UV values" 12 | 13 | type: EnumProperty(name="Type", items=align_type_items, default="MIN") 14 | 15 | axis: EnumProperty(name="Axis", items=uv_axis_items, default="U") 16 | 17 | @classmethod 18 | def poll(cls, context): 19 | return context.mode == "EDIT_MESH" and context.space_data.type == 'IMAGE_EDITOR' 20 | 21 | def execute(self, context): 22 | self.uv_align(context, self.type, uv_align_axis_mapping_dict[self.axis]) 23 | return {'FINISHED'} 24 | 25 | def uv_align(self, context, type, axis): 26 | active = context.active_object 27 | sync = context.scene.tool_settings.use_uv_select_sync 28 | 29 | bm = bmesh.from_edit_mesh(active.data) 30 | bm.normal_update() 31 | bm.verts.ensure_lookup_table() 32 | 33 | uvs = bm.loops.layers.uv.verify() 34 | 35 | # get selected loops 36 | if sync: 37 | loops = [l for v in bm.verts if v.select for l in v.link_loops] 38 | 39 | else: 40 | loops = [l for f in bm.faces if f.select for l in f.loops if l[uvs].select] 41 | 42 | # it's possible you have verts/faces selected but no loops, if you multiple objects are in edit mode and non of the selected loops are in the active object 43 | if loops: 44 | axiscoords = [l[uvs].uv[axis] for l in loops] 45 | 46 | # get target value depending on type 47 | if type == "MIN": 48 | target = min(axiscoords) 49 | 50 | elif type == "MAX": 51 | target = max(axiscoords) 52 | 53 | elif type == "ZERO": 54 | target = 0 55 | 56 | elif type == "AVERAGE": 57 | target = sum(axiscoords) / len(axiscoords) 58 | 59 | elif type == "CURSOR": 60 | target = context.space_data.cursor_location[axis] 61 | 62 | # set the new coordinates 63 | for l in loops: 64 | l[uvs].uv[axis] = target 65 | 66 | bmesh.update_edit_mesh(active.data) 67 | -------------------------------------------------------------------------------- /utils/append.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | 4 | 5 | def append_group(filepath, name, link=False, relative=False): 6 | return append_element(filepath, "groups", name, link, relative) 7 | 8 | 9 | def append_collection(filepath, name, link=False, relative=False): 10 | return append_element(filepath, "collections", name, link, relative) 11 | 12 | 13 | def append_object(filepath, name, link=False, relative=False): 14 | return append_element(filepath, "objects", name, link, relative) 15 | 16 | 17 | def append_material(filepath, name, link=False, relative=False): 18 | return append_element(filepath, "materials", name, link, relative) 19 | 20 | 21 | def append_scene(filepath, name, link=False, relative=False): 22 | return append_element(filepath, "scenes", name, link, relative) 23 | 24 | 25 | def append_world(filepath, name, link=False, relative=False): 26 | return append_element(filepath, "worlds", name, link, relative) 27 | 28 | 29 | def append_nodetree(filepath, name, link=False, relative=False): 30 | return append_element(filepath, "node_groups", name, link, relative) 31 | 32 | 33 | def append_element(filepath, collection, name, link, relative): 34 | if os.path.exists(filepath): 35 | 36 | with bpy.data.libraries.load(filepath, link=link, relative=relative) as (data_from, data_to): 37 | if name in getattr(data_from, collection): 38 | getattr(data_to, collection).append(name) 39 | 40 | else: 41 | print("%s does not exist in %s/%s" % (name, filepath, collection)) 42 | return 43 | 44 | return getattr(data_to, collection)[0] 45 | 46 | else: 47 | print("The file %s does not exist" % (filepath)) 48 | -------------------------------------------------------------------------------- /utils/asset.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | from . system import printd 4 | from . registration import get_prefs 5 | 6 | 7 | def get_catalogs_from_asset_libraries(context, debug=False): 8 | ''' 9 | scan cat files of all asset libraries and get the uuid for each catalog 10 | if different catalogs share a name, only take the first one 11 | ''' 12 | 13 | asset_libraries = context.preferences.filepaths.asset_libraries 14 | all_catalogs = [] 15 | 16 | for lib in asset_libraries: 17 | name = lib.name 18 | path = lib.path 19 | 20 | cat_path = os.path.join(path, 'blender_assets.cats.txt') 21 | 22 | if os.path.exists(cat_path): 23 | if debug: 24 | print(name, cat_path) 25 | 26 | with open(cat_path) as f: 27 | lines = f.readlines() 28 | 29 | for line in lines: 30 | if line != '\n' and not any([line.startswith(skip) for skip in ['#', 'VERSION']]) and len(line.split(':')) == 3: 31 | all_catalogs.append(line[:-1]) 32 | 33 | catalogs = {} 34 | 35 | for cat in all_catalogs: 36 | uuid, catalog, simple_name = cat.split(':') 37 | 38 | if catalog not in catalogs: 39 | catalogs[catalog] = {'uuid': uuid, 40 | 'simple_name': simple_name} 41 | 42 | if debug: 43 | printd(catalogs) 44 | 45 | return catalogs 46 | 47 | 48 | def update_asset_catalogs(self, context): 49 | self.catalogs = get_catalogs_from_asset_libraries(context, debug=False) 50 | 51 | items = [('NONE', 'None', '')] 52 | 53 | for catalog in self.catalogs: 54 | # print(catalog) 55 | items.append((catalog, catalog, "")) 56 | 57 | default = get_prefs().preferred_default_catalog if get_prefs().preferred_default_catalog in self.catalogs else 'NONE' 58 | bpy.types.WindowManager.M3_asset_catalogs = bpy.props.EnumProperty(name="Asset Categories", items=items, default=default) 59 | -------------------------------------------------------------------------------- /utils/collection.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . registration import get_addon 3 | 4 | 5 | def get_groups_collection(scene): 6 | mcol = scene.collection 7 | 8 | gpcol = bpy.data.collections.get("Groups") 9 | 10 | if gpcol: 11 | # link Groups collection, if it exists but is not linked to the master collection 12 | if gpcol.name not in mcol.children: 13 | mcol.children.link(gpcol) 14 | 15 | # create Groups collection, if it doesn't exist 16 | else: 17 | gpcol = bpy.data.collections.new(name="Groups") 18 | mcol.children.link(gpcol) 19 | 20 | return gpcol 21 | 22 | 23 | def get_scene_collections(scene, ignore_decals=True): 24 | decalmachine, _, _, _ = get_addon("DECALmachine") 25 | mcol = scene.collection 26 | 27 | scenecols = [] 28 | seen = list(mcol.children) 29 | 30 | while seen: 31 | col = seen.pop(0) 32 | if col not in scenecols: 33 | if not (ignore_decals and decalmachine and (col.DM.isdecaltypecol or col.DM.isdecalparentcol)): 34 | scenecols.append(col) 35 | seen.extend(list(col.children)) 36 | 37 | return scenecols 38 | 39 | 40 | def get_collection_depth(self, collections, depth=0, init=False): 41 | if init or depth > self.depth: 42 | self.depth = depth 43 | 44 | for col in collections: 45 | if col.children: 46 | get_collection_depth(self, col.children, depth + 1, init=False) 47 | 48 | return self.depth 49 | -------------------------------------------------------------------------------- /utils/developer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pkgutil 3 | import importlib 4 | import time 5 | 6 | 7 | chronicle = [] 8 | 9 | 10 | class Benchmark(): 11 | def __init__(self, do_benchmark): 12 | if do_benchmark: 13 | os.system("clear") 14 | self.do_benchmark = do_benchmark 15 | self.start_time = self.time = time.time() 16 | self.chronicle = [] 17 | 18 | def measure(self, name=""): 19 | if self.do_benchmark: 20 | t = time.time() - self.time 21 | self.time += t 22 | self.chronicle.append(t) 23 | 24 | global chronicle 25 | if chronicle: 26 | diff = self.chronicle[-1] - chronicle[len(self.chronicle) - 1] 27 | diff = "+ %.6f" % diff if diff > 0 else ("%.6f" % diff).replace("-", "- ") 28 | 29 | print("--- %f (%s) - %s" % (t, diff, name)) 30 | else: 31 | print("--- %f - %s" % (t, name)) 32 | 33 | def total(self): 34 | if self.do_benchmark: 35 | t = time.time() - self.start_time 36 | self.chronicle.append(t) 37 | 38 | global chronicle 39 | if chronicle: 40 | diff = self.chronicle[-1] - chronicle[len(self.chronicle) - 1] 41 | diff = "+ %.6f" % diff if diff > 0 else ("%.6f" % diff).replace("-", "- ") 42 | 43 | print(" » %f (%s) - %s" % (t, diff, "total")) 44 | else: 45 | print(" » %f - %s" % (t, "total")) 46 | 47 | chronicle = self.chronicle 48 | 49 | 50 | def output_traceback(self): 51 | import traceback 52 | print() 53 | traceback.print_exc() 54 | 55 | tb = traceback.format_exc() + "\nPLEASE REPORT THIS ERROR to mesh@machin3.io" 56 | self.report({'ERROR'}, tb) 57 | -------------------------------------------------------------------------------- /utils/geometry.py: -------------------------------------------------------------------------------- 1 | from math import cos, sin, pi 2 | from mathutils import Vector 3 | 4 | 5 | def calculate_thread(segments=12, loops=2, radius=1, depth=0.1, h1=0.2, h2=0.0, h3=0.2, h4=0.0, fade=0.15): 6 | ''' 7 | create thread coordinates and face indices using the folowing profile 8 | thread profile 9 | # | h4 10 | # \ h3 11 | # | h2 12 | # / h1 13 | also ceate coordinates and indices for faces at the bottom and top of the thread, creating a full cylinder 14 | return coords and indices tuples for thread, bottom and top faces, as well as the total height of the thread 15 | ''' 16 | 17 | height = h1 + h2 + h3 + h4 18 | 19 | # fade determines how many of the segments falloff 20 | falloff = segments * fade 21 | 22 | # create profile coords, there are 3-5 coords, depending on the h2 and h4 "spacer values" 23 | profile = [Vector((radius, 0, 0))] 24 | profile.append(Vector((radius + depth, 0, h1))) 25 | 26 | if h2 > 0: 27 | profile.append(Vector((radius + depth, 0, h1 + h2))) 28 | 29 | profile.append(Vector((radius, 0, h1 + h2 + h3))) 30 | 31 | if h4 > 0: 32 | profile.append(Vector((radius, 0, h1 + h2 + h3 + h4))) 33 | 34 | # based on the profile create the thread coords and indices 35 | pcount = len(profile) 36 | 37 | coords = [] 38 | indices = [] 39 | 40 | bottom_coords = [] 41 | bottom_indices = [] 42 | 43 | top_coords = [] 44 | top_indices = [] 45 | 46 | for loop in range(loops): 47 | for segment in range(segments + 1): 48 | angle = segment * 2 * pi / segments 49 | 50 | # create the thread coords 51 | for pidx, co in enumerate(profile): 52 | 53 | # the radius for individual points is always the x coord, except when adjusting the falloff for the first or last segments 54 | if loop == 0 and segment <= falloff and pidx in ([1, 2] if h2 else [1]): 55 | r = radius + depth * segment / falloff 56 | elif loop == loops - 1 and segments - segment <= falloff and pidx in ([1, 2] if h2 else [1]): 57 | r = radius + depth * (segments - segment) / falloff 58 | else: 59 | r = co.x 60 | 61 | # slightly increase each profile coords height per segment, and offset it per loop too 62 | z = co.z + (segment / segments) * height + (height * loop) 63 | 64 | # add thread coords 65 | coords.append(Vector((r * cos(angle), r * sin(angle), z))) 66 | 67 | # add bottom coords, to close off the thread faces into a full cylinder 68 | if loop == 0 and pidx == 0: 69 | 70 | # the last segment, has coords for all the verts of the profile! 71 | if segment == segments: 72 | bottom_coords.extend([Vector((radius, 0, co.z)) for co in profile]) 73 | 74 | # every other segment has a point at z == 0 and the first point in the profile 75 | else: 76 | bottom_coords.extend([Vector((r * cos(angle), r * sin(angle), 0)), Vector((r * cos(angle), r * sin(angle), z))]) 77 | 78 | elif loop == loops - 1 and pidx == len(profile) - 1: 79 | 80 | # the first segment, has coords for all the verts of the profile! 81 | if segment == 0: 82 | top_coords.extend([Vector((radius, 0, co.z + height + height * loop)) for co in profile]) 83 | 84 | # every other segment has a point at max height and the last point in the profile 85 | else: 86 | # top_coords.extend([Vector((r * cos(angle), r * sin(angle), 2 * height + height * loop)), Vector((r * cos(angle), r * sin(angle), z))]) 87 | top_coords.extend([Vector((r * cos(angle), r * sin(angle), z)), Vector((r * cos(angle), r * sin(angle), 2 * height + height * loop))]) 88 | 89 | 90 | # for each segment - starting with the second one - create the face indices 91 | if segment > 0: 92 | 93 | # create thread face indices, pcount - 1 rows of them 94 | for p in range(pcount - 1): 95 | indices.append([len(coords) + i + p for i in [-pcount * 2, -pcount, -pcount + 1, -pcount * 2 + 1]]) 96 | 97 | # create bottom face indices 98 | if loop == 0: 99 | if segment < segments: 100 | bottom_indices.append([len(bottom_coords) + i for i in [-4, -2, -1, -3]]) 101 | 102 | # the last face will have 5-7 verts, depending on h2 and h4 103 | else: 104 | bottom_indices.append([len(bottom_coords) + i for i in [-1 - pcount, -2 - pcount] + [i - pcount for i in range(pcount)]]) 105 | 106 | # create bottom face indices 107 | if loop == loops - 1: 108 | # the first face will have 5-7 verts, depending on h2 and h4 109 | if segment == 1: 110 | top_indices.append([len(top_coords) + i for i in [-2, -1] + [-3 - i for i in range(pcount)]]) 111 | else: 112 | top_indices.append([len(top_coords) + i for i in [-4, -2, -1, -3]]) 113 | 114 | return (coords, indices), (bottom_coords, bottom_indices), (top_coords, top_indices), height + height * loops 115 | -------------------------------------------------------------------------------- /utils/graph.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def build_mesh_graph(verts, edges, topo=True): 5 | mg = {} 6 | for v in verts: 7 | mg[v] = [] 8 | 9 | for e in edges: 10 | distance = 1 if topo else e.calc_length() 11 | 12 | mg[e.verts[0]].append((e.verts[1], distance)) 13 | mg[e.verts[1]].append((e.verts[0], distance)) 14 | 15 | return mg 16 | 17 | 18 | def get_shortest_path(bm, vstart, vend, topo=False, select=False): 19 | """ 20 | author: "G Bantle, Bagration, MACHIN3", 21 | source: "https://blenderartists.org/forum/showthread.php?58564-Path-Select-script(Update-20060307-Ported-to-C-now-in-CVS", 22 | video: https://www.youtube.com/watch?v=_lHSawdgXpI 23 | """ 24 | 25 | def dijkstra(mg, vstart, vend, topo=True): 26 | # initiate dict to collect distances from stat vert to every other vert 27 | d = dict.fromkeys(mg.keys(), sys.maxsize) 28 | 29 | # predecessor dict to track the path walked 30 | predecessor = dict.fromkeys(mg.keys()) 31 | 32 | # the distance of the start vert to itself is 0 33 | d[vstart] = 0 34 | 35 | # keep track of what verts are seen and add the the accumulated distances to those verts 36 | unknownverts = [(0, vstart)] 37 | 38 | # with topo you can exit as soon as you hit the vend, without topo you can't, because the shorter distance my involve more vert hops 39 | # while (topo and vstart != vend) or (not topo and unknownverts): 40 | while unknownverts: 41 | # sorting actually doesn't seem to be required, but it was in the original source 42 | # I question why the list needs to be sorted by index at all. also, doing it via the lambda function is very slow for some reason 43 | # unknownverts.sort(key=lambda x: x[1].index) 44 | 45 | # get the next vert that is closest to vstart 46 | dist, vcurrent = unknownverts[0] 47 | 48 | # use the mesh graph to retrieve the other verts connected 49 | others = mg[vcurrent] 50 | 51 | # choose the next vert with the shortest accumulated distance 52 | for vother, distance in others: 53 | if d[vother] > d[vcurrent] + distance: 54 | d[vother] = d[vcurrent] + distance 55 | 56 | unknownverts.append((d[vother], vother)) 57 | predecessor[vother] = vcurrent 58 | 59 | # we've finished exploring this vert, pop it 60 | unknownverts.pop(0) 61 | 62 | # you can break out early when determening the topological distances 63 | if topo and vcurrent == vend: 64 | break 65 | 66 | # backtrace from the end vertex using the predecessor dict 67 | path = [] 68 | endvert = vend 69 | 70 | while endvert is not None: 71 | path.append(endvert) 72 | endvert = predecessor[endvert] 73 | 74 | return reversed(path) 75 | 76 | def f7(seq): 77 | seen = set() 78 | seen_add = seen.add 79 | return [x for x in seq if not (x in seen or seen_add(x))] 80 | 81 | verts = [v for v in bm.verts] 82 | edges = [e for e in bm.edges] 83 | 84 | mg = build_mesh_graph(verts, edges, topo) 85 | 86 | # vert list, shortest dist from vstart to vend 87 | path = dijkstra(mg, vstart, vend, topo) 88 | 89 | # remove duplicates, keeps order, see https://stackoverflow.com/a/480227 90 | path = f7(path) 91 | 92 | # optionally select the path 93 | if select: 94 | for v in path: 95 | v.select = True 96 | 97 | return path 98 | -------------------------------------------------------------------------------- /utils/light.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def adjust_lights_for_rendering(mode='DECREASE'): 5 | divider = bpy.context.scene.M3.adjust_lights_on_render_divider 6 | 7 | for light in bpy.data.lights: 8 | if light.type == 'AREA': 9 | print("", light.name, light.energy, ' > ', light.energy / divider) 10 | 11 | if mode == 'DECREASE': 12 | light.energy /= divider 13 | 14 | elif mode == 'INCREASE': 15 | light.energy *= divider 16 | 17 | 18 | def get_area_light_poll(): 19 | return [obj for obj in bpy.data.objects if obj.type == 'LIGHT' and obj.data.type == 'AREA'] 20 | -------------------------------------------------------------------------------- /utils/material.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def get_last_node(mat): 4 | if mat.use_nodes: 5 | tree = mat.node_tree 6 | output = tree.nodes.get("Material Output") 7 | if output: 8 | surf = output.inputs.get("Surface") 9 | if surf: 10 | if surf.links: 11 | return surf.links[0].from_node 12 | 13 | 14 | def lighten_color(color, amount): 15 | def remap(value, new_low): 16 | old_range = (1 - 0) 17 | new_range = (1 - new_low) 18 | return (((value - 0) * new_range) / old_range) + new_low 19 | 20 | return tuple(remap(c, amount) for c in color) 21 | -------------------------------------------------------------------------------- /utils/math.py: -------------------------------------------------------------------------------- 1 | from mathutils import Matrix, Vector 2 | from math import log10, floor 3 | 4 | 5 | # VALUE 6 | 7 | def dynamic_format(value, decimal_offset=0): 8 | ''' 9 | see https://stackoverflow.com/questions/8011017/python-find-first-non-zero-digit-after-decimal-point 10 | and https://stackoverflow.com/questions/658763/how-to-suppress-scientific-notation-when-printing-float-values 11 | 12 | decimal offset adds additional decimal places 13 | 14 | return formated string 15 | ''' 16 | if round(value, 6) == 0: 17 | return '0' 18 | 19 | l10 = log10(abs(value)) 20 | f = floor(abs(l10)) 21 | 22 | if l10 < 0: 23 | precision = f + 1 + decimal_offset 24 | 25 | else: 26 | precision = decimal_offset 27 | return f"{'-' if value < 0 else ''}{abs(value):.{precision}f}" 28 | 29 | 30 | # VECTOR 31 | 32 | def get_center_between_points(point1, point2, center=0.5): 33 | return point1 + (point2 - point1) * center 34 | 35 | 36 | def get_center_between_verts(vert1, vert2, center=0.5): 37 | return get_center_between_points(vert1.co, vert2.co, center=center) 38 | 39 | 40 | def get_edge_normal(edge): 41 | return average_normals([f.normal for f in edge.link_faces]) 42 | 43 | 44 | def get_face_center(face, method='MEDIAN_WEIGHTED'): 45 | if method == 'BOUNDS': 46 | return face.calc_center_bounds() 47 | elif method == 'MEDIAN': 48 | return face.calc_center_median() 49 | elif method == 'MEDIAN_WEIGHTED': 50 | return face.calc_center_median_weighted() 51 | 52 | 53 | def average_locations(locationslist, size=3): 54 | avg = Vector.Fill(size) 55 | 56 | for n in locationslist: 57 | avg += n 58 | 59 | return avg / len(locationslist) 60 | 61 | 62 | def average_normals(normalslist): 63 | avg = Vector() 64 | 65 | for n in normalslist: 66 | avg += n 67 | 68 | return avg.normalized() 69 | 70 | 71 | # MATRIX 72 | 73 | def flatten_matrix(mx): 74 | dimension = len(mx) 75 | return [mx[j][i] for i in range(dimension) for j in range(dimension)] 76 | 77 | 78 | def compare_matrix(mx1, mx2, precision=4): 79 | ''' 80 | matrix comparison by rounding the individual values 81 | this is used for comparing cursor matrices, 82 | which if changed used set_cursor has the tendenciy to have float precission issues prevent proper comparison 83 | ''' 84 | 85 | round1 = [round(i, precision) for i in flatten_matrix(mx1)] 86 | round2 = [round(i, precision) for i in flatten_matrix(mx2)] 87 | return round1 == round2 88 | 89 | 90 | def get_loc_matrix(location): 91 | return Matrix.Translation(location) 92 | 93 | 94 | def get_rot_matrix(rotation): 95 | return rotation.to_matrix().to_4x4() 96 | 97 | 98 | def get_sca_matrix(scale): 99 | scale_mx = Matrix() 100 | for i in range(3): 101 | scale_mx[i][i] = scale[i] 102 | return scale_mx 103 | 104 | 105 | def create_rotation_matrix_from_vertex(obj, vert): 106 | ''' 107 | create world space rotation matrix from vertex 108 | supports loose vertices too 109 | ''' 110 | mx = obj.matrix_world 111 | 112 | # get the vertex normal in world space 113 | normal = mx.to_3x3() @ vert.normal 114 | 115 | # get binormal from longest linked edge 116 | if vert.link_edges: 117 | longest_edge = max([e for e in vert.link_edges], key=lambda x: x.calc_length()) 118 | binormal = (mx.to_3x3() @ (longest_edge.other_vert(vert).co - vert.co)).normalized() 119 | 120 | # the tangent is a simple cross product 121 | tangent = binormal.cross(normal).normalized() 122 | 123 | # recalculate the binormal, because it's not guarantieed the previous one is 90 degrees to the normal 124 | binormal = normal.cross(tangent).normalized() 125 | 126 | # without linked faces get a binormal from the objects up vector 127 | else: 128 | objup = (mx.to_3x3() @ Vector((0, 0, 1))).normalized() 129 | 130 | # use the x axis if the edge is already pointing in z 131 | dot = normal.dot(objup) 132 | if abs(round(dot, 6)) == 1: 133 | objup = (mx.to_3x3() @ Vector((1, 0, 0))).normalized() 134 | 135 | tangent = normal.cross(objup).normalized() 136 | binormal = normal.cross(tangent).normalized() 137 | 138 | # we want the normal, tangent and binormal to become Z, X and Y, in that order 139 | # see http://renderdan.blogspot.com/2006/05/rotation-matrix-from-axis-vectors.html 140 | rot = Matrix() 141 | rot[0].xyz = tangent 142 | rot[1].xyz = binormal 143 | rot[2].xyz = normal 144 | 145 | # transpose, because blender is column major 146 | return rot.transposed() 147 | 148 | 149 | def create_rotation_matrix_from_edge(obj, edge): 150 | ''' 151 | create world space rotation matrix from edge 152 | supports loose edges too 153 | ''' 154 | mx = obj.matrix_world 155 | 156 | # call the direction, the binormal, we want this to be the y axis at the end 157 | binormal = (mx.to_3x3() @ (edge.verts[1].co - edge.verts[0].co)).normalized() 158 | 159 | # get normal from linked faces 160 | if edge.link_faces: 161 | normal = (mx.to_3x3() @ get_edge_normal(edge)).normalized() 162 | tangent = binormal.cross(normal).normalized() 163 | 164 | # recalculate the normal, that's because the one calculated from the neighbouring faces may not actually be perpendicular to the binormal, if the faces are not planar 165 | normal = tangent.cross(binormal).normalized() 166 | 167 | # without linked faces get a normal from the objects up vector 168 | else: 169 | objup = (mx.to_3x3() @ Vector((0, 0, 1))).normalized() 170 | 171 | # use the x axis if the edge is already pointing in z 172 | dot = binormal.dot(objup) 173 | if abs(round(dot, 6)) == 1: 174 | objup = (mx.to_3x3() @ Vector((1, 0, 0))).normalized() 175 | 176 | tangent = (binormal.cross(objup)).normalized() 177 | normal = tangent.cross(binormal) 178 | 179 | # we want the normal, tangent and binormal to become Z, X and Y, in that order 180 | rotmx = Matrix() 181 | rotmx[0].xyz = tangent 182 | rotmx[1].xyz = binormal 183 | rotmx[2].xyz = normal 184 | 185 | # transpose, because blender is column major 186 | return rotmx.transposed() 187 | 188 | 189 | def create_rotation_matrix_from_face(mx, face): 190 | ''' 191 | create world space rotation matrix from face 192 | ''' 193 | 194 | # get the face normal in world space 195 | normal = (mx.to_3x3() @ face.normal).normalized() 196 | 197 | # tangent = (mx.to_3x3() @ face.calc_tangent_edge()).normalized() 198 | tangent = (mx.to_3x3() @ face.calc_tangent_edge_pair()).normalized() 199 | 200 | # the binormal is a simple cross product 201 | binormal = normal.cross(tangent) 202 | 203 | # we want the normal, tangent and binormal to become Z, X and Y, in that order 204 | rot = Matrix() 205 | rot[0].xyz = tangent 206 | rot[1].xyz = binormal 207 | rot[2].xyz = normal 208 | 209 | # transpose, because blender is column major 210 | return rot.transposed() 211 | 212 | 213 | def create_rotation_difference_matrix_from_quat(v1, v2): 214 | q = v1.rotation_difference(v2) 215 | return q.to_matrix().to_4x4() 216 | 217 | 218 | def create_selection_bbox(coords): 219 | minx = min(coords, key=lambda x: x[0]) 220 | maxx = max(coords, key=lambda x: x[0]) 221 | 222 | miny = min(coords, key=lambda x: x[1]) 223 | maxy = max(coords, key=lambda x: x[1]) 224 | 225 | minz = min(coords, key=lambda x: x[2]) 226 | maxz = max(coords, key=lambda x: x[2]) 227 | 228 | midx = get_center_between_points(minx, maxx) 229 | midy = get_center_between_points(miny, maxy) 230 | midz = get_center_between_points(minz, maxz) 231 | 232 | mid = Vector((midx[0], midy[1], midz[2])) 233 | 234 | bbox = [Vector((minx.x, miny.y, minz.z)), Vector((maxx.x, miny.y, minz.z)), 235 | Vector((maxx.x, maxy.y, minz.z)), Vector((minx.x, maxy.y, minz.z)), 236 | Vector((minx.x, miny.y, maxz.z)), Vector((maxx.x, miny.y, maxz.z)), 237 | Vector((maxx.x, maxy.y, maxz.z)), Vector((minx.x, maxy.y, maxz.z))] 238 | 239 | return bbox, mid 240 | 241 | 242 | def get_right_and_up_axes(context, mx): 243 | r3d = context.space_data.region_3d 244 | 245 | # get view right (and up) vectors in 3d space 246 | view_right = r3d.view_rotation @ Vector((1, 0, 0)) 247 | view_up = r3d.view_rotation @ Vector((0, 1, 0)) 248 | 249 | # get the right and up axes depending on the matrix that was passed in (object's local space, world space, etc) 250 | axes_right = [] 251 | axes_up = [] 252 | 253 | for idx, axis in enumerate([Vector((1, 0, 0)), Vector((0, 1, 0)), Vector((0, 0, 1))]): 254 | dot = view_right.dot(mx.to_3x3() @ axis) 255 | axes_right.append((dot, idx)) 256 | 257 | dot = view_up.dot(mx.to_3x3() @ axis) 258 | axes_up.append((dot, idx)) 259 | 260 | axis_right = max(axes_right, key=lambda x: abs(x[0])) 261 | axis_up = max(axes_up, key=lambda x: abs(x[0])) 262 | 263 | # determine flip 264 | flip_right = True if axis_right[0] < 0 else False 265 | flip_up = True if axis_up[0] < 0 else False 266 | 267 | return axis_right[1], axis_up[1], flip_right, flip_up 268 | -------------------------------------------------------------------------------- /utils/mesh.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from mathutils import Vector, Matrix 4 | import numpy as np 5 | 6 | 7 | def get_coords(mesh, mx=None, offset=0, indices=False): 8 | verts = mesh.vertices 9 | vert_count = len(verts) 10 | 11 | coords = np.empty((vert_count, 3), np.float) 12 | mesh.vertices.foreach_get('co', np.reshape(coords, vert_count * 3)) 13 | 14 | # offset along vertex normal 15 | if offset: 16 | normals = np.empty((vert_count, 3), np.float) 17 | mesh.vertices.foreach_get('normal', np.reshape(normals, vert_count * 3)) 18 | 19 | coords = coords + normals * offset 20 | 21 | # bring coords into non-local space 22 | if mx: 23 | coords_4d = np.ones((vert_count, 4), dtype=np.float) 24 | coords_4d[:, :-1] = coords 25 | 26 | coords = np.einsum('ij,aj->ai', mx, coords_4d)[:, :-1] 27 | 28 | # float64 seems faster to calculate than 32, + the einsum turns it into 64 anyway 29 | # but float32 is needed for gpu drawing, so convert 30 | coords = np.float32(coords) 31 | 32 | if indices: 33 | edges = mesh.edges 34 | edge_count = len(edges) 35 | 36 | indices = np.empty((edge_count, 2), 'i') 37 | edges.foreach_get('vertices', np.reshape(indices, edge_count * 2)) 38 | 39 | return coords, indices 40 | 41 | 42 | return coords 43 | 44 | 45 | # MESH 46 | 47 | def hide(mesh): 48 | mesh.polygons.foreach_set('hide', [True] * len(mesh.polygons)) 49 | mesh.edges.foreach_set('hide', [True] * len(mesh.edges)) 50 | mesh.vertices.foreach_set('hide', [True] * len(mesh.vertices)) 51 | 52 | mesh.update() 53 | 54 | 55 | def unhide(mesh): 56 | mesh.polygons.foreach_set('hide', [False] * len(mesh.polygons)) 57 | mesh.edges.foreach_set('hide', [False] * len(mesh.edges)) 58 | mesh.vertices.foreach_set('hide', [False] * len(mesh.vertices)) 59 | 60 | mesh.update() 61 | 62 | 63 | def unhide_select(mesh): 64 | polygons = len(mesh.polygons) 65 | edges = len(mesh.edges) 66 | vertices = len(mesh.vertices) 67 | 68 | mesh.polygons.foreach_set('hide', [False] * polygons) 69 | mesh.edges.foreach_set('hide', [False] * edges) 70 | mesh.vertices.foreach_set('hide', [False] * vertices) 71 | 72 | mesh.polygons.foreach_set('select', [True] * polygons) 73 | mesh.edges.foreach_set('select', [True] * edges) 74 | mesh.vertices.foreach_set('select', [True] * vertices) 75 | 76 | mesh.update() 77 | 78 | 79 | def unhide_deselect(mesh): 80 | polygons = len(mesh.polygons) 81 | edges = len(mesh.edges) 82 | vertices = len(mesh.vertices) 83 | 84 | mesh.polygons.foreach_set('hide', [False] * polygons) 85 | mesh.edges.foreach_set('hide', [False] * edges) 86 | mesh.vertices.foreach_set('hide', [False] * vertices) 87 | 88 | mesh.polygons.foreach_set('select', [False] * polygons) 89 | mesh.edges.foreach_set('select', [False] * edges) 90 | mesh.vertices.foreach_set('select', [False] * vertices) 91 | 92 | mesh.update() 93 | 94 | 95 | def select(mesh): 96 | mesh.polygons.foreach_set('select', [True] * len(mesh.polygons)) 97 | mesh.edges.foreach_set('select', [True] * len(mesh.edges)) 98 | mesh.vertices.foreach_set('select', [True] * len(mesh.vertices)) 99 | 100 | mesh.update() 101 | 102 | 103 | def deselect(mesh): 104 | mesh.polygons.foreach_set('select', [False] * len(mesh.polygons)) 105 | mesh.edges.foreach_set('select', [False] * len(mesh.edges)) 106 | mesh.vertices.foreach_set('select', [False] * len(mesh.vertices)) 107 | 108 | mesh.update() 109 | 110 | # BMESH 111 | 112 | def blast(mesh, prop, type): 113 | bm = bmesh.new() 114 | bm.from_mesh(mesh) 115 | bm.normal_update() 116 | bm.verts.ensure_lookup_table() 117 | 118 | if prop == "hidden": 119 | faces = [f for f in bm.faces if f.hide] 120 | 121 | elif prop == "visible": 122 | faces = [f for f in bm.faces if not f.hide] 123 | 124 | elif prop == "selected": 125 | faces = [f for f in bm.faces if f.select] 126 | 127 | bmesh.ops.delete(bm, geom=faces, context=type) 128 | 129 | bm.to_mesh(mesh) 130 | bm.clear() 131 | 132 | 133 | def smooth(mesh, smooth=True): 134 | bm = bmesh.new() 135 | bm.from_mesh(mesh) 136 | bm.normal_update() 137 | bm.verts.ensure_lookup_table() 138 | 139 | for f in bm.faces: 140 | f.smooth = smooth 141 | 142 | bm.to_mesh(mesh) 143 | bm.free() 144 | 145 | 146 | def flip_normals(mesh): 147 | bm = bmesh.new() 148 | bm.from_mesh(mesh) 149 | bm.normal_update() 150 | 151 | bmesh.ops.reverse_faces(bm, faces=bm.faces) 152 | bm.to_mesh(mesh) 153 | bm.free() 154 | 155 | 156 | def join(target, objects, select=[]): 157 | mxi = target.matrix_world.inverted_safe() 158 | 159 | bm = bmesh.new() 160 | bm.from_mesh(target.data) 161 | bm.normal_update() 162 | bm.verts.ensure_lookup_table() 163 | 164 | select_layer = bm.faces.layers.int.get('Machin3FaceSelect') 165 | 166 | if not select_layer: 167 | select_layer = bm.faces.layers.int.new('Machin3FaceSelect') 168 | 169 | if any([obj.data.use_auto_smooth for obj in objects]): 170 | target.data.use_auto_smooth = True 171 | 172 | for idx, obj in enumerate(objects): 173 | mesh = obj.data 174 | mx = obj.matrix_world 175 | mesh.transform(mxi @ mx) 176 | 177 | bmm = bmesh.new() 178 | bmm.from_mesh(mesh) 179 | bmm.normal_update() 180 | bmm.verts.ensure_lookup_table() 181 | 182 | obj_select_layer = bmm.faces.layers.int.get('Machin3FaceSelect') 183 | 184 | if not obj_select_layer: 185 | obj_select_layer = bmm.faces.layers.int.new('Machin3FaceSelect') 186 | 187 | for f in bmm.faces: 188 | f[obj_select_layer] = idx + 1 189 | 190 | bmm.to_mesh(mesh) 191 | bmm.free() 192 | 193 | bm.from_mesh(mesh) 194 | 195 | bpy.data.meshes.remove(mesh, do_unlink=True) 196 | 197 | if select: 198 | for f in bm.faces: 199 | if f[select_layer] in select: 200 | f.select_set(True) 201 | 202 | bm.to_mesh(target.data) 203 | bm.free() 204 | -------------------------------------------------------------------------------- /utils/modifier.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .. items import mirror_props 3 | 4 | 5 | # ADD 6 | 7 | def add_triangulate(obj): 8 | mod = obj.modifiers.new(name="Triangulate", type="TRIANGULATE") 9 | mod.keep_custom_normals = True 10 | mod.quad_method = 'FIXED' 11 | mod.show_expanded = True 12 | return mod 13 | 14 | 15 | def add_shrinkwrap(obj, target): 16 | mod = obj.modifiers.new(name="Shrinkwrap", type="SHRINKWRAP") 17 | 18 | mod.target = target 19 | mod.show_on_cage = True 20 | mod.show_expanded = False 21 | return mod 22 | 23 | 24 | def add_mods_from_dict(obj, modsdict): 25 | for name, props in modsdict.items(): 26 | mod = obj.modifiers.new(name=name, type=props['type']) 27 | 28 | for pname, pvalue in props.items(): 29 | if pname != 'type': 30 | setattr(mod, pname, pvalue) 31 | 32 | 33 | def add_bevel(obj, method='WEIGHT'): 34 | mod = obj.modifiers.new(name='Bevel', type='BEVEL') 35 | mod.limit_method = method 36 | 37 | mod.show_expanded = False 38 | return mod 39 | 40 | 41 | # REMOVE 42 | 43 | def remove_mod(modname, objtype='MESH', context=None, object=None): 44 | ''' 45 | remove modifier named modname 46 | optionaly with context override on an object that isn't active 47 | ''' 48 | 49 | if context and object: 50 | with context.temp_override(object=object): 51 | if objtype == 'GPENCIL': 52 | bpy.ops.object.gpencil_modifier_remove(modifier=modname) 53 | else: 54 | bpy.ops.object.modifier_remove(modifier=modname) 55 | 56 | else: 57 | if objtype == 'GPENCIL': 58 | bpy.ops.object.gpencil_modifier_remove(modifier=modname) 59 | else: 60 | bpy.ops.object.modifier_remove(modifier=modname) 61 | 62 | 63 | def remove_triangulate(obj): 64 | lastmod = obj.modifiers[-1] if obj.modifiers else None 65 | 66 | if lastmod and lastmod.type == 'TRIANGULATE': 67 | obj.modifiers.remove(lastmod) 68 | return True 69 | 70 | 71 | # DICT REPRESENTATION 72 | 73 | def get_mod_as_dict(mod, skip_show_expanded=False): 74 | d = {} 75 | 76 | if mod.type == 'MIRROR': 77 | for prop in mirror_props: 78 | if skip_show_expanded and prop == 'show_expanded': 79 | continue 80 | 81 | if prop in ['use_axis', 'use_bisect_axis', 'use_bisect_flip_axis']: 82 | d[prop] = tuple(getattr(mod, prop)) 83 | else: 84 | d[prop] = getattr(mod, prop) 85 | 86 | return d 87 | 88 | 89 | def get_mods_as_dict(obj, types=[], skip_show_expanded=False): 90 | mods = [] 91 | 92 | # get all mods or all mods of a type in types 93 | for mod in obj.modifiers: 94 | if types: 95 | if mod.type in types: 96 | mods.append(mod) 97 | 98 | else: 99 | mods.append(mod) 100 | 101 | modsdict = {} 102 | 103 | for mod in mods: 104 | modsdict[mod.name] = get_mod_as_dict(mod, skip_show_expanded=skip_show_expanded) 105 | 106 | return modsdict 107 | 108 | 109 | # APPLY 110 | 111 | def apply_mod(modname): 112 | bpy.ops.object.modifier_apply(modifier=modname) 113 | 114 | 115 | # MOD OBJECT 116 | 117 | def get_mod_obj(mod): 118 | if mod.type in ['BOOLEAN', 'HOOK', 'LATTICE', 'DATA_TRANSFER', 'GP_MIRROR']: 119 | return mod.object 120 | elif mod.type == 'MIRROR': 121 | return mod.mirror_object 122 | elif mod.type == 'ARRAY': 123 | return mod.offset_object 124 | -------------------------------------------------------------------------------- /utils/object.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from mathutils import Matrix, Vector 4 | from . math import flatten_matrix 5 | 6 | 7 | def parent(obj, parentobj): 8 | if obj.parent: 9 | unparent(obj) 10 | 11 | obj.parent = parentobj 12 | obj.matrix_parent_inverse = parentobj.matrix_world.inverted_safe() 13 | 14 | 15 | def unparent(obj): 16 | if obj.parent: 17 | omx = obj.matrix_world.copy() 18 | obj.parent = None 19 | obj.matrix_world = omx 20 | 21 | 22 | def unparent_children(obj): 23 | children = [] 24 | 25 | for c in obj.children: 26 | unparent(c) 27 | children.append(c) 28 | 29 | return children 30 | 31 | 32 | def compensate_children(obj, oldmx, newmx): 33 | ''' 34 | compensate object's childen, for instance, if obj's world matrix is about to be changed and "affect parents only" is enabled 35 | ''' 36 | 37 | # the delta matrix, aka the old mx expressed in the new one's local space 38 | deltamx = newmx.inverted_safe() @ oldmx 39 | children = [c for c in obj.children] 40 | 41 | for c in children: 42 | pmx = c.matrix_parent_inverse 43 | c.matrix_parent_inverse = deltamx @ pmx 44 | 45 | 46 | def flatten(obj, depsgraph=None): 47 | if not depsgraph: 48 | depsgraph = bpy.context.evaluated_depsgraph_get() 49 | 50 | oldmesh = obj.data 51 | 52 | obj.data = bpy.data.meshes.new_from_object(obj.evaluated_get(depsgraph)) 53 | obj.modifiers.clear() 54 | 55 | # remove the old mesh 56 | bpy.data.meshes.remove(oldmesh, do_unlink=True) 57 | 58 | 59 | def add_vgroup(obj, name="", ids=[], weight=1, debug=False): 60 | vgroup = obj.vertex_groups.new(name=name) 61 | 62 | if debug: 63 | print(" » Created new vertex group: %s" % (name)) 64 | 65 | if ids: 66 | vgroup.add(ids, weight, "ADD") 67 | 68 | # from selection 69 | else: 70 | obj.vertex_groups.active_index = vgroup.index 71 | bpy.ops.object.vertex_group_assign() 72 | 73 | return vgroup 74 | 75 | 76 | def add_facemap(obj, name="", ids=[]): 77 | fmap = obj.face_maps.new(name=name) 78 | 79 | if ids: 80 | fmap.add(ids) 81 | 82 | return fmap 83 | 84 | 85 | def set_obj_origin(obj, mx, bm=None, decalmachine=False, meshmachine=False): 86 | ''' 87 | change object origin to supplied matrix, support doing it in edit mode when bmesh is passed in 88 | also update decal backups and stashes if decalmachine or meshmachine are True 89 | ''' 90 | 91 | # pre-origin adjusted object matrix 92 | omx = obj.matrix_world.copy() 93 | 94 | # get children and compensate for the parent transform 95 | children = [c for c in obj.children] 96 | compensate_children(obj, omx, mx) 97 | 98 | # object mx expressed in new mx's local space, this is the "difference matrix" representing the origin change 99 | deltamx = mx.inverted_safe() @ obj.matrix_world 100 | 101 | obj.matrix_world = mx 102 | 103 | if bm: 104 | bmesh.ops.transform(bm, verts=bm.verts, matrix=deltamx) 105 | bmesh.update_edit_mesh(obj.data) 106 | else: 107 | obj.data.transform(deltamx) 108 | 109 | if obj.type == 'MESH': 110 | obj.data.update() 111 | 112 | # the decal origin needs to be chanegd too and the backupmx needs to be compensated for the change in parent object origin 113 | if decalmachine and children: 114 | 115 | # decal backup's backup matrices, but only for projected/sliced decals! 116 | for c in [c for c in children if c.DM.isdecal and c.DM.decalbackup]: 117 | backup = c.DM.decalbackup 118 | backup.DM.backupmx = flatten_matrix(deltamx @ backup.DM.backupmx) 119 | 120 | # adjust stashes and stash matrices 121 | if meshmachine: 122 | 123 | # the following originally immitated stash retrieval and then re-creation, it just chained both events together. this could then be simplifed further and further. setting stash.obj.matrix_world is optional 124 | for stash in obj.MM.stashes: 125 | 126 | # MEShmachine 0.7 uses a delta and orphan matrix 127 | if getattr(stash, 'version', False) and float('.'.join([v for v in stash.version.split('.')[:2]])) >= 0.7: 128 | stashdeltamx = stash.obj.MM.stashdeltamx 129 | 130 | # duplicate "instanced" stash objs, to prevent offsetting stashes on object's whose origin is not changed 131 | # NOTE: it seems this is only required for self stashes for some reason 132 | if stash.self_stash: 133 | if stash.obj.users > 2: 134 | print(f"INFO: Duplicating {stash.name}'s stashobj {stash.obj.name} as it's used by multiple stashes") 135 | 136 | dup = stash.obj.copy() 137 | dup.data = stash.obj.data.copy() 138 | stash.obj = dup 139 | 140 | stash.obj.MM.stashdeltamx = flatten_matrix(deltamx @ stashdeltamx) 141 | stash.obj.MM.stashorphanmx = flatten_matrix(mx) 142 | 143 | # for self stashes, cange the stash obj origin in the same way as it was chaged for the main object 144 | # NOTE: this seems to work, it properly changes the origin of the stash object in the same way 145 | # ####: however the stash is drawn in and retrieved in the wrong location, in the pre-origin change location 146 | # ####: you can then align it properly, but why would it not be drawing and retrieved properly?? 147 | 148 | # if stash.self_stash: 149 | # stash.obj.matrix_world = mx 150 | # stash.obj.data.transform(deltamx) 151 | # stash.obj.data.update() 152 | 153 | # just disable self_stashes until you get this sorted 154 | stash.self_stash = False 155 | 156 | # older versions use the stashmx and targetmx 157 | else: 158 | # stashmx in stashtargetmx's local space, aka the stash difference matrix(which is all that's actually needed for stashes, just like for decal backups) 159 | stashdeltamx = stash.obj.MM.stashtargetmx.inverted_safe() @ stash.obj.MM.stashmx 160 | 161 | stash.obj.MM.stashmx = flatten_matrix(omx @ stashdeltamx) 162 | stash.obj.MM.stashtargetmx = flatten_matrix(mx) 163 | 164 | stash.obj.data.transform(deltamx) 165 | stash.obj.matrix_world = mx 166 | 167 | 168 | def get_eval_bbox(obj): 169 | return [Vector(co) for co in obj.bound_box] 170 | -------------------------------------------------------------------------------- /utils/property.py: -------------------------------------------------------------------------------- 1 | def step_list(current, list, step, loop=True): 2 | item_idx = list.index(current) 3 | 4 | step_idx = item_idx + step 5 | 6 | if step_idx >= len(list): 7 | if loop: 8 | step_idx = 0 9 | else: 10 | step_idx = len(list) - 1 11 | 12 | elif step_idx < 0: 13 | if loop: 14 | step_idx = len(list) - 1 15 | else: 16 | step_idx = 0 17 | 18 | return list[step_idx] 19 | 20 | 21 | def step_enum(current, items, step, loop=True): 22 | item_list = [item[0] for item in items] 23 | item_idx = item_list.index(current) 24 | 25 | step_idx = item_idx + step 26 | 27 | if step_idx >= len(item_list): 28 | if loop: 29 | step_idx = 0 30 | else: 31 | step_idx = len(item_list) - 1 32 | elif step_idx < 0: 33 | if loop: 34 | step_idx = len(item_list) - 1 35 | else: 36 | step_idx = 0 37 | 38 | return item_list[step_idx] 39 | 40 | 41 | def step_collection(object, currentitem, itemsname, indexname, step): 42 | item_list = [item for item in getattr(object, itemsname)] 43 | item_idx = item_list.index(currentitem) 44 | 45 | step_idx = item_idx + step 46 | 47 | if step_idx >= len(item_list): 48 | # step_idx = 0 49 | step_idx = len(item_list) - 1 50 | elif step_idx < 0: 51 | # step_idx = len(item_list) - 1 52 | step_idx = 0 53 | 54 | setattr(object, indexname, step_idx) 55 | 56 | return getattr(object, itemsname)[step_idx] 57 | -------------------------------------------------------------------------------- /utils/raycast.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy_extras.view3d_utils import region_2d_to_origin_3d, region_2d_to_vector_3d 3 | import bmesh 4 | from mathutils.bvhtree import BVHTree as BVH 5 | import sys 6 | 7 | 8 | # RAYCASTING BVH 9 | 10 | def cast_bvh_ray_from_mouse(mousepos, candidates=None, bmeshes={}, bvhs={}, debug=False): 11 | region = bpy.context.region 12 | region_data = bpy.context.region_data 13 | 14 | origin_3d = region_2d_to_origin_3d(region, region_data, mousepos) 15 | vector_3d = region_2d_to_vector_3d(region, region_data, mousepos) 16 | 17 | objects = [(obj, None) for obj in candidates if obj.type == "MESH"] 18 | 19 | hitobj = None 20 | hitlocation = None 21 | hitnormal = None 22 | hitindex = None 23 | hitdistance = sys.maxsize 24 | 25 | cache = {'bmesh': {}, 26 | 'bvh': {}} 27 | 28 | for obj, src in objects: 29 | mx = obj.matrix_world 30 | mxi = mx.inverted_safe() 31 | 32 | ray_origin = mxi @ origin_3d 33 | ray_direction = mxi.to_3x3() @ vector_3d 34 | 35 | # use cached bmesh if possible 36 | if obj.name in bmeshes: 37 | # print("fetching existing bmesh") 38 | bm = bmeshes[obj.name] 39 | else: 40 | # print("creating new bmesh") 41 | bm = bmesh.new() 42 | bm.from_mesh(obj.data) 43 | cache['bmesh'][obj.name] = bm 44 | 45 | # use cached bvh if possible 46 | if obj.name in bvhs: 47 | # print("fetching exsiting BVH") 48 | bvh = bvhs[obj.name] 49 | else: 50 | # print("creating new BVH") 51 | bvh = BVH.FromBMesh(bm) 52 | cache['bvh'][obj.name] = bvh 53 | 54 | location, normal, index, distance = bvh.ray_cast(ray_origin, ray_direction) 55 | 56 | # recalculate distance in worldspace 57 | if distance: 58 | distance = (mx @ location - origin_3d).length 59 | 60 | if debug: 61 | print("candidate:", obj.name, location, normal, index, distance) 62 | 63 | if distance and distance < hitdistance: 64 | hitobj, hitlocation, hitnormal, hitindex, hitdistance = obj, mx @ location, mx.to_3x3() @ normal, index, distance 65 | 66 | 67 | if debug: 68 | print("best hit:", hitobj.name if hitobj else None, hitlocation, hitnormal, hitindex, hitdistance if hitobj else None) 69 | print() 70 | 71 | if hitobj: 72 | return hitobj, hitlocation, hitnormal, hitindex, hitdistance, cache 73 | 74 | # the cache is always returned! 75 | return None, None, None, None, None, cache 76 | 77 | 78 | # RAYCASTING OBJ 79 | 80 | def cast_obj_ray_from_mouse(mousepos, depsgraph=None, candidates=None, debug=False): 81 | region = bpy.context.region 82 | region_data = bpy.context.region_data 83 | 84 | origin_3d = region_2d_to_origin_3d(region, region_data, mousepos) 85 | vector_3d = region_2d_to_vector_3d(region, region_data, mousepos) 86 | 87 | # get candidate objects, that could be hit 88 | if not candidates: 89 | candidates = bpy.context.visible_objects 90 | 91 | objects = [obj for obj in candidates if obj.type == "MESH"] 92 | 93 | hitobj = None 94 | hitobj_eval = None 95 | hitlocation = None 96 | hitnormal = None 97 | hitindex = None 98 | hitdistance = sys.maxsize 99 | 100 | for obj in objects: 101 | mx = obj.matrix_world 102 | mxi = mx.inverted_safe() 103 | 104 | ray_origin = mxi @ origin_3d 105 | ray_direction = mxi.to_3x3() @ vector_3d 106 | 107 | success, location, normal, index = obj.ray_cast(origin=ray_origin, direction=ray_direction, depsgraph=depsgraph) 108 | distance = (mx @ location - origin_3d).length 109 | 110 | if debug: 111 | print("candidate:", success, obj.name, location, normal, index, distance) 112 | 113 | if success and distance < hitdistance: 114 | hitobj, hitobj_eval, hitlocation, hitnormal, hitindex, hitdistance = obj, obj.evaluated_get(depsgraph) if depsgraph else None, mx @ location, mx.to_3x3() @ normal, index, distance 115 | 116 | if debug: 117 | print("best hit:", hitobj.name if hitobj else None, hitlocation, hitnormal, hitindex, hitdistance if hitobj else None) 118 | print() 119 | 120 | if hitobj: 121 | return hitobj, hitobj_eval, hitlocation, hitnormal, hitindex, hitdistance 122 | 123 | return None, None, None, None, None, None 124 | 125 | 126 | # CLOSEST POINT ON MESH 127 | 128 | def get_closest(origin, candidates=[], depsgraph=None, debug=False): 129 | nearestobj = None 130 | nearestlocation = None 131 | nearestnormal = None 132 | nearestindex = None 133 | nearestdistance = sys.maxsize 134 | 135 | if not candidates: 136 | candidates = bpy.context.visible_objects 137 | 138 | objects = [obj for obj in candidates if obj.type == 'MESH'] 139 | 140 | for obj in objects: 141 | mx = obj.matrix_world 142 | 143 | origin_local = mx.inverted_safe() @ origin 144 | 145 | # as a safety meassure, only get the closets when the evaluated mesh actually has faces 146 | obj_eval = obj.evaluated_get(depsgraph) 147 | 148 | if obj_eval.data.polygons: 149 | # location, normal, index, distance = bvh.find_nearest(origin_local) 150 | success, location, normal, index = obj.closest_point_on_mesh(origin_local, depsgraph=depsgraph) 151 | 152 | # NOTE: should this be run on the evaluated mesh instead? see crash/freeze issues in create_panel_decal_from_edges() in EPanel() 153 | # success, location, normal, index = target_eval.closest_point_on_mesh(origin_local) 154 | 155 | distance = (mx @ location - origin).length if success else sys.maxsize 156 | 157 | if debug: 158 | print("candidate:", success, obj, location, normal, index, distance) 159 | 160 | if distance is not None and distance < nearestdistance: 161 | nearestobj, nearestlocation, nearestnormal, nearestindex, nearestdistance = obj, mx @ location, mx.to_3x3() @ normal, index, distance 162 | 163 | elif debug: 164 | print("candidate:", "%s's evaluated mesh contains no faces" % (obj)) 165 | 166 | 167 | if debug: 168 | print("best hit:", nearestobj, nearestlocation, nearestnormal, nearestindex, nearestdistance) 169 | 170 | if nearestobj: 171 | return nearestobj, nearestobj.evaluated_get(depsgraph), nearestlocation, nearestnormal, nearestindex, nearestdistance 172 | 173 | return None, None, None, None, None, None 174 | 175 | 176 | # SCENE RAYCASTING 177 | 178 | def cast_scene_ray_from_mouse(mousepos, depsgraph, exclude=[], exclude_wire=False, unhide=[], debug=False): 179 | region = bpy.context.region 180 | region_data = bpy.context.region_data 181 | 182 | view_origin = region_2d_to_origin_3d(region, region_data, mousepos) 183 | view_dir = region_2d_to_vector_3d(region, region_data, mousepos) 184 | 185 | scene = bpy.context.scene 186 | 187 | # temporary unhide obects in the unhide list, usefuly if you want to self.snap edit mesh objects, which is achieved by excluding the active object and snapping on an unchanging duplicate that is hidden 188 | for ob in unhide: 189 | ob.hide_set(False) 190 | 191 | # initial cast 192 | hit, location, normal, index, obj, mx = scene.ray_cast(depsgraph=depsgraph, origin=view_origin, direction=view_dir) 193 | 194 | 195 | # objects are excluded by temporary hiding them, collect them to reveal them at the end 196 | hidden = [] 197 | 198 | # additional casts in case the hit object should be excluded 199 | if hit: 200 | if obj in exclude or (exclude_wire and obj.display_type == 'WIRE'): 201 | ignore = True 202 | 203 | while ignore: 204 | if debug: 205 | print(" Ignoring object", obj.name) 206 | 207 | # temporarily hide and collect excluded object 208 | obj.hide_set(True) 209 | hidden.append(obj) 210 | 211 | hit, location, normal, index, obj, mx = scene.ray_cast(depsgraph=depsgraph, origin=view_origin, direction=view_dir) 212 | 213 | if hit: 214 | ignore = obj in exclude or (exclude_wire and obj.display_type == 'WIRE') 215 | else: 216 | break 217 | 218 | # hide the unhide objects again 219 | for ob in unhide: 220 | ob.hide_set(True) 221 | 222 | # reveal hidden objects again 223 | for ob in hidden: 224 | ob.hide_set(False) 225 | 226 | if hit: 227 | if debug: 228 | print(obj.name, index, location, normal) 229 | 230 | return hit, obj, index, location, normal, mx 231 | 232 | else: 233 | if debug: 234 | print(None) 235 | 236 | return None, None, None, None, None, None 237 | -------------------------------------------------------------------------------- /utils/scene.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Vector, Quaternion 3 | 4 | 5 | def set_cursor(matrix=None, location=Vector(), rotation=Quaternion()): 6 | ''' 7 | NOTE: setting the cursor rotation will result in tiny float imprecision issues and so the cursor matrix will differ very slightly afterwards 8 | ''' 9 | 10 | cursor = bpy.context.scene.cursor 11 | 12 | # setting cursor.matrix has no effect unless a selection event occurs, so set location and rotation individually 13 | if matrix: 14 | cursor.location = matrix.to_translation() 15 | cursor.rotation_quaternion = matrix.to_quaternion() 16 | cursor.rotation_mode = 'QUATERNION' 17 | 18 | # use passed in location and rotation 19 | else: 20 | cursor.location = location 21 | 22 | if cursor.rotation_mode == 'QUATERNION': 23 | cursor.rotation_quaternion = rotation 24 | 25 | elif cursor.rotation_mode == 'AXIS_ANGLE': 26 | cursor.rotation_axis_angle = rotation.to_axis_angle() 27 | 28 | else: 29 | cursor.rotation_euler = rotation.to_euler(cursor.rotation_mode) 30 | -------------------------------------------------------------------------------- /utils/selection.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # SORTING 4 | 5 | def get_selected_vert_sequences(verts, ensure_seq_len=False, debug=False): 6 | ''' 7 | return sorted lists of vertices, where vertices are considered connected if their edges are selected, and faces are not selected 8 | ''' 9 | 10 | sequences = [] 11 | 12 | # if edge loops are non-cyclic, it matters at what vert you start the sorting 13 | noncyclicstartverts = [v for v in verts if len([e for e in v.link_edges if e.select]) == 1] 14 | 15 | if noncyclicstartverts: 16 | v = noncyclicstartverts[0] 17 | 18 | # in cyclic edge loops, any vert works 19 | else: 20 | v = verts[0] 21 | 22 | seq = [] 23 | 24 | while verts: 25 | seq.append(v) 26 | 27 | # safty precaution,for EPanel, where people may select intersecting edge loops 28 | if v not in verts: 29 | break 30 | 31 | else: 32 | verts.remove(v) 33 | 34 | if v in noncyclicstartverts: 35 | noncyclicstartverts.remove(v) 36 | 37 | nextv = [e.other_vert(v) for e in v.link_edges if e.select and e.other_vert(v) not in seq] 38 | 39 | # next vert in sequence 40 | if nextv: 41 | v = nextv[0] 42 | 43 | # finished a sequence 44 | else: 45 | # determine cyclicity 46 | cyclic = True if len([e for e in v.link_edges if e.select]) == 2 else False 47 | 48 | # store sequence and cyclicity 49 | sequences.append((seq, cyclic)) 50 | 51 | # start a new sequence, if there are still verts left 52 | if verts: 53 | if noncyclicstartverts: 54 | v = noncyclicstartverts[0] 55 | else: 56 | v = verts[0] 57 | 58 | seq = [] 59 | 60 | # again for EPanel, make sure sequences are longer than one vert 61 | if ensure_seq_len: 62 | seqs = [] 63 | 64 | for seq, cyclic in sequences: 65 | if len(seq) > 1: 66 | seqs.append((seq, cyclic)) 67 | 68 | sequences = seqs 69 | 70 | if debug: 71 | for seq, cyclic in sequences: 72 | print(cyclic, [v.index for v in seq]) 73 | 74 | return sequences 75 | 76 | 77 | def get_edges_vert_sequences(verts, edges, debug=False): 78 | """ 79 | return sorted lists of vertices, where vertices are considered connected if they are verts of the passed in edges 80 | selection states are completely ignored. 81 | """ 82 | sequences = [] 83 | 84 | # if edge loops are non-cyclic, it matters at what vert you start the sorting 85 | noncyclicstartverts = [v for v in verts if len([e for e in v.link_edges if e in edges]) == 1] 86 | 87 | if noncyclicstartverts: 88 | v = noncyclicstartverts[0] 89 | 90 | # in cyclic edge loops, any vert works 91 | else: 92 | v = verts[0] 93 | 94 | seq = [] 95 | 96 | while verts: 97 | seq.append(v) 98 | verts.remove(v) 99 | 100 | if v in noncyclicstartverts: 101 | noncyclicstartverts.remove(v) 102 | 103 | nextv = [e.other_vert(v) for e in v.link_edges if e in edges and e.other_vert(v) not in seq] 104 | 105 | # next vert in sequence 106 | if nextv: 107 | v = nextv[0] 108 | 109 | # finished a sequence 110 | else: 111 | # determine cyclicity 112 | cyclic = True if len([e for e in v.link_edges if e in edges]) == 2 else False 113 | 114 | # store sequence and cyclicity 115 | sequences.append((seq, cyclic)) 116 | 117 | # start a new sequence, if there are still verts left 118 | if verts: 119 | if noncyclicstartverts: 120 | v = noncyclicstartverts[0] 121 | else: 122 | v = verts[0] 123 | 124 | seq = [] 125 | 126 | if debug: 127 | for verts, cyclic in sequences: 128 | print(cyclic, [v.index for v in verts]) 129 | 130 | return sequences 131 | 132 | 133 | # REGIONS 134 | 135 | def get_selection_islands(faces, debug=False): 136 | ''' 137 | return island tuples (verts, edges, faces), sorted by amount of faces in each, highest first 138 | ''' 139 | 140 | if debug: 141 | print("selected:", [f.index for f in faces]) 142 | 143 | face_islands = [] 144 | 145 | while faces: 146 | island = [faces[0]] 147 | foundmore = [faces[0]] 148 | 149 | if debug: 150 | print("island:", [f.index for f in island]) 151 | print("foundmore:", [f.index for f in foundmore]) 152 | 153 | while foundmore: 154 | for e in foundmore[0].edges: 155 | # get unseen selected border faces 156 | bf = [f for f in e.link_faces if f.select and f not in island] 157 | if bf: 158 | island.append(bf[0]) 159 | foundmore.append(bf[0]) 160 | 161 | if debug: 162 | print("popping", foundmore[0].index) 163 | 164 | foundmore.pop(0) 165 | 166 | face_islands.append(island) 167 | 168 | for f in island: 169 | faces.remove(f) 170 | 171 | if debug: 172 | print() 173 | for idx, island in enumerate(face_islands): 174 | print("island:", idx) 175 | print(" » ", ", ".join([str(f.index) for f in island])) 176 | 177 | 178 | islands = [] 179 | 180 | for fi in face_islands: 181 | vi = set() 182 | ei = set() 183 | 184 | for f in fi: 185 | vi.update(f.verts) 186 | ei.update(f.edges) 187 | 188 | # f.select = False 189 | 190 | islands.append((list(vi), list(ei), fi)) 191 | 192 | return sorted(islands, key=lambda x: len(x[2]), reverse=True) 193 | 194 | 195 | def get_boundary_edges(faces, region_to_loop=False): 196 | """ 197 | return boundary edges of selected faces 198 | as boundary, non-manifold edges, as well as edges, haveing any unselected face 199 | this is faster than mesh.region_to_loop() btw, even with region_to_loop True 200 | """ 201 | 202 | boundary_edges = [e for f in faces for e in f.edges if (not e.is_manifold) or (any(not f.select for f in e.link_faces))] 203 | 204 | if region_to_loop: 205 | for f in faces: 206 | f.select_set(False) 207 | 208 | for e in boundary_edges: 209 | e.select_set(True) 210 | 211 | return boundary_edges 212 | -------------------------------------------------------------------------------- /utils/system.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | import sys 4 | import re 5 | from pprint import pprint 6 | 7 | 8 | enc = sys.getfilesystemencoding() 9 | 10 | 11 | def abspath(path): 12 | return os.path.abspath(bpy.path.abspath(path)) 13 | 14 | 15 | def quotepath(path): 16 | if " " in path: 17 | path = '"%s"' % (path) 18 | return path 19 | 20 | 21 | def add_path_to_recent_files(path): 22 | ''' 23 | add the path to the recent files list, for some reason it's not done automatically when saving or loading 24 | ''' 25 | 26 | try: 27 | recent_path = bpy.utils.user_resource('CONFIG', path="recent-files.txt") 28 | with open(recent_path, "r+", encoding=enc) as f: 29 | content = f.read() 30 | f.seek(0, 0) 31 | f.write(path.rstrip('\r\n') + '\n' + content) 32 | 33 | except (IOError, OSError, FileNotFoundError): 34 | pass 35 | 36 | 37 | def open_folder(path): 38 | import platform 39 | import subprocess 40 | 41 | if platform.system() == "Windows": 42 | os.startfile(path) 43 | elif platform.system() == "Darwin": 44 | subprocess.Popen(["open", path]) 45 | else: 46 | # subprocess.Popen(["xdg-open", path]) 47 | os.system('xdg-open "%s" %s &' % (path, "> /dev/null 2> /dev/null")) # > sends stdout, 2> sends stderr 48 | 49 | 50 | def makedir(pathstring): 51 | if not os.path.exists(pathstring): 52 | os.makedirs(pathstring) 53 | return pathstring 54 | 55 | 56 | def printd(d, name=''): 57 | print(f"\n{name}") 58 | pprint(d, sort_dicts=False) 59 | 60 | 61 | def get_incremented_paths(currentblend): 62 | path = os.path.dirname(currentblend) 63 | filename = os.path.basename(currentblend) 64 | 65 | filenameRegex = re.compile(r"(.+)\.blend\d*$") 66 | 67 | mo = filenameRegex.match(filename) 68 | 69 | if mo: 70 | name = mo.group(1) 71 | numberendRegex = re.compile(r"(.*?)(\d+)$") 72 | 73 | mo = numberendRegex.match(name) 74 | 75 | if mo: 76 | basename = mo.group(1) 77 | numberstr = mo.group(2) 78 | else: 79 | basename = name + "_" 80 | numberstr = "000" 81 | 82 | number = int(numberstr) 83 | 84 | incr = number + 1 85 | incrstr = str(incr).zfill(len(numberstr)) 86 | 87 | incrname = basename + incrstr + ".blend" 88 | 89 | return os.path.join(path, incrname), os.path.join(path, name + '_01.blend') 90 | -------------------------------------------------------------------------------- /utils/tools.py: -------------------------------------------------------------------------------- 1 | from bl_ui.space_toolsystem_toolbar import VIEW3D_PT_tools_active as view3d_tools 2 | from .. items import tool_name_mapping_dict 3 | 4 | 5 | def get_tools_from_context(context): 6 | tools = {} 7 | 8 | active = view3d_tools.tool_active_from_context(context) 9 | 10 | for tool in view3d_tools.tools_from_context(context): 11 | if tool: 12 | 13 | # tuple tool 14 | if type(tool) is tuple: 15 | for subtool in tool: 16 | tools[subtool.idname] = {'label': subtool.label, 17 | 'icon_value': view3d_tools._icon_value_from_icon_handle(subtool.icon), 18 | 'active': subtool.idname == active.idname} 19 | 20 | # single tool 21 | else: 22 | tools[tool.idname] = {'label': tool.label, 23 | 'icon_value': view3d_tools._icon_value_from_icon_handle(tool.icon), 24 | 'active': tool.idname == active.idname} 25 | 26 | return tools 27 | 28 | 29 | def get_active_tool(context): 30 | return view3d_tools.tool_active_from_context(context) 31 | 32 | 33 | def get_tool_options(context, tool_idname, operator_idname): 34 | for tooldef in context.workspace.tools: 35 | if tooldef and tooldef.idname == tool_idname: 36 | if tooldef.mode == context.mode: 37 | try: 38 | return tooldef.operator_properties(operator_idname) 39 | except: 40 | return None 41 | 42 | def prettify_tool_name(name): 43 | if name in tool_name_mapping_dict: 44 | return tool_name_mapping_dict[name] 45 | return name 46 | -------------------------------------------------------------------------------- /utils/view.py: -------------------------------------------------------------------------------- 1 | from mathutils import Matrix, Vector 2 | from bpy_extras.view3d_utils import location_3d_to_region_2d 3 | 4 | 5 | def set_xray(context): 6 | x = (context.scene.M3.pass_through, context.scene.M3.show_edit_mesh_wire) 7 | shading = context.space_data.shading 8 | 9 | shading.show_xray = True if any(x) else False 10 | 11 | if context.scene.M3.show_edit_mesh_wire: 12 | shading.xray_alpha = 0.1 13 | 14 | elif context.scene.M3.pass_through: 15 | shading.xray_alpha = 1 if context.active_object and context.active_object.type == "MESH" else 0.5 16 | 17 | 18 | def reset_xray(context): 19 | shading = context.space_data.shading 20 | 21 | shading.show_xray = False 22 | shading.xray_alpha = 0.5 23 | 24 | 25 | def update_local_view(space_data, states): 26 | """ 27 | states: list of (obj, bool) tuples, True being in local view, False being out 28 | """ 29 | if space_data.local_view: 30 | for obj, local in states: 31 | obj.local_view_set(space_data, local) 32 | 33 | 34 | def reset_viewport(context, disable_toolbar=False): 35 | for screen in context.workspace.screens: 36 | for area in screen.areas: 37 | if area.type == 'VIEW_3D': 38 | for space in area.spaces: 39 | if space.type == 'VIEW_3D': 40 | r3d = space.region_3d 41 | 42 | # it seems to be important to set the view distance first, to get the correct viewport rotation focus 43 | r3d.view_distance = 10 44 | r3d.view_matrix = Matrix(((1, 0, 0, 0), 45 | (0, 0.2, 1, -1), 46 | (0, -1, 0.2, -10), 47 | (0, 0, 0, 1))) 48 | 49 | if disable_toolbar: 50 | space.show_region_toolbar = False 51 | 52 | 53 | def sync_light_visibility(scene): 54 | ''' 55 | set light's hide_render prop based on light's hide_get() 56 | ''' 57 | 58 | # print("syncing light visibility/renderability") 59 | 60 | for view_layer in scene.view_layers: 61 | lights = [obj for obj in view_layer.objects if obj.type == 'LIGHT'] 62 | 63 | for light in lights: 64 | hidden = light.hide_get(view_layer=view_layer) 65 | 66 | if light.hide_render != hidden: 67 | light.hide_render = hidden 68 | 69 | 70 | def get_loc_2d(context, loc): 71 | ''' 72 | project 3d location into 2d space 73 | ''' 74 | 75 | loc_2d = location_3d_to_region_2d(context.region, context.region_data, loc) 76 | return loc_2d if loc_2d else Vector((-1000, -1000)) 77 | -------------------------------------------------------------------------------- /utils/world.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def get_world_output(world): 4 | if not world.use_nodes: 5 | world.use_nodes = True 6 | 7 | output = world.node_tree.nodes.get('World Outputs') 8 | 9 | # fallback 10 | if not output: 11 | for node in world.node_tree.nodes: 12 | if node.type == 'OUTPUT_WORLD': 13 | return node 14 | return output 15 | --------------------------------------------------------------------------------