diff options
author | Ryan Inch <mythologylover75@gmail.com> | 2019-08-31 07:04:43 +0300 |
---|---|---|
committer | Ryan Inch <mythologylover75@gmail.com> | 2019-08-31 07:04:43 +0300 |
commit | c63bb5abce16b85dd0f6888b0114c65092747e7a (patch) | |
tree | cf86e4fb884894b37fdf558b5802cbab8d9cddc9 /space_view3d_brush_menus | |
parent | 9f3bea45726e50b01491596993779e1e05306b99 (diff) |
Dynamic Brush Menus: return to release: T68350 T63750 437e41a06681
Diffstat (limited to 'space_view3d_brush_menus')
-rw-r--r-- | space_view3d_brush_menus/__init__.py | 135 | ||||
-rw-r--r-- | space_view3d_brush_menus/brush_menu.py | 630 | ||||
-rw-r--r-- | space_view3d_brush_menus/brushes.py | 163 | ||||
-rw-r--r-- | space_view3d_brush_menus/curve_menu.py | 88 | ||||
-rw-r--r-- | space_view3d_brush_menus/dyntopo_menu.py | 172 | ||||
-rw-r--r-- | space_view3d_brush_menus/stroke_menu.py | 200 | ||||
-rw-r--r-- | space_view3d_brush_menus/symmetry_menu.py | 80 | ||||
-rw-r--r-- | space_view3d_brush_menus/texture_menu.py | 415 | ||||
-rw-r--r-- | space_view3d_brush_menus/utils_core.py | 105 |
9 files changed, 1988 insertions, 0 deletions
diff --git a/space_view3d_brush_menus/__init__.py b/space_view3d_brush_menus/__init__.py new file mode 100644 index 00000000..5381d14c --- /dev/null +++ b/space_view3d_brush_menus/__init__.py @@ -0,0 +1,135 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### +# Modified by Meta-Androcto + +""" Copyright 2011 GPL licence applies""" + +bl_info = { + "name": "Dynamic Brush Menus", + "description": "Fast access to brushes & tools in Sculpt and Paint Modes", + "author": "Ryan Inch (Imaginer)", + "version": (1, 1, 7), + "blender": (2, 80, 0), + "location": "Spacebar in Sculpt/Paint Modes", + "warning": '', + "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/" + "Scripts/3D_interaction/Advanced_UI_Menus", + "category": "3D View"} + + +if "bpy" in locals(): + import importlib + importlib.reload(utils_core) + importlib.reload(brush_menu) + importlib.reload(brushes) + importlib.reload(curve_menu) + importlib.reload(dyntopo_menu) + importlib.reload(stroke_menu) + importlib.reload(symmetry_menu) + importlib.reload(texture_menu) +else: + from . import utils_core + from . import brush_menu + from . import brushes + from . import curve_menu + from . import dyntopo_menu + from . import stroke_menu + from . import symmetry_menu + from . import texture_menu + + +import bpy +from bpy.types import AddonPreferences +from bpy.props import ( + EnumProperty, + IntProperty, + ) + + +addon_files = ( + brush_menu, + brushes, + curve_menu, + dyntopo_menu, + stroke_menu, + symmetry_menu, + texture_menu, + ) + + + +class VIEW3D_MT_Brushes_Pref(AddonPreferences): + bl_idname = __name__ + + column_set: IntProperty( + name="Number of Columns", + description="Number of columns used for the brushes menu", + default=2, + min=1, + max=10 + ) + + def draw(self, context): + layout = self.layout + + col = layout.column(align=True) + col.prop(self, "column_set", slider=True) + + +# New hotkeys and registration + +addon_keymaps = [] + + +def register(): + # register all files + for addon_file in addon_files: + addon_file.register() + + # set the add-on name variable to access the preferences + utils_core.get_addon_name = __name__ + + # register preferences + bpy.utils.register_class(VIEW3D_MT_Brushes_Pref) + + # register hotkeys + wm = bpy.context.window_manager + modes = ('Sculpt', 'Vertex Paint', 'Weight Paint', 'Image Paint', 'Particle') + + for mode in modes: + km = wm.keyconfigs.addon.keymaps.new(name=mode) + kmi = km.keymap_items.new('wm.call_menu', 'SPACE', 'PRESS') + kmi.properties.name = "VIEW3D_MT_sv3_brush_options" + addon_keymaps.append((km, kmi)) + + +def unregister(): + # unregister all files + for addon_file in addon_files: + addon_file.unregister() + + # unregister preferences + bpy.utils.unregister_class(VIEW3D_MT_Brushes_Pref) + + for km, kmi in addon_keymaps: + km.keymap_items.remove(kmi) + addon_keymaps.clear() + + +if __name__ == "__main__": + register() diff --git a/space_view3d_brush_menus/brush_menu.py b/space_view3d_brush_menus/brush_menu.py new file mode 100644 index 00000000..28caf747 --- /dev/null +++ b/space_view3d_brush_menus/brush_menu.py @@ -0,0 +1,630 @@ +# gpl author: Ryan Inch (Imaginer) + +import bpy +from bpy.types import ( + Operator, + Menu, + ) +from bpy.props import BoolProperty +from . import utils_core +from . import brushes +from bl_ui.properties_paint_common import UnifiedPaintPanel + +class BrushOptionsMenu(Menu): + bl_label = "Brush Options" + bl_idname = "VIEW3D_MT_sv3_brush_options" + + @classmethod + def poll(self, context): + return utils_core.get_mode() in ( + 'SCULPT', 'VERTEX_PAINT', + 'WEIGHT_PAINT', 'TEXTURE_PAINT', + 'PARTICLE_EDIT' + ) + + def draw(self, context): + mode = utils_core.get_mode() + layout = self.layout + + if mode == 'SCULPT': + self.sculpt(mode, layout, context) + + elif mode in ('VERTEX_PAINT', 'WEIGHT_PAINT'): + self.vw_paint(mode, layout, context) + + elif mode == 'TEXTURE_PAINT': + self.texpaint(mode, layout, context) + + else: + self.particle(layout, context) + + def sculpt(self, mode, layout, context): + has_brush = utils_core.get_brush_link(context, types="brush") + icons = brushes.brush_icon[mode][has_brush.sculpt_tool] if \ + has_brush else "BRUSH_DATA" + layout.operator("wm.toolbar", text="Tools", icon='TOOL_SETTINGS') + layout.row().menu("VIEW3D_MT_sv3_brushes_menu", + icon=icons) + + layout.row().menu(BrushRadiusMenu.bl_idname) + + if has_brush: + # if the active brush is unlinked these menus don't do anything + layout.row().menu(BrushStrengthMenu.bl_idname) + layout.row().menu(BrushAutosmoothMenu.bl_idname) + layout.row().menu(BrushModeMenu.bl_idname) + layout.row().menu("VIEW3D_MT_sv3_texture_menu") + layout.row().menu("VIEW3D_MT_sv3_stroke_options") + layout.row().menu("VIEW3D_MT_sv3_brush_curve_menu") + + layout.row().menu("VIEW3D_MT_sv3_dyntopo") + layout.row().menu("VIEW3D_MT_sv3_master_symmetry_menu") + + def vw_paint(self, mode, layout, context): + has_brush = utils_core.get_brush_link(context, types="brush") + icons = brushes.brush_icon[mode][has_brush.vertex_tool] if \ + has_brush else "BRUSH_DATA" + + layout.operator("wm.toolbar", text="Tools", icon='TOOL_SETTINGS') + + if mode == 'VERTEX_PAINT': + layout.row().operator(ColorPickerPopup.bl_idname, icon="COLOR") + layout.row().separator() + + layout.row().menu("VIEW3D_MT_sv3_brushes_menu", + icon=icons) + + if mode == 'VERTEX_PAINT': + layout.row().menu(BrushRadiusMenu.bl_idname) + + if has_brush: + # if the active brush is unlinked these menus don't do anything + layout.row().menu(BrushStrengthMenu.bl_idname) + layout.row().menu(BrushModeMenu.bl_idname) + layout.row().menu("VIEW3D_MT_sv3_texture_menu") + layout.row().menu("VIEW3D_MT_sv3_stroke_options") + layout.row().menu("VIEW3D_MT_sv3_brush_curve_menu") + + if mode == 'WEIGHT_PAINT': + layout.row().menu(BrushWeightMenu.bl_idname) + layout.row().menu(BrushRadiusMenu.bl_idname) + + if has_brush: + # if the active brush is unlinked these menus don't do anything + layout.row().menu(BrushStrengthMenu.bl_idname) + layout.row().menu(BrushModeMenu.bl_idname) + layout.row().menu("VIEW3D_MT_sv3_stroke_options") + layout.row().menu("VIEW3D_MT_sv3_brush_curve_menu") + + def texpaint(self, mode, layout, context): + toolsettings = context.tool_settings.image_paint + + has_brush = utils_core.get_brush_link(context, types="brush") + icons = brushes.brush_icon[mode][has_brush.image_tool] if \ + has_brush else "BRUSH_DATA" + + if context.image_paint_object and not toolsettings.detect_data(): + if toolsettings.missing_uvs or toolsettings.missing_materials or \ + toolsettings.missing_texture: + layout.row().label(text="Missing Data", icon='ERROR') + layout.row().operator_menu_enum("paint.add_texture_paint_slot", \ + "type", \ + icon='ADD', \ + text="Add Texture Paint Slot") + + return + + elif toolsettings.missing_stencil: + layout.row().label(text="Missing Data", icon='ERROR') + layout.row().label(text="See Mask Properties", icon='FORWARD') + layout.row().separator() + layout.operator("wm.toolbar", text="Tools", icon='TOOL_SETTINGS') + layout.row().menu("VIEW3D_MT_sv3_brushes_menu", + icon=icons) + + return + + else: + layout.row().label(text="Missing Data", icon="INFO") + + else: + layout.operator("wm.toolbar", text="Tools", icon='TOOL_SETTINGS') + + if has_brush and has_brush.image_tool in {'DRAW', 'FILL'} and \ + has_brush.blend not in {'ERASE_ALPHA', 'ADD_ALPHA'}: + layout.row().operator(ColorPickerPopup.bl_idname, icon="COLOR") + layout.row().separator() + + layout.row().menu("VIEW3D_MT_sv3_brushes_menu", + icon=icons) + + if has_brush: + # if the active brush is unlinked these menus don't do anything + if has_brush and has_brush.image_tool in {'MASK'}: + layout.row().menu(BrushWeightMenu.bl_idname, text="Mask Value") + + if has_brush and has_brush.image_tool not in {'FILL'}: + layout.row().menu(BrushRadiusMenu.bl_idname) + + layout.row().menu(BrushStrengthMenu.bl_idname) + + if has_brush and has_brush.image_tool in {'DRAW'}: + layout.row().menu(BrushModeMenu.bl_idname) + + layout.row().menu("VIEW3D_MT_sv3_texture_menu") + layout.row().menu("VIEW3D_MT_sv3_stroke_options") + layout.row().menu("VIEW3D_MT_sv3_brush_curve_menu") + + layout.row().menu("VIEW3D_MT_sv3_master_symmetry_menu") + + def particle(self, layout, context): + particle_edit = context.tool_settings.particle_edit + + layout.operator("wm.toolbar", text="Tools", icon='TOOL_SETTINGS') + + layout.row().menu("VIEW3D_MT_sv3_brushes_menu", + icon="BRUSH_DATA") + layout.row().menu(BrushRadiusMenu.bl_idname) + + if particle_edit.tool != 'ADD': + layout.row().menu(BrushStrengthMenu.bl_idname) + else: + layout.row().menu(ParticleCountMenu.bl_idname) + layout.row().separator() + layout.row().prop(particle_edit, "use_default_interpolate", toggle=True) + + layout.row().prop(particle_edit.brush, "steps", slider=True) + layout.row().prop(particle_edit, "default_key_count", slider=True) + + if particle_edit.tool == 'LENGTH': + layout.row().separator() + layout.row().menu(ParticleLengthMenu.bl_idname) + + if particle_edit.tool == 'PUFF': + layout.row().separator() + layout.row().menu(ParticlePuffMenu.bl_idname) + layout.row().prop(particle_edit.brush, "use_puff_volume", toggle=True) + + +class BrushRadiusMenu(Menu): + bl_label = "Radius" + bl_idname = "VIEW3D_MT_sv3_brush_radius_menu" + bl_description = "Change the size of the brushes" + + def init(self): + if utils_core.get_mode() == 'PARTICLE_EDIT': + settings = (("100", 100), + ("70", 70), + ("50", 50), + ("30", 30), + ("20", 20), + ("10", 10)) + + datapath = "tool_settings.particle_edit.brush.size" + proppath = bpy.context.tool_settings.particle_edit.brush + + else: + settings = (("200", 200), + ("150", 150), + ("100", 100), + ("50", 50), + ("35", 35), + ("10", 10)) + + datapath = "tool_settings.unified_paint_settings.size" + proppath = bpy.context.tool_settings.unified_paint_settings + + return settings, datapath, proppath + + def draw(self, context): + settings, datapath, proppath = self.init() + layout = self.layout + + # add the top slider + layout.row().prop(proppath, "size", slider=True) + layout.row().separator() + + # add the rest of the menu items + for i in range(len(settings)): + utils_core.menuprop( + layout.row(), settings[i][0], settings[i][1], + datapath, icon='RADIOBUT_OFF', disable=True, + disable_icon='RADIOBUT_ON' + ) + + +class BrushStrengthMenu(Menu): + bl_label = "Strength" + bl_idname = "VIEW3D_MT_sv3_brush_strength_menu" + + def init(self): + mode = utils_core.get_mode() + settings = (("1.0", 1.0), + ("0.7", 0.7), + ("0.5", 0.5), + ("0.3", 0.3), + ("0.2", 0.2), + ("0.1", 0.1)) + + proppath = utils_core.get_brush_link(bpy.context, types="brush") + + if mode == 'SCULPT': + datapath = "tool_settings.sculpt.brush.strength" + + elif mode == 'VERTEX_PAINT': + datapath = "tool_settings.vertex_paint.brush.strength" + + elif mode == 'WEIGHT_PAINT': + datapath = "tool_settings.weight_paint.brush.strength" + + elif mode == 'TEXTURE_PAINT': + datapath = "tool_settings.image_paint.brush.strength" + + else: + datapath = "tool_settings.particle_edit.brush.strength" + proppath = bpy.context.tool_settings.particle_edit.brush + + return settings, datapath, proppath + + def draw(self, context): + settings, datapath, proppath = self.init() + layout = self.layout + + # add the top slider + if proppath: + layout.row().prop(proppath, "strength", slider=True) + layout.row().separator() + + # add the rest of the menu items + for i in range(len(settings)): + utils_core.menuprop( + layout.row(), settings[i][0], settings[i][1], + datapath, icon='RADIOBUT_OFF', disable=True, + disable_icon='RADIOBUT_ON' + ) + else: + layout.row().label(text="No brushes available", icon="INFO") + + +class BrushModeMenu(Menu): + bl_label = "Brush Mode" + bl_idname = "VIEW3D_MT_sv3_brush_mode_menu" + + def init(self): + mode = utils_core.get_mode() + has_brush = utils_core.get_brush_link(bpy.context, types="brush") + + if mode == 'SCULPT': + enum = has_brush.bl_rna.properties['sculpt_plane'].enum_items if \ + has_brush else None + path = "tool_settings.sculpt.brush.sculpt_plane" + + elif mode == 'VERTEX_PAINT': + enum = has_brush.bl_rna.properties['blend'].enum_items if \ + has_brush else None + path = "tool_settings.vertex_paint.brush.blend" + + elif mode == 'WEIGHT_PAINT': + enum = has_brush.bl_rna.properties['blend'].enum_items if \ + has_brush else None + path = "tool_settings.weight_paint.brush.blend" + + elif mode == 'TEXTURE_PAINT': + enum = has_brush.bl_rna.properties['blend'].enum_items if \ + has_brush else None + path = "tool_settings.image_paint.brush.blend" + + else: + enum = None + path = "" + + return enum, path + + def draw(self, context): + enum, path = self.init() + layout = self.layout + colum_n = utils_core.addon_settings() + + layout.row().label(text="Brush Mode") + layout.row().separator() + + if enum: + if utils_core.get_mode() != 'SCULPT': + column_flow = layout.column_flow(columns=colum_n) + + # add all the brush modes to the menu + for brush in enum: + utils_core.menuprop( + column_flow.row(), brush.name, + brush.identifier, path, icon='RADIOBUT_OFF', + disable=True, disable_icon='RADIOBUT_ON' + ) + else: + # add all the brush modes to the menu + for brush in enum: + utils_core.menuprop( + layout.row(), brush.name, + brush.identifier, path, icon='RADIOBUT_OFF', + disable=True, disable_icon='RADIOBUT_ON' + ) + else: + layout.row().label(text="No brushes available", icon="INFO") + + +class BrushAutosmoothMenu(Menu): + bl_label = "Autosmooth" + bl_idname = "VIEW3D_MT_sv3_brush_autosmooth_menu" + + def init(self): + settings = (("1.0", 1.0), + ("0.7", 0.7), + ("0.5", 0.5), + ("0.3", 0.3), + ("0.2", 0.2), + ("0.1", 0.1)) + + return settings + + def draw(self, context): + settings = self.init() + layout = self.layout + has_brush = utils_core.get_brush_link(context, types="brush") + + if has_brush: + # add the top slider + layout.row().prop(has_brush, "auto_smooth_factor", slider=True) + layout.row().separator() + + # add the rest of the menu items + for i in range(len(settings)): + utils_core.menuprop( + layout.row(), settings[i][0], settings[i][1], + "tool_settings.sculpt.brush.auto_smooth_factor", + icon='RADIOBUT_OFF', disable=True, + disable_icon='RADIOBUT_ON' + ) + else: + layout.row().label(text="No Smooth options available", icon="INFO") + + +class BrushWeightMenu(Menu): + bl_label = "Weight" + bl_idname = "VIEW3D_MT_sv3_brush_weight_menu" + + def init(self): + settings = (("1.0", 1.0), + ("0.7", 0.7), + ("0.5", 0.5), + ("0.3", 0.3), + ("0.2", 0.2), + ("0.1", 0.1)) + + if utils_core.get_mode() == 'WEIGHT_PAINT': + brush = bpy.context.tool_settings.unified_paint_settings + brushstr = "tool_settings.unified_paint_settings.weight" + name = "Weight" + + else: + brush = bpy.context.tool_settings.image_paint.brush + brushstr = "tool_settings.image_paint.brush.weight" + name = "Mask Value" + + return settings, brush, brushstr, name + + def draw(self, context): + settings, brush, brushstr, name = self.init() + layout = self.layout + + if brush: + # add the top slider + layout.row().prop(brush, "weight", text=name, slider=True) + layout.row().separator() + + # add the rest of the menu items + for i in range(len(settings)): + utils_core.menuprop( + layout.row(), settings[i][0], settings[i][1], + brushstr, + icon='RADIOBUT_OFF', disable=True, + disable_icon='RADIOBUT_ON' + ) + else: + layout.row().label(text="No brush available", icon="INFO") + + +class ParticleCountMenu(Menu): + bl_label = "Count" + bl_idname = "VIEW3D_MT_sv3_particle_count_menu" + + def init(self): + settings = (("50", 50), + ("25", 25), + ("10", 10), + ("5", 5), + ("3", 3), + ("1", 1)) + + return settings + + def draw(self, context): + settings = self.init() + layout = self.layout + + # add the top slider + layout.row().prop(context.tool_settings.particle_edit.brush, + "count", slider=True) + layout.row().separator() + + # add the rest of the menu items + for i in range(len(settings)): + utils_core.menuprop( + layout.row(), settings[i][0], settings[i][1], + "tool_settings.particle_edit.brush.count", + icon='RADIOBUT_OFF', disable=True, + disable_icon='RADIOBUT_ON' + ) + + +class ParticleLengthMenu(Menu): + bl_label = "Length Mode" + bl_idname = "VIEW3D_MT_sv3_particle_length_menu" + + def draw(self, context): + layout = self.layout + path = "tool_settings.particle_edit.brush.length_mode" + + # add the menu items + for item in context.tool_settings.particle_edit.brush. \ + bl_rna.properties['length_mode'].enum_items: + utils_core.menuprop( + layout.row(), item.name, item.identifier, path, + icon='RADIOBUT_OFF', + disable=True, + disable_icon='RADIOBUT_ON' + ) + + +class ParticlePuffMenu(Menu): + bl_label = "Puff Mode" + bl_idname = "VIEW3D_MT_sv3_particle_puff_menu" + + def draw(self, context): + layout = self.layout + path = "tool_settings.particle_edit.brush.puff_mode" + + # add the menu items + for item in context.tool_settings.particle_edit.brush. \ + bl_rna.properties['puff_mode'].enum_items: + utils_core.menuprop( + layout.row(), item.name, item.identifier, path, + icon='RADIOBUT_OFF', + disable=True, + disable_icon='RADIOBUT_ON' + ) + + +class FlipColorsAll(Operator): + """Switch between Foreground and Background colors""" + bl_label = "Flip Colors" + bl_idname = "view3d.sv3_flip_colors_all" + bl_description = "Switch between Foreground and Background colors" + + is_tex: BoolProperty( + default=False, + options={'HIDDEN'} + ) + + def execute(self, context): + try: + if self.is_tex is False: + color = context.tool_settings.vertex_paint.brush.color + secondary_color = context.tool_settings.vertex_paint.brush.secondary_color + + orig_prim = color.hsv + orig_sec = secondary_color.hsv + + color.hsv = orig_sec + secondary_color.hsv = orig_prim + else: + color = context.tool_settings.image_paint.brush.color + secondary_color = context.tool_settings.image_paint.brush.secondary_color + + orig_prim = color.hsv + orig_sec = secondary_color.hsv + + color.hsv = orig_sec + secondary_color.hsv = orig_prim + + return {'FINISHED'} + + except Exception as e: + utils_core.error_handlers(self, "view3d.sv3_flip_colors_all", e, + "Flip Colors could not be completed") + + return {'CANCELLED'} + + +class ColorPickerPopup(Operator): + """Open Color Picker""" + bl_label = "Color" + bl_idname = "view3d.sv3_color_picker_popup" + bl_description = "Open Color Picker" + bl_options = {'REGISTER'} + + @classmethod + def poll(self, context): + return utils_core.get_mode() in ( + 'VERTEX_PAINT', + 'TEXTURE_PAINT' + ) + + def check(self, context): + return True + + def init(self): + if utils_core.get_mode() == 'TEXTURE_PAINT': + settings = bpy.context.tool_settings.image_paint + brush = getattr(settings, "brush", None) + else: + settings = bpy.context.tool_settings.vertex_paint + brush = settings.brush + brush = getattr(settings, "brush", None) + + return settings, brush + + def draw(self, context): + layout = self.layout + settings, brush = self.init() + + + if brush: + layout.row().template_color_picker(brush, "color", value_slider=True) + prim_sec_row = layout.row(align=True) + prim_sec_row.prop(brush, "color", text="") + prim_sec_row.prop(brush, "secondary_color", text="") + + if utils_core.get_mode() == 'VERTEX_PAINT': + prim_sec_row.operator( + FlipColorsAll.bl_idname, + icon='FILE_REFRESH', text="" + ).is_tex = False + else: + prim_sec_row.operator( + FlipColorsAll.bl_idname, + icon='FILE_REFRESH', text="" + ).is_tex = True + + if settings.palette: + layout.column().template_palette(settings, "palette", color=True) + + layout.row().template_ID(settings, "palette", new="palette.new") + else: + layout.row().label(text="No brushes currently available", icon="INFO") + + return + + def execute(self, context): + return context.window_manager.invoke_popup(self, width=180) + + +classes = ( + BrushOptionsMenu, + BrushRadiusMenu, + BrushStrengthMenu, + BrushModeMenu, + BrushAutosmoothMenu, + BrushWeightMenu, + ParticleCountMenu, + ParticleLengthMenu, + ParticlePuffMenu, + FlipColorsAll, + ColorPickerPopup + ) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/space_view3d_brush_menus/brushes.py b/space_view3d_brush_menus/brushes.py new file mode 100644 index 00000000..3478d575 --- /dev/null +++ b/space_view3d_brush_menus/brushes.py @@ -0,0 +1,163 @@ +# gpl author: Ryan Inch (Imaginer) + +import bpy +from bpy.types import Menu +from . import utils_core +from bl_ui.properties_paint_common import UnifiedPaintPanel + +# Particle Tools + +particle_tools = ( + ("Comb", 'COMB'), + ("Smooth", 'SMOOTH'), + ("Add", 'ADD'), + ("Length", 'LENGTH'), + ("Puff", 'PUFF'), + ("Cut", 'CUT'), + ("Weight", 'WEIGHT') +) + +# Brush Datapaths + +brush_datapath = { + 'SCULPT': "tool_settings.sculpt.brush", + 'VERTEX_PAINT': "tool_settings.vertex_paint.brush", + 'WEIGHT_PAINT': "tool_settings.weight_paint.brush", + 'TEXTURE_PAINT': "tool_settings.image_paint.brush", + 'PARTICLE_EDIT': "tool_settings.particle_edit.tool" +} + +# Brush Icons + +brush_icon = { + 'SCULPT': { + "BLOB": 'BRUSH_BLOB', + "CLAY": 'BRUSH_CLAY', + "CLAY_STRIPS": 'BRUSH_CLAY_STRIPS', + "CREASE": 'BRUSH_CREASE', + "DRAW": 'BRUSH_SCULPT_DRAW', + "FILL": 'BRUSH_FILL', + "FLATTEN": 'BRUSH_FLATTEN', + "GRAB": 'BRUSH_GRAB', + "INFLATE": 'BRUSH_INFLATE', + "LAYER": 'BRUSH_LAYER', + "MASK": 'BRUSH_MASK', + "NUDGE": 'BRUSH_NUDGE', + "PINCH": 'BRUSH_PINCH', + "ROTATE": 'BRUSH_ROTATE', + "SCRAPE": 'BRUSH_SCRAPE', + "SIMPLIFY": 'BRUSH_DATA', + "SMOOTH": 'BRUSH_SMOOTH', + "SNAKE_HOOK": 'BRUSH_SNAKE_HOOK', + "THUMB": 'BRUSH_THUMB' + }, + + 'VERTEX_PAINT': { + "AVERAGE": 'BRUSH_BLUR', + "BLUR": 'BRUSH_BLUR', + "DRAW": 'BRUSH_MIX', + "SMEAR": 'BRUSH_BLUR' + }, + + 'WEIGHT_PAINT': { + "AVERAGE": 'BRUSH_BLUR', + "BLUR": 'BRUSH_BLUR', + "DRAW": 'BRUSH_MIX', + "SMEAR": 'BRUSH_BLUR' + }, + + 'TEXTURE_PAINT': { + "CLONE": 'BRUSH_CLONE', + "DRAW": 'BRUSH_TEXDRAW', + "FILL": 'BRUSH_TEXFILL', + "MASK": 'BRUSH_TEXMASK', + "SMEAR": 'BRUSH_SMEAR', + "SOFTEN": 'BRUSH_SOFTEN' + } +} + + +class BrushesMenu(Menu): + bl_label = "Brush" + bl_idname = "VIEW3D_MT_sv3_brushes_menu" + + def draw(self, context): + mode = utils_core.get_mode() + layout = self.layout + settings = UnifiedPaintPanel.paint_settings(context) + colum_n = utils_core.addon_settings() + + layout.row().label(text="Brush") + layout.row().separator() + + has_brush = utils_core.get_brush_link(context, types="brush") + current_brush = eval("bpy.context.{}".format(brush_datapath[mode])) if has_brush else None + + # get the current brush's name + if current_brush and utils_core.get_mode() != 'PARTICLE_EDIT': + current_brush = current_brush.name + + if mode == 'PARTICLE_EDIT': + # if you are in particle edit mode add the menu items for particle mode + for tool in particle_tools: + utils_core.menuprop( + layout.row(), tool[0], tool[1], brush_datapath[mode], + icon='RADIOBUT_OFF', disable=True, + disable_icon='RADIOBUT_ON' + ) + else: + column_flow = layout.column_flow(columns=colum_n) + + # iterate over all the brushes + for item in bpy.data.brushes: + if mode == 'SCULPT': + if item.use_paint_sculpt: + # if you are in sculpt mode and the brush + # is a sculpt brush add the brush to the menu + utils_core.menuprop( + column_flow.row(), item.name, + 'bpy.data.brushes["%s"]' % item.name, + brush_datapath[mode], icon=brush_icon[mode][item.sculpt_tool], + disable=True, custom_disable_exp=(item.name, current_brush), + path=True + ) + if mode == 'VERTEX_PAINT': + if item.use_paint_vertex: + # if you are in vertex paint mode and the brush + # is a vertex paint brush add the brush to the menu + utils_core.menuprop( + column_flow.row(), item.name, + 'bpy.data.brushes["%s"]' % item.name, + brush_datapath[mode], icon=brush_icon[mode][item.vertex_tool], + disable=True, custom_disable_exp=(item.name, current_brush), + path=True + ) + if mode == 'WEIGHT_PAINT': + if item.use_paint_weight: + # if you are in weight paint mode and the brush + # is a weight paint brush add the brush to the menu + utils_core.menuprop( + column_flow.row(), item.name, + 'bpy.data.brushes["%s"]' % item.name, + brush_datapath[mode], icon=brush_icon[mode][item.vertex_tool], + disable=True, custom_disable_exp=(item.name, current_brush), + path=True + ) + if utils_core.get_mode() == 'TEXTURE_PAINT': + if item.use_paint_image: + # if you are in texture paint mode and the brush + # is a texture paint brush add the brush to the menu + utils_core.menuprop( + column_flow.row(), item.name, + 'bpy.data.brushes["%s"]' % item.name, + brush_datapath[mode], icon=brush_icon[mode][item.image_tool], + disable=True, custom_disable_exp=(item.name, current_brush), + path=True + ) + + +def register(): + bpy.utils.register_class(BrushesMenu) + +def unregister(): + bpy.utils.unregister_class(BrushesMenu) diff --git a/space_view3d_brush_menus/curve_menu.py b/space_view3d_brush_menus/curve_menu.py new file mode 100644 index 00000000..5a836031 --- /dev/null +++ b/space_view3d_brush_menus/curve_menu.py @@ -0,0 +1,88 @@ +# gpl author: Ryan Inch (Imaginer) + +import bpy +from bpy.types import ( + Operator, + Menu, + ) +from . import utils_core + + +class BrushCurveMenu(Menu): + bl_label = "Curve" + bl_idname = "VIEW3D_MT_sv3_brush_curve_menu" + + @classmethod + def poll(self, context): + return utils_core.get_mode() in ( + 'SCULPT', 'VERTEX_PAINT', + 'WEIGHT_PAINT', 'TEXTURE_PAINT', + 'PARTICLE_EDIT' + ) + + def draw(self, context): + layout = self.layout + curves = (("Smooth", "SMOOTH", "SMOOTHCURVE"), + ("Sphere", "ROUND", "SPHERECURVE"), + ("Root", "ROOT", "ROOTCURVE"), + ("Sharp", "SHARP", "SHARPCURVE"), + ("Linear", "LINE", "LINCURVE"), + ("Constant", "MAX", "NOCURVE")) + + # add the top slider + layout.row().operator(CurvePopup.bl_idname, icon="RNDCURVE") + layout.row().separator() + + # add the rest of the menu items + for curve in curves: + item = layout.row().operator("brush.curve_preset", + text=curve[0], icon=curve[2]) + item.shape = curve[1] + + +class CurvePopup(Operator): + """Edit Falloff Curve""" + bl_label = "Adjust Curve" + bl_idname = "view3d.sv3_curve_popup" + bl_description = "Edit Falloff Curve" + bl_options = {'REGISTER'} + + @classmethod + def poll(self, context): + return utils_core.get_mode() in ( + 'SCULPT', 'VERTEX_PAINT', + 'WEIGHT_PAINT', 'TEXTURE_PAINT' + ) + + def draw(self, context): + layout = self.layout + has_brush = utils_core.get_brush_link(context, types="brush") + + if utils_core.get_mode() == 'SCULPT' or \ + utils_core.get_mode() == 'VERTEX_PAINT' or \ + utils_core.get_mode() == 'WEIGHT_PAINT' or \ + utils_core.get_mode() == 'TEXTURE_PAINT': + if has_brush: + layout.column().template_curve_mapping(has_brush, + "curve", brush=True) + else: + layout.row().label(text="No brushes available", icon="INFO") + else: + layout.row().label(text="No brushes available", icon="INFO") + + def execute(self, context): + return context.window_manager.invoke_popup(self, width=180) + + +classes = ( + BrushCurveMenu, + CurvePopup + ) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/space_view3d_brush_menus/dyntopo_menu.py b/space_view3d_brush_menus/dyntopo_menu.py new file mode 100644 index 00000000..1a569787 --- /dev/null +++ b/space_view3d_brush_menus/dyntopo_menu.py @@ -0,0 +1,172 @@ +# gpl author: Ryan Inch (Imaginer) + +import bpy +from bpy.types import Menu +from . import utils_core + + +class DynTopoMenu(Menu): + bl_label = "Dyntopo" + bl_idname = "VIEW3D_MT_sv3_dyntopo" + + @classmethod + def poll(self, context): + return utils_core.get_mode() == 'SCULPT' + + def draw(self, context): + layout = self.layout + + if context.object.use_dynamic_topology_sculpting: + layout.row().operator("sculpt.dynamic_topology_toggle", + text="Disable Dynamic Topology") + + layout.row().separator() + + layout.row().menu(DynDetailMenu.bl_idname) + layout.row().menu(DetailMethodMenu.bl_idname) + + layout.row().separator() + + layout.row().operator("sculpt.optimize") + if context.tool_settings.sculpt.detail_type_method == 'CONSTANT': + layout.row().operator("sculpt.detail_flood_fill") + + layout.row().menu(SymmetrizeMenu.bl_idname) + layout.row().prop(context.tool_settings.sculpt, + "use_smooth_shading", toggle=True) + + else: + row = layout.row() + row.operator_context = 'INVOKE_DEFAULT' + row.operator("sculpt.dynamic_topology_toggle", + text="Enable Dynamic Topology") + + +class DynDetailMenu(Menu): + bl_label = "Detail Size" + bl_idname = "VIEW3D_MT_sv3_dyn_detail" + + def init(self): + settings = (("40", 40), + ("30", 30), + ("20", 20), + ("10", 10), + ("5", 5), + ("1", 1)) + + if bpy.context.tool_settings.sculpt.detail_type_method == 'RELATIVE': + datapath = "tool_settings.sculpt.detail_size" + slider_setting = "detail_size" + + elif bpy.context.tool_settings.sculpt.detail_type_method == 'CONSTANT': + datapath = "tool_settings.sculpt.constant_detail_resolution" + slider_setting = "constant_detail_resolution" + else: + datapath = "tool_settings.sculpt.detail_percent" + slider_setting = "detail_percent" + settings = (("100", 100), + ("75", 75), + ("50", 50), + ("25", 25), + ("10", 10), + ("5", 5)) + + return settings, datapath, slider_setting + + def draw(self, context): + settings, datapath, slider_setting = self.init() + layout = self.layout + + # add the top slider + layout.row().prop(context.tool_settings.sculpt, + slider_setting, slider=True) + layout.row().separator() + + # add the rest of the menu items + for i in range(len(settings)): + utils_core.menuprop( + layout.row(), settings[i][0], settings[i][1], datapath, + icon='RADIOBUT_OFF', disable=True, + disable_icon='RADIOBUT_ON' + ) + + +class DetailMethodMenu(Menu): + bl_label = "Detail Method" + bl_idname = "VIEW3D_MT_sv3_detail_method_menu" + + def draw(self, context): + layout = self.layout + refine_path = "tool_settings.sculpt.detail_refine_method" + type_path = "tool_settings.sculpt.detail_type_method" + + refine_items = (("Subdivide Edges", 'SUBDIVIDE'), + ("Collapse Edges", 'COLLAPSE'), + ("Subdivide Collapse", 'SUBDIVIDE_COLLAPSE')) + + type_items = (("Relative Detail", 'RELATIVE'), + ("Constant Detail", 'CONSTANT'), + ("Brush Detail", 'BRUSH')) + + layout.row().label(text="Refine") + layout.row().separator() + + # add the refine menu items + for item in refine_items: + utils_core.menuprop( + layout.row(), item[0], item[1], + refine_path, disable=True, + icon='RADIOBUT_OFF', + disable_icon='RADIOBUT_ON' + ) + + layout.row().label(text="") + + layout.row().label(text="Type") + layout.row().separator() + + # add the type menu items + for item in type_items: + utils_core.menuprop( + layout.row(), item[0], item[1], + type_path, disable=True, + icon='RADIOBUT_OFF', disable_icon='RADIOBUT_ON' + ) + + +class SymmetrizeMenu(Menu): + bl_label = "Symmetrize" + bl_idname = "VIEW3D_MT_sv3_symmetrize_menu" + + def draw(self, context): + layout = self.layout + path = "tool_settings.sculpt.symmetrize_direction" + + # add the the symmetrize operator to the menu + layout.row().operator("sculpt.symmetrize") + layout.row().separator() + + # add the rest of the menu items + for item in context.tool_settings.sculpt. \ + bl_rna.properties['symmetrize_direction'].enum_items: + utils_core.menuprop( + layout.row(), item.name, item.identifier, + path, disable=True, + icon='RADIOBUT_OFF', disable_icon='RADIOBUT_ON' + ) + + +classes = ( + DynTopoMenu, + DynDetailMenu, + DetailMethodMenu, + SymmetrizeMenu + ) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/space_view3d_brush_menus/stroke_menu.py b/space_view3d_brush_menus/stroke_menu.py new file mode 100644 index 00000000..8c5b0407 --- /dev/null +++ b/space_view3d_brush_menus/stroke_menu.py @@ -0,0 +1,200 @@ +# gpl author: Ryan Inch (Imaginer) + +import bpy +from bpy.types import Menu +from . import utils_core +from .brushes import brush_datapath + +# stroke methods: 'AIRBRUSH' 'ANCHORED' 'SPACE' 'DRAG_DOT' 'DOTS' 'LINE' 'CURVE' + +class PaintCurvesMenu(Menu): + bl_label = "Paint Curves" + bl_idname = "VIEW3D_MT_sv3_paint_curves_menu" + + def draw(self, context): + mode = utils_core.get_mode() + layout = self.layout + colum_n = utils_core.addon_settings() + + layout.row().label(text="Paint Curves") + layout.row().separator() + + has_brush = utils_core.get_brush_link(context, types="brush") + + has_current_curve = has_brush.paint_curve if has_brush else None + current_curve = has_current_curve.name if has_current_curve else '' + + column_flow = layout.column_flow(columns=colum_n) + + if len(bpy.data.paint_curves) != 0: + for x, item in enumerate(bpy.data.paint_curves): + utils_core.menuprop( + column_flow.row(), + item.name, + 'bpy.data.paint_curves["%s"]' % item.name, + brush_datapath[mode] + ".paint_curve", + icon='RADIOBUT_OFF', + disable=True, + disable_icon='RADIOBUT_ON', + custom_disable_exp=(item.name, current_curve), + path=True + ) + + else: + layout.row().label(text="No Paint Curves Available", icon="INFO") + +class StrokeOptionsMenu(Menu): + bl_label = "Stroke Options" + bl_idname = "VIEW3D_MT_sv3_stroke_options" + + @classmethod + def poll(self, context): + return utils_core.get_mode() in ( + 'SCULPT', 'VERTEX_PAINT', + 'WEIGHT_PAINT', 'TEXTURE_PAINT', + 'PARTICLE_EDIT' + ) + + def init(self): + has_brush = utils_core.get_brush_link(bpy.context, types="brush") + if utils_core.get_mode() == 'SCULPT': + settings = bpy.context.tool_settings.sculpt + + elif utils_core.get_mode() == 'VERTEX_PAINT': + settings = bpy.context.tool_settings.vertex_paint + + elif utils_core.get_mode() == 'WEIGHT_PAINT': + settings = bpy.context.tool_settings.weight_paint + + elif utils_core.get_mode() == 'TEXTURE_PAINT': + settings = bpy.context.tool_settings.image_paint + + else: + settings = None + + stroke_method = has_brush.stroke_method if has_brush else None + + return settings, has_brush, stroke_method + + def draw(self, context): + settings, brush, stroke_method = self.init() + layout = self.layout + + layout.row().menu(StrokeMethodMenu.bl_idname) + layout.row().separator() + + if stroke_method: + + if stroke_method in ('SPACE', 'LINE') and brush: + layout.row().prop(brush, "spacing", + text=utils_core.PIW + "Spacing", slider=True) + + elif stroke_method == 'AIRBRUSH' and brush: + layout.row().prop(brush, "rate", + text=utils_core.PIW + "Rate", slider=True) + + elif stroke_method == 'ANCHORED' and brush: + layout.row().prop(brush, "use_edge_to_edge") + + elif stroke_method == 'CURVE' and brush: + has_current_curve = brush.paint_curve if brush else None + current_curve = has_current_curve.name if has_current_curve else 'No Curve Selected' + + layout.row().menu(PaintCurvesMenu.bl_idname, text=current_curve, + icon='CURVE_BEZCURVE') + layout.row().operator("paintcurve.new", icon='ADD') + layout.row().operator("paintcurve.draw") + + layout.row().separator() + + layout.row().prop(brush, "spacing", + text=utils_core.PIW + "Spacing", + slider=True) + + else: + pass + + if utils_core.get_mode() == 'SCULPT' and stroke_method in ('DRAG_DOT', 'ANCHORED'): + pass + else: + if brush: + layout.row().prop(brush, "jitter", + text=utils_core.PIW + "Jitter", slider=True) + + layout.row().prop(settings, "input_samples", + text=utils_core.PIW + "Input Samples", slider=True) + + if stroke_method in ('DOTS', 'SPACE', 'AIRBRUSH') and brush: + layout.row().separator() + + layout.row().prop(brush, "use_smooth_stroke", toggle=True) + + if brush.use_smooth_stroke: + layout.row().prop(brush, "smooth_stroke_radius", + text=utils_core.PIW + "Radius", slider=True) + layout.row().prop(brush, "smooth_stroke_factor", + text=utils_core.PIW + "Factor", slider=True) + else: + layout.row().label(text="No Stroke Options available", icon="INFO") + + +class StrokeMethodMenu(Menu): + bl_label = "Stroke Method" + bl_idname = "VIEW3D_MT_sv3_stroke_method" + + def init(self): + has_brush = utils_core.get_brush_link(bpy.context, types="brush") + if utils_core.get_mode() == 'SCULPT': + path = "tool_settings.sculpt.brush.stroke_method" + + elif utils_core.get_mode() == 'VERTEX_PAINT': + path = "tool_settings.vertex_paint.brush.stroke_method" + + elif utils_core.get_mode() == 'WEIGHT_PAINT': + path = "tool_settings.weight_paint.brush.stroke_method" + + elif utils_core.get_mode() == 'TEXTURE_PAINT': + path = "tool_settings.image_paint.brush.stroke_method" + + else: + path = "" + + return has_brush, path + + def draw(self, context): + brush, path = self.init() + layout = self.layout + + layout.row().label(text="Stroke Method") + layout.row().separator() + + if brush: + # add the menu items dynamically based on values in enum property + for tool in brush.bl_rna.properties['stroke_method'].enum_items: + if tool.identifier in ('ANCHORED', 'DRAG_DOT') and \ + utils_core.get_mode() in ('VERTEX_PAINT', + 'WEIGHT_PAINT'): + continue + + utils_core.menuprop( + layout.row(), tool.name, tool.identifier, path, + icon='RADIOBUT_OFF', disable=True, + disable_icon='RADIOBUT_ON' + ) + else: + layout.row().label(text="No Stroke Method available", icon="INFO") + + +classes = ( + PaintCurvesMenu, + StrokeOptionsMenu, + StrokeMethodMenu + ) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/space_view3d_brush_menus/symmetry_menu.py b/space_view3d_brush_menus/symmetry_menu.py new file mode 100644 index 00000000..21e566d4 --- /dev/null +++ b/space_view3d_brush_menus/symmetry_menu.py @@ -0,0 +1,80 @@ +# gpl author: Ryan Inch (Imaginer) + +import bpy +from bpy.types import Menu +from . import utils_core + + +class MasterSymmetryMenu(Menu): + bl_label = "Symmetry Options" + bl_idname = "VIEW3D_MT_sv3_master_symmetry_menu" + + @classmethod + def poll(self, context): + return utils_core.get_mode() in ( + 'SCULPT', + 'TEXTURE_PAINT' + ) + + def draw(self, context): + layout = self.layout + + if utils_core.get_mode() == 'TEXTURE_PAINT': + layout.row().prop(context.tool_settings.image_paint, + "use_symmetry_x", toggle=True) + layout.row().prop(context.tool_settings.image_paint, + "use_symmetry_y", toggle=True) + layout.row().prop(context.tool_settings.image_paint, + "use_symmetry_z", toggle=True) + else: + layout.row().menu(SymmetryMenu.bl_idname) + layout.row().menu(SymmetryRadialMenu.bl_idname) + layout.row().prop(context.tool_settings.sculpt, + "use_symmetry_feather", toggle=True) + + +class SymmetryMenu(Menu): + bl_label = "Symmetry" + bl_idname = "VIEW3D_MT_sv3_symmetry_menu" + + def draw(self, context): + layout = self.layout + + layout.row().label(text="Symmetry") + layout.row().separator() + + layout.row().prop(context.tool_settings.sculpt, + "use_symmetry_x", toggle=True) + layout.row().prop(context.tool_settings.sculpt, + "use_symmetry_y", toggle=True) + layout.row().prop(context.tool_settings.sculpt, + "use_symmetry_z", toggle=True) + + +class SymmetryRadialMenu(Menu): + bl_label = "Radial" + bl_idname = "VIEW3D_MT_sv3_symmetry_radial_menu" + + def draw(self, context): + layout = self.layout + + layout.row().label(text="Radial") + layout.row().separator() + + layout.column().prop(context.tool_settings.sculpt, + "radial_symmetry", text="", slider=True) + + +classes = ( + MasterSymmetryMenu, + SymmetryMenu, + SymmetryRadialMenu + ) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/space_view3d_brush_menus/texture_menu.py b/space_view3d_brush_menus/texture_menu.py new file mode 100644 index 00000000..94484538 --- /dev/null +++ b/space_view3d_brush_menus/texture_menu.py @@ -0,0 +1,415 @@ +# gpl author: Ryan Inch (Imaginer) + +import bpy +from bpy.types import Menu +from . import utils_core + + +class TextureMenu(Menu): + bl_label = "Texture Options" + bl_idname = "VIEW3D_MT_sv3_texture_menu" + + @classmethod + def poll(self, context): + return utils_core.get_mode() in ( + 'SCULPT', + 'VERTEX_PAINT', + 'TEXTURE_PAINT' + ) + + def draw(self, context): + layout = self.layout + + if utils_core.get_mode() == 'SCULPT': + self.sculpt(layout, context) + + elif utils_core.get_mode() == 'VERTEX_PAINT': + self.vertpaint(layout, context) + + else: + self.texpaint(layout, context) + + def sculpt(self, layout, context): + has_brush = utils_core.get_brush_link(context, types="brush") + tex_slot = has_brush.texture_slot if has_brush else None + + # Menus + layout.row().menu(Textures.bl_idname) + layout.row().menu(TextureMapMode.bl_idname) + layout.row().separator() + + # Checkboxes + if tex_slot: + if tex_slot.map_mode != '3D': + if tex_slot.map_mode in ('RANDOM', 'VIEW_PLANE', 'AREA_PLANE'): + layout.row().prop(tex_slot, "use_rake", toggle=True) + layout.row().prop(tex_slot, "use_random", toggle=True) + + # Sliders + layout.row().prop(tex_slot, "angle", + text=utils_core.PIW + "Angle", slider=True) + + if tex_slot.tex_paint_map_mode in ('RANDOM', 'VIEW_PLANE') and tex_slot.use_random: + layout.row().prop(tex_slot, "random_angle", + text=utils_core.PIW + "Random Angle", slider=True) + + # Operator + if tex_slot.tex_paint_map_mode == 'STENCIL': + if has_brush.texture and has_brush.texture.type == 'IMAGE': + layout.row().operator("brush.stencil_fit_image_aspect") + + layout.row().operator("brush.stencil_reset_transform") + + else: + layout.row().label(text="No Texture Slot available", icon="INFO") + + def vertpaint(self, layout, context): + has_brush = utils_core.get_brush_link(context, types="brush") + tex_slot = has_brush.texture_slot if has_brush else None + + # Menus + layout.row().menu(Textures.bl_idname) + layout.row().menu(TextureMapMode.bl_idname) + + # Checkboxes + if tex_slot: + if tex_slot.tex_paint_map_mode != '3D': + if tex_slot.tex_paint_map_mode in ('RANDOM', 'VIEW_PLANE'): + layout.row().prop(tex_slot, "use_rake", toggle=True) + layout.row().prop(tex_slot, "use_random", toggle=True) + + # Sliders + layout.row().prop(tex_slot, "angle", + text=utils_core.PIW + "Angle", slider=True) + + if tex_slot.tex_paint_map_mode in ('RANDOM', 'VIEW_PLANE') and tex_slot.use_random: + layout.row().prop(tex_slot, "random_angle", + text=utils_core.PIW + "Random Angle", slider=True) + + # Operator + if tex_slot.tex_paint_map_mode == 'STENCIL': + if has_brush.texture and has_brush.texture.type == 'IMAGE': + layout.row().operator("brush.stencil_fit_image_aspect") + + layout.row().operator("brush.stencil_reset_transform") + + else: + layout.row().label(text="No Texture Slot available", icon="INFO") + + def texpaint(self, layout, context): + has_brush = utils_core.get_brush_link(context, types="brush") + tex_slot = has_brush.texture_slot if has_brush else None + mask_tex_slot = has_brush.mask_texture_slot if has_brush else None + + # Texture Section + layout.row().label(text="Texture", icon='TEXTURE') + + # Menus + layout.row().menu(Textures.bl_idname) + layout.row().menu(TextureMapMode.bl_idname) + + # Checkboxes + if tex_slot: + if tex_slot.tex_paint_map_mode != '3D': + if tex_slot.tex_paint_map_mode in ('RANDOM', 'VIEW_PLANE'): + layout.row().prop(tex_slot, "use_rake", toggle=True) + layout.row().prop(tex_slot, "use_random", toggle=True) + + # Sliders + layout.row().prop(tex_slot, "angle", + text=utils_core.PIW + "Angle", slider=True) + + if tex_slot.tex_paint_map_mode in ('RANDOM', 'VIEW_PLANE') and tex_slot.use_random: + layout.row().prop(tex_slot, "random_angle", + text=utils_core.PIW + "Random Angle", slider=True) + + # Operator + if tex_slot.tex_paint_map_mode == 'STENCIL': + if has_brush.texture and has_brush.texture.type == 'IMAGE': + layout.row().operator("brush.stencil_fit_image_aspect") + + layout.row().operator("brush.stencil_reset_transform") + + else: + layout.row().label(text="No Texture Slot available", icon="INFO") + + layout.row().separator() + + # Texture Mask Section + layout.row().label(text="Texture Mask", icon='MOD_MASK') + + # Menus + layout.row().menu(MaskTextures.bl_idname) + layout.row().menu(MaskMapMode.bl_idname) + layout.row().menu(MaskPressureModeMenu.bl_idname) + + # Checkboxes + if mask_tex_slot: + if mask_tex_slot.mask_map_mode in ('RANDOM', 'VIEW_PLANE'): + layout.row().prop(mask_tex_slot, "use_rake", toggle=True) + layout.row().prop(mask_tex_slot, "use_random", toggle=True) + + # Sliders + layout.row().prop(mask_tex_slot, "angle", + text=utils_core.PIW + "Angle", icon_value=5, slider=True) + + if mask_tex_slot.mask_map_mode in ('RANDOM', 'VIEW_PLANE') and mask_tex_slot.use_random: + layout.row().prop(mask_tex_slot, "random_angle", + text=utils_core.PIW + "Random Angle", slider=True) + + # Operator + if mask_tex_slot.mask_map_mode == 'STENCIL': + if has_brush.mask_texture and has_brush.mask_texture.type == 'IMAGE': + layout.row().operator("brush.stencil_fit_image_aspect") + + prop = layout.row().operator("brush.stencil_reset_transform") + prop.mask = True + + else: + layout.row().label(text="Mask Texture not available", icon="INFO") + + +class Textures(Menu): + bl_label = "Brush Texture" + bl_idname = "VIEW3D_MT_sv3_texture_list" + + def init(self): + if utils_core.get_mode() == 'SCULPT': + datapath = "tool_settings.sculpt.brush.texture" + + elif utils_core.get_mode() == 'VERTEX_PAINT': + datapath = "tool_settings.vertex_paint.brush.texture" + + elif utils_core.get_mode() == 'TEXTURE_PAINT': + datapath = "tool_settings.image_paint.brush.texture" + + else: + datapath = "" + + return datapath + + def draw(self, context): + datapath = self.init() + has_brush = utils_core.get_brush_link(context, types="brush") + current_texture = eval("bpy.context.{}".format(datapath)) if \ + has_brush else None + layout = self.layout + + # get the current texture's name + if current_texture: + current_texture = current_texture.name + + layout.row().label(text="Brush Texture") + layout.row().separator() + + # add an item to set the texture to None + utils_core.menuprop(layout.row(), "None", "None", + datapath, icon='RADIOBUT_OFF', disable=True, + disable_icon='RADIOBUT_ON', + custom_disable_exp=(None, current_texture), + path=True) + + # add the menu items + for item in bpy.data.textures: + utils_core.menuprop(layout.row(), item.name, + 'bpy.data.textures["%s"]' % item.name, + datapath, icon='RADIOBUT_OFF', + disable=True, + disable_icon='RADIOBUT_ON', + custom_disable_exp=(item.name, current_texture), + path=True) + + +class TextureMapMode(Menu): + bl_label = "Brush Mapping" + bl_idname = "VIEW3D_MT_sv3_texture_map_mode" + + def draw(self, context): + layout = self.layout + has_brush = utils_core.get_brush_link(context, types="brush") + + layout.row().label(text="Brush Mapping") + layout.row().separator() + + if has_brush: + if utils_core.get_mode() == 'SCULPT': + path = "tool_settings.sculpt.brush.texture_slot.map_mode" + + # add the menu items + for item in has_brush. \ + texture_slot.bl_rna.properties['map_mode'].enum_items: + utils_core.menuprop( + layout.row(), item.name, item.identifier, path, + icon='RADIOBUT_OFF', + disable=True, + disable_icon='RADIOBUT_ON' + ) + elif utils_core.get_mode() == 'VERTEX_PAINT': + path = "tool_settings.vertex_paint.brush.texture_slot.tex_paint_map_mode" + + # add the menu items + for item in has_brush. \ + texture_slot.bl_rna.properties['tex_paint_map_mode'].enum_items: + utils_core.menuprop( + layout.row(), item.name, item.identifier, path, + icon='RADIOBUT_OFF', + disable=True, + disable_icon='RADIOBUT_ON' + ) + else: + path = "tool_settings.image_paint.brush.texture_slot.tex_paint_map_mode" + + # add the menu items + for item in has_brush. \ + texture_slot.bl_rna.properties['tex_paint_map_mode'].enum_items: + utils_core.menuprop( + layout.row(), item.name, item.identifier, path, + icon='RADIOBUT_OFF', + disable=True, + disable_icon='RADIOBUT_ON' + ) + else: + layout.row().label(text="No brushes available", icon="INFO") + + +class MaskTextures(Menu): + bl_label = "Mask Texture" + bl_idname = "VIEW3D_MT_sv3_mask_texture_list" + + def draw(self, context): + layout = self.layout + datapath = "tool_settings.image_paint.brush.mask_texture" + has_brush = utils_core.get_brush_link(context, types="brush") + current_texture = eval("bpy.context.{}".format(datapath)) if \ + has_brush else None + + layout.row().label(text="Mask Texture") + layout.row().separator() + + if has_brush: + # get the current texture's name + if current_texture: + current_texture = current_texture.name + + # add an item to set the texture to None + utils_core.menuprop( + layout.row(), "None", "None", + datapath, icon='RADIOBUT_OFF', disable=True, + disable_icon='RADIOBUT_ON', + custom_disable_exp=(None, current_texture), + path=True + ) + + # add the menu items + for item in bpy.data.textures: + utils_core.menuprop( + layout.row(), item.name, 'bpy.data.textures["%s"]' % item.name, + datapath, icon='RADIOBUT_OFF', disable=True, + disable_icon='RADIOBUT_ON', + custom_disable_exp=(item.name, current_texture), + path=True + ) + else: + layout.row().label(text="No brushes available", icon="INFO") + + +class MaskMapMode(Menu): + bl_label = "Mask Mapping" + bl_idname = "VIEW3D_MT_sv3_mask_map_mode" + + def draw(self, context): + layout = self.layout + path = "tool_settings.image_paint.brush.mask_texture_slot.mask_map_mode" + has_brush = utils_core.get_brush_link(context, types="brush") + + layout.row().label(text="Mask Mapping") + layout.row().separator() + if has_brush: + items = has_brush. \ + mask_texture_slot.bl_rna.properties['mask_map_mode'].enum_items + # add the menu items + for item in items: + utils_core.menuprop( + layout.row(), item.name, item.identifier, path, + icon='RADIOBUT_OFF', + disable=True, + disable_icon='RADIOBUT_ON' + ) + else: + layout.row().label(text="No brushes available", icon="INFO") + + +class TextureAngleSource(Menu): + bl_label = "Texture Angle Source" + bl_idname = "VIEW3D_MT_sv3_texture_angle_source" + + def draw(self, context): + layout = self.layout + has_brush = utils_core.get_brush_link(context, types="brush") + + if has_brush: + if utils_core.get_mode() == 'SCULPT': + items = has_brush. \ + bl_rna.properties['texture_angle_source_random'].enum_items + path = "tool_settings.sculpt.brush.texture_angle_source_random" + + elif utils_core.get_mode() == 'VERTEX_PAINT': + items = has_brush. \ + bl_rna.properties['texture_angle_source_random'].enum_items + path = "tool_settings.vertex_paint.brush.texture_angle_source_random" + + else: + items = has_brush. \ + bl_rna.properties['texture_angle_source_random'].enum_items + path = "tool_settings.image_paint.brush.texture_angle_source_random" + + # add the menu items + for item in items: + utils_core.menuprop( + layout.row(), item[0], item[1], path, + icon='RADIOBUT_OFF', + disable=True, + disable_icon='RADIOBUT_ON' + ) + else: + layout.row().label(text="No brushes available", icon="INFO") + +class MaskPressureModeMenu(Menu): + bl_label = "Mask Pressure Mode" + bl_idname = "VIEW3D_MT_sv3_mask_pressure_mode_menu" + + def draw(self, context): + layout = self.layout + path = "tool_settings.image_paint.brush.use_pressure_masking" + + layout.row().label(text="Mask Pressure Mode") + layout.row().separator() + + # add the menu items + for item in context.tool_settings.image_paint.brush. \ + bl_rna.properties['use_pressure_masking'].enum_items: + utils_core.menuprop( + layout.row(), item.name, item.identifier, path, + icon='RADIOBUT_OFF', + disable=True, + disable_icon='RADIOBUT_ON' + ) + + +classes = ( + TextureMenu, + Textures, + TextureMapMode, + MaskTextures, + MaskMapMode, + TextureAngleSource, + MaskPressureModeMenu + ) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/space_view3d_brush_menus/utils_core.py b/space_view3d_brush_menus/utils_core.py new file mode 100644 index 00000000..dc99ba0b --- /dev/null +++ b/space_view3d_brush_menus/utils_core.py @@ -0,0 +1,105 @@ +# gpl author: Ryan Inch (Imaginer) + +import bpy + +get_addon_name = 'space_view3d_brush_menus' + +# Property Icon Width +PIW = ' ' + + +# check for (currently) brushes being linked +def get_brush_link(context, types="brush"): + tool_settings = context.tool_settings + has_brush = None + + if get_mode() == 'SCULPT': + datapath = tool_settings.sculpt + + elif get_mode() == 'VERTEX_PAINT': + datapath = tool_settings.vertex_paint + + elif get_mode() == 'WEIGHT_PAINT': + datapath = tool_settings.weight_paint + + elif get_mode() == 'TEXTURE_PAINT': + datapath = tool_settings.image_paint + else: + datapath = None + + if types == "brush": + has_brush = getattr(datapath, "brush", None) + + return has_brush + + +# Addon settings +def addon_settings(): + # separate function just for more convenience + addon = bpy.context.preferences.addons[get_addon_name] + colum_n = addon.preferences.column_set if addon else 1 + + return colum_n + + +def error_handlers(self, op_name, error, reports="ERROR", func=False): + if self and reports: + self.report({'WARNING'}, reports + " (See Console for more info)") + + is_func = "Function" if func else "Operator" + print("\n[Sculpt/Paint Brush Menus]\n{}: {}\nError: {}\n".format(is_func, op_name, error)) + + +# Object modes: +# 'OBJECT' 'EDIT' 'SCULPT' +# 'VERTEX_PAINT' 'WEIGHT_PAINT' 'TEXTURE_PAINT' +# 'PARTICLE_EDIT' 'POSE' 'GPENCIL_EDIT' +def get_mode(): + return bpy.context.object.mode + +def menuprop(item, name, value, data_path, + icon='NONE', disable=False, disable_icon=None, + custom_disable_exp=None, method=None, path=False): + + # disable the ui + if disable: + disabled = False + + # used if you need a custom expression to disable the ui + if custom_disable_exp: + if custom_disable_exp[0] == custom_disable_exp[1]: + item.enabled = False + disabled = True + + # check if the ui should be disabled for numbers + elif isinstance(eval("bpy.context.{}".format(data_path)), float): + if round(eval("bpy.context.{}".format(data_path)), 2) == value: + item.enabled = False + disabled = True + + # check if the ui should be disabled for anything else + else: + if eval("bpy.context.{}".format(data_path)) == value: + item.enabled = False + disabled = True + + # change the icon to the disable_icon if the ui has been disabled + if disable_icon and disabled: + icon = disable_icon + + # creates the menu item + prop = item.operator("wm.context_set_value", text=name, icon=icon) + + # sets what the menu item changes + if path: + prop.value = value + value = eval(value) + + elif type(value) == str: + prop.value = "'{}'".format(value) + + else: + prop.value = '{}'.format(value) + + # sets the path to what is changed + prop.data_path = data_path |