Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyan Inch <mythologylover75@gmail.com>2019-08-31 07:04:43 +0300
committerRyan Inch <mythologylover75@gmail.com>2019-08-31 07:04:43 +0300
commitc63bb5abce16b85dd0f6888b0114c65092747e7a (patch)
treecf86e4fb884894b37fdf558b5802cbab8d9cddc9
parent9f3bea45726e50b01491596993779e1e05306b99 (diff)
Dynamic Brush Menus: return to release: T68350 T63750 437e41a06681
-rw-r--r--space_view3d_brush_menus/__init__.py135
-rw-r--r--space_view3d_brush_menus/brush_menu.py630
-rw-r--r--space_view3d_brush_menus/brushes.py163
-rw-r--r--space_view3d_brush_menus/curve_menu.py88
-rw-r--r--space_view3d_brush_menus/dyntopo_menu.py172
-rw-r--r--space_view3d_brush_menus/stroke_menu.py200
-rw-r--r--space_view3d_brush_menus/symmetry_menu.py80
-rw-r--r--space_view3d_brush_menus/texture_menu.py415
-rw-r--r--space_view3d_brush_menus/utils_core.py105
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