Files
test2/scripts/startup/bl_ui/properties_paint_common.py
Sean Kim a40d484b1a UI: Show preset controls for brush strength, size, and jitter curves
Toggles the new `show_presets` option on for the recently added brush
size, strength, and jitter curves across paint modes to allow users
to set these custom curves to any of the defined internal presets.

Pull Request: https://projects.blender.org/blender/blender/pulls/147000
2025-09-30 00:34:20 +02:00

1899 lines
70 KiB
Python

# SPDX-FileCopyrightText: 2012-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
from bpy.types import Menu, Panel
from bpy.app.translations import (
contexts as i18n_contexts,
pgettext_iface as iface_,
pgettext_n as n_,
)
class BrushAssetShelf:
bl_options = {
'DEFAULT_VISIBLE',
'NO_ASSET_DRAG',
'STORE_ENABLED_CATALOGS_IN_PREFERENCES',
# Ensure `bl_activate_operator` is called when spawning the context menu. Operators there
# rely on the imported, active brush, not just the active asset representation.
'ACTIVATE_FOR_CONTEXT_MENU',
}
bl_activate_operator = "BRUSH_OT_asset_activate"
bl_default_preview_size = 48
brush_type_prop = None
mode_prop = None
@classmethod
def poll(cls, context):
return (ob := getattr(context, "object", None)) is not None and ob.mode == cls.mode
@classmethod
def has_tool_with_brush_type(cls, context, brush_type):
from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
space_type = context.space_data.type
brush_type_items = bpy.types.Brush.bl_rna.properties[cls.brush_type_prop].enum_items
tool_helper_cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
for item in ToolSelectPanelHelper._tools_flatten(
tool_helper_cls.tools_from_context(context, mode=context.mode),
):
if item is None:
continue
if item.idname in {
"builtin.arc",
"builtin.curve",
"builtin.line",
"builtin.box",
"builtin.circle",
"builtin.polyline",
}:
continue
if item.options is None or ('USE_BRUSHES' not in item.options):
continue
if item.brush_type is not None:
if brush_type_items[item.brush_type].value == brush_type:
return True
return False
@classmethod
def brush_type_poll(cls, context, asset):
from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
tool = ToolSelectPanelHelper.tool_active_from_context(context)
if not tool:
return True
if not cls.brush_type_prop:
return True
asset_brush_type = asset.metadata.get(cls.brush_type_prop)
# Asset metadata doesn't store a brush type. Only show it when the tool doesn't require a
# certain brush type.
if asset_brush_type is None:
return False
# For the general brush that supports any brush type, filter out brushes that show up for
# other tools already.
if tool.brush_type == 'ANY':
return not cls.has_tool_with_brush_type(context, asset_brush_type)
brush_type_items = bpy.types.Brush.bl_rna.properties[cls.brush_type_prop].enum_items
return brush_type_items[tool.brush_type].value == asset_brush_type
@classmethod
def asset_poll(cls, asset):
if asset.id_type != 'BRUSH':
return False
if cls.mode_prop and not asset.metadata.get(cls.mode_prop, False):
return False
context = bpy.context
prefs = context.preferences
is_asset_shelf_region = context.region and context.region.type == 'ASSET_SHELF'
# Show all brushes in the popup asset shelves. Otherwise filter out brushes that
# are incompatible with the tool.
if is_asset_shelf_region and prefs.view.use_filter_brushes_by_tool:
return cls.brush_type_poll(context, asset)
return True
@classmethod
def get_active_asset(cls):
# Only show active highlight when using the brush tool.
from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
tool = ToolSelectPanelHelper.tool_active_from_context(bpy.context)
if not tool or not tool.use_brushes:
return None
paint_settings = UnifiedPaintPanel.paint_settings(bpy.context)
return paint_settings.brush_asset_reference if paint_settings else None
@classmethod
def draw_context_menu(self, context, asset, layout):
# Currently this menu adds operators that deal with the affected brush and don't take the
# asset into account. Luckily that is okay for now, since right clicking in the grid view
# also activates the item.
layout.menu_contents("VIEW3D_MT_brush_context_menu")
@staticmethod
def get_shelf_name_from_context(context):
mode_map = {
'SCULPT': "VIEW3D_AST_brush_sculpt",
'PAINT_VERTEX': "VIEW3D_AST_brush_vertex_paint",
'PAINT_WEIGHT': "VIEW3D_AST_brush_weight_paint",
'PAINT_TEXTURE': "VIEW3D_AST_brush_texture_paint",
'PAINT_2D': "IMAGE_AST_brush_paint",
'PAINT_GREASE_PENCIL': "VIEW3D_AST_brush_gpencil_paint",
'SCULPT_GREASE_PENCIL': "VIEW3D_AST_brush_gpencil_sculpt",
'WEIGHT_GREASE_PENCIL': "VIEW3D_AST_brush_gpencil_weight",
'VERTEX_GREASE_PENCIL': "VIEW3D_AST_brush_gpencil_vertex",
'SCULPT_CURVES': "VIEW3D_AST_brush_sculpt_curves",
}
mode = UnifiedPaintPanel.get_brush_mode(context)
if not mode:
return None
return mode_map[mode]
@staticmethod
def draw_popup_selector(layout, context, brush, show_name=True):
preview_icon_id = brush.preview.icon_id if brush and brush.preview else 0
shelf_name = BrushAssetShelf.get_shelf_name_from_context(context)
if not shelf_name:
return
display_name = brush.name if (brush and show_name) else None
if display_name and brush.has_unsaved_changes:
display_name = display_name + "*"
layout.template_asset_shelf_popover(
shelf_name,
name=display_name,
icon='BRUSH_DATA' if not preview_icon_id else 'NONE',
icon_value=preview_icon_id,
)
class VIEW3D_PT_brush_asset_shelf_filter(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'HEADER'
bl_label = "Filter"
bl_parent_id = "ASSETSHELF_PT_display"
@classmethod
def poll(cls, context):
if context.asset_shelf is None:
return False
return context.asset_shelf.bl_idname == BrushAssetShelf.get_shelf_name_from_context(context)
def draw(self, context):
layout = self.layout
prefs = context.preferences
layout.prop(prefs.view, "use_filter_brushes_by_tool", text="By Active Tool")
class UnifiedPaintPanel:
# subclass must set
# bl_space_type = 'IMAGE_EDITOR'
# bl_region_type = 'UI'
@staticmethod
def get_brush_mode(context):
""" Get the correct mode for this context. For any context where this returns None,
no brush options should be displayed."""
mode = context.mode
if mode == 'PARTICLE':
# Particle brush settings currently completely do their own thing.
return None
from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
tool = ToolSelectPanelHelper.tool_active_from_context(context)
if not tool:
# If there is no active tool, then there can't be an active brush.
return None
if not tool.use_brushes:
return None
space_data = context.space_data
tool_settings = context.tool_settings
if space_data:
space_type = space_data.type
if space_type == 'IMAGE_EDITOR':
return 'PAINT_2D'
elif space_type in {'VIEW_3D', 'PROPERTIES'}:
if mode == 'PAINT_TEXTURE':
if tool_settings.image_paint:
return mode
else:
return None
return mode
return None
@staticmethod
def paint_settings(context):
tool_settings = context.tool_settings
mode = UnifiedPaintPanel.get_brush_mode(context)
# 3D paint settings
if mode == 'SCULPT':
return tool_settings.sculpt
elif mode == 'PAINT_VERTEX':
return tool_settings.vertex_paint
elif mode == 'PAINT_WEIGHT':
return tool_settings.weight_paint
elif mode == 'PAINT_TEXTURE':
return tool_settings.image_paint
elif mode == 'PARTICLE':
return tool_settings.particle_edit
# 2D paint settings
elif mode == 'PAINT_2D':
return tool_settings.image_paint
elif mode == 'SCULPT_CURVES':
return tool_settings.curves_sculpt
# Grease Pencil settings
elif mode == 'PAINT_GREASE_PENCIL':
return tool_settings.gpencil_paint
elif mode == 'SCULPT_GREASE_PENCIL':
return tool_settings.gpencil_sculpt_paint
elif mode == 'WEIGHT_GREASE_PENCIL':
return tool_settings.gpencil_weight_paint
elif mode == 'VERTEX_GREASE_PENCIL':
return tool_settings.gpencil_vertex_paint
return None
@staticmethod
def prop_unified(
layout,
context,
brush,
prop_name,
unified_paint_settings_override=None,
unified_name=None,
pressure_name=None,
curve_visibility_name=None,
icon='NONE',
text=None,
slider=False,
header=False,
):
""" Generalized way of adding brush options to the UI,
along with their pen pressure setting and global toggle, if they exist.
:param unified_paint_settings_override allows a caller to pass in a specific object for usage. Needed for
some 'brush-like' tools."""
row = layout.row(align=True)
paint = UnifiedPaintPanel.paint_settings(context)
if unified_paint_settings_override:
ups = unified_paint_settings_override
else:
ups = UnifiedPaintPanel.paint_settings(context).unified_paint_settings
prop_owner = brush
if unified_name and getattr(ups, unified_name):
prop_owner = ups
row.prop(prop_owner, prop_name, icon=icon, text=text, slider=slider)
if unified_name and not header:
# NOTE: We don't draw UnifiedPaintSettings in the header to reduce clutter. D5928#136281
row.prop(ups, unified_name, text="", icon='BRUSHES_ALL')
if pressure_name:
row.prop(brush, pressure_name, text="")
if curve_visibility_name and not header:
is_active = getattr(paint, curve_visibility_name)
row.prop(
paint,
curve_visibility_name,
text="",
icon='DOWNARROW_HLT' if is_active else 'RIGHTARROW',
emboss=False)
return row
@staticmethod
def prop_unified_color(parent, context, brush, prop_name, *, text=None):
ups = UnifiedPaintPanel.paint_settings(context).unified_paint_settings
prop_owner = ups if ups.use_unified_color else brush
parent.prop(prop_owner, prop_name, text=text)
@staticmethod
def prop_unified_color_picker(parent, context, brush, prop_name, value_slider=True):
ups = UnifiedPaintPanel.paint_settings(context).unified_paint_settings
prop_owner = ups if ups.use_unified_color else brush
parent.template_color_picker(prop_owner, prop_name, value_slider=value_slider)
### Classes to let various paint modes' panels share code, by sub-classing these classes. ###
class BrushPanel(UnifiedPaintPanel):
@classmethod
def poll(cls, context):
return cls.get_brush_mode(context) is not None
class BrushSelectPanel(BrushPanel):
bl_label = "Brush Asset"
# Use header preset function to set the title.
def draw_header_preset(self, context):
# layout = self.layout # UNUSED.
settings = self.paint_settings(context)
if settings is None:
return
brush = settings.brush
if brush is None:
return
if brush.has_unsaved_changes:
self.bl_label = n_("Brush Asset (Unsaved)")
else:
self.bl_label = n_("Brush Asset")
def draw(self, context):
layout = self.layout
settings = self.paint_settings(context)
if settings is None:
return
brush = settings.brush
row = layout.row()
col = row.column(align=True)
BrushAssetShelf.draw_popup_selector(col, context, brush, show_name=False)
if brush:
col.prop(brush, "name", text="")
if brush is None:
return
col = row.column()
col.menu("VIEW3D_MT_brush_context_menu", icon='DOWNARROW_HLT', text="")
class ColorPalettePanel(BrushPanel):
bl_label = "Color Palette"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
if not super().poll(context):
return False
settings = cls.paint_settings(context)
if (brush := settings.brush) is None:
return False
if context.space_data.type == 'IMAGE_EDITOR' or context.image_paint_object:
capabilities = brush.image_paint_capabilities
return capabilities.has_color
elif context.vertex_paint_object:
capabilities = brush.vertex_paint_capabilities
return capabilities.has_color
elif context.sculpt_object:
capabilities = brush.sculpt_capabilities
return capabilities.has_color
return False
def draw(self, context):
layout = self.layout
settings = self.paint_settings(context)
layout.template_ID(settings, "palette", new="palette.new")
if settings.palette:
layout.template_palette(settings, "palette", color=True)
class ClonePanel(BrushPanel):
bl_label = "Clone"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
if not super().poll(context):
return False
settings = cls.paint_settings(context)
mode = cls.get_brush_mode(context)
if mode == 'PAINT_TEXTURE':
brush = settings.brush
return brush.image_brush_type == 'CLONE'
return False
def draw_header(self, context):
settings = self.paint_settings(context)
self.layout.prop(settings, "use_clone_layer", text="")
def draw(self, context):
layout = self.layout
settings = self.paint_settings(context)
layout.active = settings.use_clone_layer
ob = context.active_object
col = layout.column()
if settings.mode == 'MATERIAL':
if len(ob.material_slots) > 1:
col.label(text="Materials")
col.template_list(
"MATERIAL_UL_matslots", "",
ob, "material_slots",
ob, "active_material_index",
rows=2,
)
mat = ob.active_material
if mat:
col.label(text="Source Clone Slot")
col.template_list(
"TEXTURE_UL_texpaintslots", "",
mat, "texture_paint_slots",
mat, "paint_clone_slot",
rows=2,
)
elif settings.mode == 'IMAGE':
mesh = ob.data
clone_text = mesh.uv_layer_clone.name if mesh.uv_layer_clone else ""
col.label(text="Source Clone Image")
col.template_ID(settings, "clone_image")
col.label(text="Source Clone UV Map")
col.menu("VIEW3D_MT_tools_projectpaint_clone", text=clone_text, translate=False)
class TextureMaskPanel(BrushPanel):
bl_label = "Texture Mask"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
brush = context.tool_settings.image_paint.brush
mask_tex_slot = brush.mask_texture_slot
col = layout.column()
col.template_ID_preview(mask_tex_slot, "texture", new="texture.new", rows=3, cols=8)
# map_mode
layout.row().prop(mask_tex_slot, "mask_map_mode", text="Mask Mapping")
if mask_tex_slot.map_mode == 'STENCIL':
if brush.mask_texture and brush.mask_texture.type == 'IMAGE':
layout.operator("brush.stencil_fit_image_aspect").mask = True
layout.operator("brush.stencil_reset_transform").mask = True
col = layout.column()
col.prop(brush, "use_pressure_masking", text="Pressure Masking")
# angle and texture_angle_source
if mask_tex_slot.has_texture_angle:
col = layout.column()
col.prop(mask_tex_slot, "angle", text="Angle")
if mask_tex_slot.has_texture_angle_source:
col.prop(mask_tex_slot, "use_rake", text="Rake")
if brush.brush_capabilities.has_random_texture_angle and mask_tex_slot.has_random_texture_angle:
col.prop(mask_tex_slot, "use_random", text="Random")
if mask_tex_slot.use_random:
col.prop(mask_tex_slot, "random_angle", text="Random Angle")
# scale and offset
col.prop(mask_tex_slot, "offset")
col.prop(mask_tex_slot, "scale")
class StrokePanel(BrushPanel):
bl_label = "Stroke"
bl_options = {'DEFAULT_CLOSED'}
bl_ui_units_x = 13
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
mode = self.get_brush_mode(context)
settings = self.paint_settings(context)
brush = settings.brush
col = layout.column()
col.prop(brush, "stroke_method")
col.separator()
if brush.use_anchor:
col.prop(brush, "use_edge_to_edge", text="Edge to Edge")
if brush.use_airbrush:
col.prop(brush, "rate", text="Rate", slider=True)
if brush.use_space:
row = col.row(align=True)
row.prop(brush, "spacing", text="Spacing")
row.prop(brush, "use_pressure_spacing", toggle=True, text="")
if brush.use_line or brush.use_curve:
row = col.row(align=True)
row.prop(brush, "spacing", text="Spacing")
if mode == 'SCULPT':
col.row().prop(brush, "use_scene_spacing", text="Spacing Distance", expand=True)
if mode in {'PAINT_TEXTURE', 'PAINT_2D', 'SCULPT'}:
if brush.image_paint_capabilities.has_space_attenuation or brush.sculpt_capabilities.has_space_attenuation:
col.prop(brush, "use_space_attenuation")
if brush.use_curve:
col.separator()
col.template_ID(brush, "paint_curve", new="paintcurve.new")
col.operator("paintcurve.draw")
col.separator()
if brush.use_space or brush.use_line or brush.use_curve:
col.separator()
row = col.row(align=True)
col.prop(brush, "dash_ratio", text="Dash Ratio")
col.prop(brush, "dash_samples", text="Dash Length")
if (mode == 'SCULPT' and brush.sculpt_capabilities.has_jitter) or mode != 'SCULPT':
col.separator()
row = col.row(align=True)
if brush.jitter_unit == 'BRUSH':
row.prop(brush, "jitter", slider=True)
else:
row.prop(brush, "jitter_absolute")
row.prop(brush, "use_pressure_jitter", toggle=True, text="")
if self.is_popover is False:
row.prop(
settings,
"show_jitter_curve",
icon='DOWNARROW_HLT' if settings.show_jitter_curve else 'RIGHTARROW',
text="",
emboss=False)
# Pen pressure mapping curve for Jitter.
if settings.show_jitter_curve and self.is_popover is False:
subcol = col.column()
subcol.active = brush.use_pressure_jitter
subcol.template_curve_mapping(brush, "curve_jitter", brush=True, show_presets=True)
col.row().prop(brush, "jitter_unit", expand=True)
col.separator()
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"input_samples",
unified_name="use_unified_input_samples",
slider=True,
)
class SmoothStrokePanel(BrushPanel):
bl_label = "Stabilize Stroke"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
if not super().poll(context):
return False
settings = cls.paint_settings(context)
brush = settings.brush
if brush.brush_capabilities.has_smooth_stroke:
return True
return False
def draw_header(self, context):
settings = self.paint_settings(context)
brush = settings.brush
self.layout.use_property_split = False
self.layout.prop(brush, "use_smooth_stroke", text=self.bl_label if self.is_popover else "")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
settings = self.paint_settings(context)
brush = settings.brush
col = layout.column()
col.active = brush.use_smooth_stroke
col.prop(brush, "smooth_stroke_radius", text="Radius", slider=True)
col.prop(brush, "smooth_stroke_factor", text="Factor", slider=True)
class FalloffPanel(BrushPanel):
bl_label = "Falloff"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
if not super().poll(context):
return False
settings = cls.paint_settings(context)
if not (settings and settings.brush and settings.brush.curve_distance_falloff):
return False
if cls.get_brush_mode(context) == 'SCULPT_CURVES':
brush = settings.brush
if brush.curves_sculpt_brush_type in {'ADD', 'DELETE'}:
return False
return True
def draw(self, context):
layout = self.layout
settings = self.paint_settings(context)
mode = self.get_brush_mode(context)
brush = settings.brush
if brush is None:
return
col = layout.column(align=True)
if context.region.type == 'TOOL_HEADER':
col.prop(brush, "curve_distance_falloff_preset", expand=True)
else:
row = col.row(align=True)
col.prop(brush, "curve_distance_falloff_preset", text="")
if brush.curve_distance_falloff_preset == 'CUSTOM':
layout.template_curve_mapping(brush, "curve_distance_falloff", brush=True,
use_negative_slope=True, show_presets=True)
col = layout.column(align=True)
row = col.row(align=True)
show_falloff_shape = False
if mode in {'SCULPT', 'PAINT_VERTEX', 'PAINT_WEIGHT'} and brush.sculpt_brush_type != 'POSE':
show_falloff_shape = True
if not show_falloff_shape and mode == 'SCULPT_CURVES' and context.space_data.type == 'PROPERTIES':
show_falloff_shape = True
if show_falloff_shape:
col.separator()
row = col.row(align=True)
row.use_property_split = True
row.use_property_decorate = False
row.prop(brush, "falloff_shape", expand=True)
class DisplayPanel(BrushPanel):
bl_label = "Brush Cursor"
bl_options = {'DEFAULT_CLOSED'}
def draw_header(self, context):
settings = self.paint_settings(context)
if settings and not self.is_popover:
self.layout.prop(settings, "show_brush", text="")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
mode = self.get_brush_mode(context)
settings = self.paint_settings(context)
brush = settings.brush
tex_slot = brush.texture_slot
tex_slot_mask = brush.mask_texture_slot
if self.is_popover:
row = layout.row(align=True)
row.use_property_split = False
row.prop(settings, "show_brush", text="Display Cursor")
col = layout.column()
col.active = settings.show_brush
col.prop(brush, "cursor_color_add", text="Cursor Color")
if mode == 'SCULPT' and brush.sculpt_capabilities.has_secondary_color:
col.prop(brush, "cursor_color_subtract", text="Inverse Color")
col.separator()
row = col.row(align=True)
row.active = settings.show_brush
row.prop(brush, "cursor_overlay_alpha", text="Falloff Opacity")
row.prop(brush, "use_cursor_overlay_override", toggle=True, text="", icon='BRUSH_DATA')
row.prop(
brush, "use_cursor_overlay", text="", toggle=True,
icon='HIDE_OFF' if brush.use_cursor_overlay else 'HIDE_ON',
)
# TODO: These settings are a mess. Both `has_overlay` and the following two blocks should read the
# appropriate texture depending on the mode, see `BKE_brush_mask_texture_get` vs `BKE_brush_color_texture_get`
texture_overlay_settings_active = brush.brush_capabilities.has_overlay and settings.show_brush
if mode in {'PAINT_2D', 'PAINT_TEXTURE', 'PAINT_VERTEX', 'SCULPT'}:
row = col.row(align=True)
row.active = texture_overlay_settings_active
row.prop(brush, "texture_overlay_alpha", text="Texture Opacity")
row.prop(brush, "use_primary_overlay_override", toggle=True, text="", icon='BRUSH_DATA')
if tex_slot.map_mode != 'STENCIL':
row.prop(
brush, "use_primary_overlay", text="", toggle=True,
icon='HIDE_OFF' if brush.use_primary_overlay else 'HIDE_ON',
)
if mode in {'PAINT_TEXTURE', 'PAINT_2D'}:
row = col.row(align=True)
row.active = texture_overlay_settings_active
row.prop(brush, "mask_overlay_alpha", text="Mask Texture Opacity")
row.prop(brush, "use_secondary_overlay_override", toggle=True, text="", icon='BRUSH_DATA')
if tex_slot_mask.map_mode != 'STENCIL':
row.prop(
brush, "use_secondary_overlay", text="", toggle=True,
icon='HIDE_OFF' if brush.use_secondary_overlay else 'HIDE_ON',
)
class VIEW3D_MT_tools_projectpaint_clone(Menu):
bl_label = "Clone Layer"
def draw(self, context):
layout = self.layout
for i, uv_layer in enumerate(context.active_object.data.uv_layers):
props = layout.operator("wm.context_set_int", text=uv_layer.name, translate=False)
props.data_path = "active_object.data.uv_layer_clone_index"
props.value = i
def brush_settings(layout, context, brush, popover=False):
""" Draw simple brush settings for Sculpt,
Texture/Vertex/Weight Paint modes, or skip certain settings for the popover """
mode = UnifiedPaintPanel.get_brush_mode(context)
### Draw simple settings unique to each paint mode. ###
brush_shared_settings(layout, context, brush, popover)
# Sculpt Mode #
if mode == 'SCULPT':
capabilities = brush.sculpt_capabilities
sculpt_brush_type = brush.sculpt_brush_type
# normal_radius_factor
layout.prop(brush, "normal_radius_factor", slider=True)
if capabilities.has_tilt:
layout.prop(brush, "tilt_strength_factor", slider=True)
row = layout.row(align=True)
row.prop(brush, "hardness", slider=True)
if capabilities.has_hardness_pressure:
row.prop(brush, "invert_hardness_pressure", text="")
row.prop(brush, "use_hardness_pressure", text="")
# auto_smooth_factor and use_inverse_smooth_pressure
if capabilities.has_auto_smooth:
pressure_name = "use_inverse_smooth_pressure" if capabilities.has_auto_smooth_pressure else None
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"auto_smooth_factor",
pressure_name=pressure_name,
slider=True,
)
# topology_rake_factor
if (
capabilities.has_topology_rake and
context.sculpt_object.use_dynamic_topology_sculpting
):
layout.prop(brush, "topology_rake_factor", slider=True)
# normal_weight
if capabilities.has_normal_weight:
layout.prop(brush, "normal_weight", slider=True)
# crease_pinch_factor
if capabilities.has_pinch_factor:
text = iface_("Pinch")
if sculpt_brush_type in {'BLOB', 'SNAKE_HOOK'}:
text = iface_("Magnify")
layout.prop(brush, "crease_pinch_factor", slider=True, text=text, translate=False)
# rake_factor
if capabilities.has_rake_factor:
layout.prop(brush, "rake_factor", slider=True)
# plane_offset, use_offset_pressure, use_plane_trim, plane_trim
if capabilities.has_plane_offset:
layout.separator()
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"plane_offset",
pressure_name="use_offset_pressure",
slider=True,
)
if sculpt_brush_type != 'PLANE':
row = layout.row(heading="Plane Trim")
row.prop(brush, "use_plane_trim", text="")
sub = row.row()
sub.active = brush.use_plane_trim
sub.prop(brush, "plane_trim", slider=True, text="")
layout.separator()
# height
if capabilities.has_height:
layout.prop(brush, "height", slider=True, text="Height")
if capabilities.has_plane_height:
layout.prop(brush, "plane_height", slider=True, text="Height")
if capabilities.has_plane_depth:
layout.prop(brush, "plane_depth", slider=True, text="Depth")
# use_persistent, set_persistent_base
if capabilities.has_persistence:
layout.separator()
layout.prop(brush, "use_persistent")
layout.operator("sculpt.set_persistent_base")
layout.separator()
if capabilities.has_color:
ups = UnifiedPaintPanel.paint_settings(context).unified_paint_settings
row = layout.row(align=True)
UnifiedPaintPanel.prop_unified_color(row, context, brush, "color", text="")
UnifiedPaintPanel.prop_unified_color(row, context, brush, "secondary_color", text="")
row.separator()
row.operator("paint.brush_colors_flip", icon='FILE_REFRESH', text="", emboss=False)
row.prop(ups, "use_unified_color", text="", icon='BRUSHES_ALL')
layout.prop(brush, "blend", text="Blend Mode")
# Per sculpt tool options.
if sculpt_brush_type == 'CLAY_STRIPS':
row = layout.row()
row.prop(brush, "tip_roundness")
row = layout.row()
row.prop(brush, "tip_scale_x")
elif sculpt_brush_type == 'ELASTIC_DEFORM':
layout.separator()
layout.prop(brush, "elastic_deform_type")
layout.prop(brush, "elastic_deform_volume_preservation", slider=True)
layout.separator()
elif sculpt_brush_type == 'SNAKE_HOOK':
layout.separator()
layout.prop(brush, "snake_hook_deform_type")
layout.separator()
elif sculpt_brush_type == 'POSE':
layout.separator()
layout.prop(brush, "deform_target")
layout.separator()
layout.prop(brush, "pose_deform_type")
layout.prop(brush, "pose_origin_type")
layout.prop(brush, "pose_offset")
layout.prop(brush, "pose_smooth_iterations")
if brush.pose_deform_type == 'ROTATE_TWIST' and brush.pose_origin_type in {'TOPOLOGY', 'FACE_SETS'}:
layout.prop(brush, "pose_ik_segments")
if brush.pose_deform_type == 'SCALE_TRANSLATE':
layout.prop(brush, "use_pose_lock_rotation")
layout.prop(brush, "use_pose_ik_anchored")
layout.prop(brush, "use_connected_only")
layout.prop(brush, "disconnected_distance_max")
layout.separator()
elif sculpt_brush_type == 'CLOTH':
layout.separator()
layout.prop(brush, "cloth_simulation_area_type")
if brush.cloth_simulation_area_type != 'GLOBAL':
layout.prop(brush, "cloth_sim_limit")
layout.prop(brush, "cloth_sim_falloff")
if brush.cloth_simulation_area_type == 'LOCAL':
layout.prop(brush, "use_cloth_pin_simulation_boundary")
layout.separator()
layout.prop(brush, "cloth_deform_type")
layout.prop(brush, "cloth_force_falloff_type")
layout.separator()
layout.prop(brush, "cloth_mass")
layout.prop(brush, "cloth_damping")
layout.prop(brush, "cloth_constraint_softbody_strength")
layout.separator()
layout.prop(brush, "use_cloth_collision")
layout.separator()
elif sculpt_brush_type == 'PLANE':
row = layout.row(align=True)
row.prop(brush, "area_radius_factor")
row.prop(brush, "use_pressure_area_radius", text="")
layout.separator()
layout.prop(brush, "plane_inversion_mode")
layout.separator()
layout.prop(brush, "stabilize_normal")
layout.prop(brush, "stabilize_plane")
elif sculpt_brush_type == 'GRAB':
layout.prop(brush, "use_grab_active_vertex")
layout.prop(brush, "use_grab_silhouette")
elif sculpt_brush_type == 'PAINT':
row = layout.row(align=True)
row.prop(brush, "flow")
row.prop(brush, "invert_flow_pressure", text="")
row.prop(brush, "use_flow_pressure", text="")
row = layout.row(align=True)
row.prop(brush, "wet_mix")
row.prop(brush, "invert_wet_mix_pressure", text="")
row.prop(brush, "use_wet_mix_pressure", text="")
row = layout.row(align=True)
row.prop(brush, "wet_persistence")
row.prop(brush, "invert_wet_persistence_pressure", text="")
row.prop(brush, "use_wet_persistence_pressure", text="")
row = layout.row(align=True)
row.prop(brush, "wet_paint_radius_factor")
row = layout.row(align=True)
row.prop(brush, "density")
row.prop(brush, "invert_density_pressure", text="")
row.prop(brush, "use_density_pressure", text="")
row = layout.row()
row.prop(brush, "tip_roundness")
row = layout.row()
row.prop(brush, "tip_scale_x")
elif sculpt_brush_type == 'SMEAR':
col = layout.column()
col.prop(brush, "smear_deform_type")
elif sculpt_brush_type == 'BOUNDARY':
layout.prop(brush, "deform_target")
layout.separator()
col = layout.column()
col.prop(brush, "boundary_deform_type")
col.prop(brush, "boundary_falloff_type")
col.prop(brush, "boundary_offset")
elif sculpt_brush_type == 'TOPOLOGY':
col = layout.column()
col.prop(brush, "slide_deform_type")
elif sculpt_brush_type == 'MULTIPLANE_SCRAPE':
col = layout.column()
col.prop(brush, "multiplane_scrape_angle")
col.prop(brush, "use_multiplane_scrape_dynamic")
col.prop(brush, "show_multiplane_scrape_planes_preview")
elif sculpt_brush_type == 'SMOOTH':
col = layout.column()
col.prop(brush, "smooth_deform_type")
if brush.smooth_deform_type == 'SURFACE':
col.prop(brush, "surface_smooth_shape_preservation")
col.prop(brush, "surface_smooth_current_vertex")
col.prop(brush, "surface_smooth_iterations")
elif sculpt_brush_type == 'DISPLACEMENT_SMEAR':
col = layout.column()
col.prop(brush, "smear_deform_type")
elif sculpt_brush_type == 'MASK':
layout.row().prop(brush, "mask_tool", expand=True)
# End sculpt_brush_type interface.
# 3D and 2D Texture Paint Mode.
elif mode in {'PAINT_TEXTURE', 'PAINT_2D'}:
capabilities = brush.image_paint_capabilities
if brush.image_brush_type == 'FILL':
# For some reason fill threshold only appears to be implemented in 2D paint.
if brush.color_type == 'COLOR':
if mode == 'PAINT_2D':
layout.prop(brush, "fill_threshold", text="Fill Threshold", slider=True)
elif brush.color_type == 'GRADIENT':
layout.row().prop(brush, "gradient_fill_mode", expand=True)
elif mode == 'SCULPT_CURVES':
if brush.curves_sculpt_brush_type == 'ADD':
layout.prop(brush.curves_sculpt_settings, "add_amount")
col = layout.column(heading="Interpolate", align=True)
col.prop(brush.curves_sculpt_settings, "use_length_interpolate", text="Length")
col.prop(brush.curves_sculpt_settings, "use_radius_interpolate", text="Radius")
col.prop(brush.curves_sculpt_settings, "use_shape_interpolate", text="Shape")
col.prop(brush.curves_sculpt_settings, "use_point_count_interpolate", text="Point Count")
col = layout.column()
col.active = not brush.curves_sculpt_settings.use_length_interpolate
col.prop(brush.curves_sculpt_settings, "curve_length", text="Length")
col = layout.column()
col.active = not brush.curves_sculpt_settings.use_radius_interpolate
col.prop(brush.curves_sculpt_settings, "curve_radius", text="Radius")
col = layout.column()
col.active = not brush.curves_sculpt_settings.use_point_count_interpolate
col.prop(brush.curves_sculpt_settings, "points_per_curve", text="Points")
if brush.curves_sculpt_brush_type == 'DENSITY':
col = layout.column()
col.prop(brush.curves_sculpt_settings, "density_add_attempts", text="Count Max")
col = layout.column(heading="Interpolate", align=True)
col.prop(brush.curves_sculpt_settings, "use_length_interpolate", text="Length")
col.prop(brush.curves_sculpt_settings, "use_radius_interpolate", text="Radius")
col.prop(brush.curves_sculpt_settings, "use_shape_interpolate", text="Shape")
col.prop(brush.curves_sculpt_settings, "use_point_count_interpolate", text="Point Count")
col = layout.column()
col.active = not brush.curves_sculpt_settings.use_length_interpolate
col.prop(brush.curves_sculpt_settings, "curve_length", text="Length")
col = layout.column()
col.active = not brush.curves_sculpt_settings.use_radius_interpolate
col.prop(brush.curves_sculpt_settings, "curve_radius", text="Radius")
col = layout.column()
col.active = not brush.curves_sculpt_settings.use_point_count_interpolate
col.prop(brush.curves_sculpt_settings, "points_per_curve", text="Points")
elif brush.curves_sculpt_brush_type == 'GROW_SHRINK':
layout.prop(brush.curves_sculpt_settings, "use_uniform_scale")
layout.prop(brush.curves_sculpt_settings, "minimum_length")
def brush_shared_settings(layout, context, brush, popover=False):
""" Draw simple brush settings that are shared between different paint modes. """
paint = UnifiedPaintPanel.paint_settings(context)
mode = UnifiedPaintPanel.get_brush_mode(context)
### Determine which settings to draw. ###
blend_mode = False
size = False
size_mode = False
size_pressure = False
strength = False
strength_pressure = False
size_pressure = False
weight = False
direction = False
# 3D and 2D Texture Paint #
if mode in {'PAINT_TEXTURE', 'PAINT_2D'}:
if not popover:
blend_mode = brush.image_paint_capabilities.has_color
size = brush.image_paint_capabilities.has_radius
strength = strength_pressure = True
size_pressure = True
# Sculpt #
if mode == 'SCULPT':
size_mode = True
if not popover:
size = True
strength = True
strength_pressure = brush.sculpt_capabilities.has_strength_pressure
direction = brush.sculpt_capabilities.has_direction
size_pressure = brush.sculpt_capabilities.has_size_pressure
# Vertex Paint #
if mode == 'PAINT_VERTEX':
if not popover:
blend_mode = True
size = True
strength = True
strength_pressure = True
size_pressure = True
# Weight Paint #
if mode == 'PAINT_WEIGHT':
if not popover:
size = True
weight = brush.weight_paint_capabilities.has_weight
strength = strength_pressure = True
size_pressure = True
# Only draw blend mode for the Draw tool, because for other tools it is pointless. D5928#137944
if brush.weight_brush_type == 'DRAW':
blend_mode = True
# Sculpt Curves #
if mode == 'SCULPT_CURVES':
tool = brush.curves_sculpt_brush_type
size = True
strength = tool not in {'ADD', 'DELETE'}
direction = tool in {'GROW_SHRINK', 'SELECTION_PAINT'}
strength_pressure = tool not in {'SLIDE', 'ADD', 'DELETE'}
size_pressure = True
# Grease Pencil #
if mode == 'PAINT_GREASE_PENCIL':
size_mode = True
size = True
strength = True
# Grease Pencil #
if mode == 'SCULPT_GREASE_PENCIL':
size = True
strength = True
size_pressure = True
### Draw settings. ###
ups = UnifiedPaintPanel.paint_settings(context).unified_paint_settings
if blend_mode:
layout.prop(brush, "blend", text="Blend")
layout.separator()
if weight:
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"weight",
unified_name="use_unified_weight",
slider=True,
)
size_owner = ups if ups.use_unified_size else brush
size_prop = "size"
if size_mode and (size_owner.use_locked_size == 'SCENE'):
size_prop = "unprojected_size"
if size or size_mode:
if size:
pressure_name = "use_pressure_size" if size_pressure else None
curve_visibility_name = "show_size_curve" if size_pressure else None
# Grease Pencil Sculpt uses size pressure but doesn't map to a custom curve, this is a weird case where we
# don't show the dropdown but do show the pressure toggle.
# TODO: Add curve support for GP Sculpt
if mode == 'SCULPT_GREASE_PENCIL':
curve_visibility_name = None
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
size_prop,
unified_name="use_unified_size",
pressure_name=pressure_name,
curve_visibility_name=curve_visibility_name,
text="Size",
slider=True,
)
if mode in {'PAINT_TEXTURE', 'PAINT_2D', 'SCULPT', 'PAINT_VERTEX', 'PAINT_WEIGHT', 'SCULPT_CURVES'}:
if paint.show_size_curve and not popover:
subcol = layout.column()
subcol.active = brush.use_pressure_size
subcol.template_curve_mapping(brush, "curve_size", brush=True, show_presets=True)
if size_mode:
layout.row().prop(size_owner, "use_locked_size", expand=True)
layout.separator()
if strength:
pressure_name = "use_pressure_strength" if strength_pressure else None
curve_visibility_name = "show_strength_curve" if strength_pressure else None
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"strength",
unified_name="use_unified_strength",
pressure_name=pressure_name,
curve_visibility_name=curve_visibility_name,
slider=True,
)
if mode in {'PAINT_TEXTURE', 'PAINT_2D', 'SCULPT', 'PAINT_VERTEX', 'PAINT_WEIGHT', 'SCULPT_CURVES'}:
if paint.show_strength_curve and not popover:
subcol = layout.column()
subcol.active = brush.use_pressure_strength
subcol.template_curve_mapping(brush, "curve_strength", brush=True, show_presets=True)
layout.separator()
if direction:
layout.row().prop(brush, "direction", expand=True)
def color_jitter_panel(layout, context, brush):
mode = UnifiedPaintPanel.get_brush_mode(context)
ups = UnifiedPaintPanel.paint_settings(context).unified_paint_settings
is_sculpt_paint_mode = mode == 'SCULPT' and brush.sculpt_capabilities.has_color
if mode in {'PAINT_TEXTURE', 'PAINT_2D', 'PAINT_VERTEX'} or is_sculpt_paint_mode:
prop_owner = ups if ups.use_unified_color else brush
layout.use_property_split = False
header, panel = layout.panel("color_jitter_panel", default_closed=True)
header.prop(prop_owner, "use_color_jitter", text="Randomize Color")
if panel:
panel.use_property_split = True
panel.use_property_decorate = False
col = panel.column(align=True)
col.use_property_split = True
row = col.row(align=True)
row.enabled = prop_owner.use_color_jitter
row.prop(prop_owner, "hue_jitter", slider=True, text="Hue")
row.prop(prop_owner, "use_stroke_random_hue", text="", icon='GP_SELECT_STROKES')
row.prop(prop_owner, "use_random_press_hue", text="", icon='STYLUS_PRESSURE')
row = col.row(align=True)
row.enabled = prop_owner.use_color_jitter
row.prop(prop_owner, "saturation_jitter", slider=True, text="Saturation")
row.prop(prop_owner, "use_stroke_random_sat", text="", icon='GP_SELECT_STROKES')
row.prop(prop_owner, "use_random_press_sat", text="", icon='STYLUS_PRESSURE')
row = col.row(align=True)
row.enabled = prop_owner.use_color_jitter
row.prop(prop_owner, "value_jitter", slider=True, text="Value", text_context=i18n_contexts.color)
row.prop(prop_owner, "use_stroke_random_val", text="", icon='GP_SELECT_STROKES')
row.prop(prop_owner, "use_random_press_val", text="", icon='STYLUS_PRESSURE')
def brush_settings_advanced(layout, context, settings, brush, popover=False):
"""Draw advanced brush settings for Sculpt, Texture/Vertex/Weight Paint modes."""
mode = UnifiedPaintPanel.get_brush_mode(context)
# In the popover we want to combine advanced brush settings with non-advanced brush settings.
if popover:
brush_settings(layout, context, brush, popover=True)
layout.separator()
layout.label(text="Advanced")
# These options are shared across many modes.
use_accumulate = False
use_frontface = False
if mode == 'SCULPT':
layout.prop(brush, "sculpt_brush_type")
layout.separator()
capabilities = brush.sculpt_capabilities
use_accumulate = capabilities.has_accumulate
use_frontface = True
col = layout.column(heading="Auto-Masking", align=True)
col.prop(brush, "use_automasking_topology", text="Topology")
col.prop(brush, "use_automasking_face_sets", text="Face Sets")
layout.separator()
col = layout.column(align=True)
row = col.row()
row.prop(brush, "use_automasking_boundary_edges", text="Mesh Boundary")
if brush.use_automasking_boundary_edges:
props = row.operator("sculpt.mask_from_boundary", text="Create Mask")
props.settings_source = 'BRUSH'
props.boundary_mode = 'MESH'
row = col.row()
row.prop(brush, "use_automasking_boundary_face_sets", text="Face Sets Boundary")
if brush.use_automasking_boundary_face_sets:
props = row.operator("sculpt.mask_from_boundary", text="Create Mask")
props.settings_source = 'BRUSH'
props.boundary_mode = 'FACE_SETS'
if brush.use_automasking_boundary_edges or brush.use_automasking_boundary_face_sets:
col = layout.column()
col.use_property_split = False
split = col.split(factor=0.4)
col = split.column()
split.prop(brush, "automasking_boundary_edges_propagation_steps")
layout.separator()
col = layout.column(align=True)
row = col.row()
row.prop(brush, "use_automasking_cavity", text="Cavity")
is_cavity_active = brush.use_automasking_cavity or brush.use_automasking_cavity_inverted
if is_cavity_active:
props = row.operator("sculpt.mask_from_cavity", text="Create Mask")
props.settings_source = 'BRUSH'
col.prop(brush, "use_automasking_cavity_inverted", text="Cavity (inverted)")
if is_cavity_active:
col = layout.column(align=True)
col.prop(brush, "automasking_cavity_factor", text="Factor")
col.prop(brush, "automasking_cavity_blur_steps", text="Blur")
col = layout.column()
col.prop(brush, "use_automasking_custom_cavity_curve", text="Custom Curve")
if brush.use_automasking_custom_cavity_curve:
col.template_curve_mapping(brush, "automasking_cavity_curve", brush=True)
layout.separator()
col = layout.column(align=True)
col.prop(brush, "use_automasking_view_normal", text="View Normal")
if brush.use_automasking_view_normal:
col.prop(brush, "use_automasking_view_occlusion", text="Occlusion")
subcol = col.column(align=True)
subcol.active = not brush.use_automasking_view_occlusion
subcol.prop(brush, "automasking_view_normal_limit", text="Limit")
subcol.prop(brush, "automasking_view_normal_falloff", text="Falloff")
col = layout.column()
col.prop(brush, "use_automasking_start_normal", text="Area Normal")
if brush.use_automasking_start_normal:
col = layout.column(align=True)
col.prop(brush, "automasking_start_normal_limit", text="Limit")
col.prop(brush, "automasking_start_normal_falloff", text="Falloff")
layout.separator()
# sculpt plane settings
if capabilities.has_sculpt_plane:
layout.prop(brush, "sculpt_plane")
if brush.sculpt_brush_type != 'PLANE':
col = layout.column(heading="Original", align=True)
col.prop(brush, "use_original_normal", text="Normal")
col.prop(brush, "use_original_plane", text="Plane")
layout.separator()
elif mode == 'SCULPT_GREASE_PENCIL':
gp_settings = brush.gpencil_settings
col = layout.column(heading="Affect", align=True)
col.prop(gp_settings, "use_edit_position", text="Position")
col.prop(gp_settings, "use_edit_strength", text="Strength", text_ctxt=i18n_contexts.id_gpencil)
col.prop(gp_settings, "use_edit_thickness", text="Thickness")
col.prop(gp_settings, "use_edit_uv", text="UV")
# 3D and 2D Texture Paint.
elif mode in {'PAINT_TEXTURE', 'PAINT_2D'}:
layout.prop(brush, "image_brush_type")
layout.separator()
capabilities = brush.image_paint_capabilities
use_accumulate = capabilities.has_accumulate
if mode == 'PAINT_2D':
layout.prop(brush, "use_paint_antialiasing")
else:
layout.prop(brush, "use_alpha")
# Tool specific settings
if brush.image_brush_type == 'SOFTEN':
layout.separator()
layout.row().prop(brush, "direction", expand=True)
layout.prop(brush, "sharp_threshold")
if mode == 'PAINT_2D':
layout.prop(brush, "blur_kernel_radius")
layout.prop(brush, "blur_mode")
elif brush.image_brush_type == 'MASK':
layout.prop(brush, "weight", text="Mask Value", slider=True)
elif brush.image_brush_type == 'CLONE':
if mode == 'PAINT_2D':
layout.prop(settings, "clone_image", text="Image")
layout.prop(settings, "clone_alpha", text="Alpha")
# Vertex Paint #
elif mode == 'PAINT_VERTEX':
layout.prop(brush, "vertex_brush_type")
layout.separator()
layout.prop(brush, "use_alpha")
if brush.vertex_brush_type != 'SMEAR':
use_accumulate = True
use_frontface = True
# Weight Paint
elif mode == 'PAINT_WEIGHT':
layout.prop(brush, "weight_brush_type")
layout.separator()
if brush.weight_brush_type != 'SMEAR':
use_accumulate = True
use_frontface = True
# Sculpt Curves
elif mode == 'SCULPT_CURVES':
layout.prop(brush, "curves_sculpt_brush_type")
# Draw shared settings.
if use_accumulate:
layout.prop(brush, "use_accumulate")
if use_frontface:
layout.prop(brush, "use_frontface", text="Front Faces Only")
if popover:
color_jitter_panel(layout, context, brush)
# Brush modes
header, panel = layout.panel("modes", default_closed=True)
header.label(text="Modes")
if panel:
panel.use_property_split = True
panel.use_property_decorate = False
col = panel.column(align=True)
col.prop(brush, "use_paint_sculpt", text="Sculpt")
col.prop(brush, "use_paint_uv_sculpt", text="UV Sculpt")
col.prop(brush, "use_paint_vertex", text="Vertex Paint")
col.prop(brush, "use_paint_weight", text="Weight Paint")
col.prop(brush, "use_paint_image", text="Texture Paint")
col.prop(brush, "use_paint_sculpt_curves", text="Sculpt Curves")
def draw_color_settings(context, layout, brush, color_type=False):
"""Draw color wheel and gradient settings."""
ups = UnifiedPaintPanel.paint_settings(context).unified_paint_settings
if color_type:
row = layout.row()
row.use_property_split = False
row.prop(brush, "color_type", expand=True)
# Color wheel
if brush.color_type == 'COLOR':
UnifiedPaintPanel.prop_unified_color_picker(layout, context, brush, "color", value_slider=True)
row = layout.row(align=True)
UnifiedPaintPanel.prop_unified_color(row, context, brush, "color", text="")
UnifiedPaintPanel.prop_unified_color(row, context, brush, "secondary_color", text="")
row.separator()
row.operator("paint.brush_colors_flip", icon='FILE_REFRESH', text="", emboss=False)
row.prop(ups, "use_unified_color", text="", icon='BRUSHES_ALL')
color_jitter_panel(layout, context, brush)
# Gradient
elif brush.color_type == 'GRADIENT':
layout.template_color_ramp(brush, "gradient", expand=True)
layout.use_property_split = True
col = layout.column()
if brush.image_brush_type == 'DRAW':
UnifiedPaintPanel.prop_unified(
col,
context,
brush,
"secondary_color",
unified_name="use_unified_color",
text="Background Color",
header=True,
)
col.prop(brush, "gradient_stroke_mode", text="Gradient Mapping")
if brush.gradient_stroke_mode in {'SPACING_REPEAT', 'SPACING_CLAMP'}:
col.prop(brush, "grad_spacing")
# Used in both the View3D toolbar and texture properties
def brush_texture_settings(layout, brush, sculpt):
tex_slot = brush.texture_slot
layout.use_property_split = True
layout.use_property_decorate = False
# map_mode
layout.prop(tex_slot, "map_mode", text="Mapping")
layout.separator()
if tex_slot.map_mode == 'STENCIL':
if brush.texture and brush.texture.type == 'IMAGE':
layout.operator("brush.stencil_fit_image_aspect").mask = False
layout.operator("brush.stencil_reset_transform").mask = False
# angle and texture_angle_source
if tex_slot.has_texture_angle:
col = layout.column()
col.prop(tex_slot, "angle", text="Angle")
if tex_slot.has_texture_angle_source:
col.prop(tex_slot, "use_rake", text="Rake")
if brush.brush_capabilities.has_random_texture_angle and tex_slot.has_random_texture_angle:
if sculpt:
if brush.sculpt_capabilities.has_random_texture_angle:
col.prop(tex_slot, "use_random", text="Random")
if tex_slot.use_random:
col.prop(tex_slot, "random_angle", text="Random Angle")
else:
col.prop(tex_slot, "use_random", text="Random")
if tex_slot.use_random:
col.prop(tex_slot, "random_angle", text="Random Angle")
# scale and offset
layout.prop(tex_slot, "offset")
layout.prop(tex_slot, "scale")
if sculpt:
# texture_sample_bias
layout.prop(brush, "texture_sample_bias", slider=True, text="Sample Bias")
if brush.sculpt_brush_type == 'DRAW':
col = layout.column()
col.active = tex_slot.map_mode == 'AREA_PLANE'
col.prop(brush, "use_color_as_displacement", text="Vector Displacement")
def brush_mask_texture_settings(layout, brush):
mask_tex_slot = brush.mask_texture_slot
layout.use_property_split = True
layout.use_property_decorate = False
# map_mode
layout.row().prop(mask_tex_slot, "mask_map_mode", text="Mask Mapping")
if mask_tex_slot.map_mode == 'STENCIL':
if brush.mask_texture and brush.mask_texture.type == 'IMAGE':
layout.operator("brush.stencil_fit_image_aspect").mask = True
layout.operator("brush.stencil_reset_transform").mask = True
col = layout.column()
col.prop(brush, "use_pressure_masking", text="Pressure Masking")
# angle and texture_angle_source
if mask_tex_slot.has_texture_angle:
col = layout.column()
col.prop(mask_tex_slot, "angle", text="Angle")
if mask_tex_slot.has_texture_angle_source:
col.prop(mask_tex_slot, "use_rake", text="Rake")
if brush.brush_capabilities.has_random_texture_angle and mask_tex_slot.has_random_texture_angle:
col.prop(mask_tex_slot, "use_random", text="Random")
if mask_tex_slot.use_random:
col.prop(mask_tex_slot, "random_angle", text="Random Angle")
# scale and offset
col.prop(mask_tex_slot, "offset")
col.prop(mask_tex_slot, "scale")
def brush_basic_texpaint_settings(layout, context, brush, *, compact=False):
"""Draw Tool Settings header for Vertex Paint and 2D and 3D Texture Paint modes."""
capabilities = brush.image_paint_capabilities
if capabilities.has_color:
row = layout.row(align=True)
row.ui_units_x = 4
UnifiedPaintPanel.prop_unified_color(row, context, brush, "color", text="")
UnifiedPaintPanel.prop_unified_color(row, context, brush, "secondary_color", text="")
row.separator()
layout.prop(brush, "blend", text="" if compact else iface_("Blend"), translate=False)
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"size",
pressure_name="use_pressure_size",
unified_name="use_unified_size",
slider=True,
text="Size",
header=True,
)
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"strength",
pressure_name="use_pressure_strength",
unified_name="use_unified_strength",
header=True,
)
def brush_basic__draw_color_selector(context, layout, brush, gp_settings):
tool_settings = context.scene.tool_settings
settings = tool_settings.gpencil_paint
ma = gp_settings.material
row = layout.row(align=True)
if not gp_settings.use_material_pin:
ma = context.object.active_material
icon_id = 0
txt_ma = ""
if ma:
ma.id_data.preview_ensure()
if ma.id_data.preview:
icon_id = ma.id_data.preview.icon_id
txt_ma = ma.name
maxw = 25
if len(txt_ma) > maxw:
txt_ma = txt_ma[:maxw - 5] + '..' + txt_ma[-3:]
sub = row.row(align=True)
sub.enabled = not gp_settings.use_material_pin
sub.ui_units_x = 8
sub.popover(
panel="TOPBAR_PT_grease_pencil_materials",
text=txt_ma,
translate=False,
icon_value=icon_id,
)
row.prop(gp_settings, "use_material_pin", text="")
if brush.gpencil_brush_type in {'DRAW', 'FILL'}:
row.separator(factor=1.0)
sub_row = row.row(align=True)
pin_draw_mode = gp_settings.pin_draw_mode
sub_row.enabled = not pin_draw_mode
if pin_draw_mode:
sub_row.prop_enum(gp_settings, "brush_draw_mode", 'MATERIAL', text="", icon='MATERIAL')
sub_row.prop_enum(gp_settings, "brush_draw_mode", 'VERTEXCOLOR', text="", icon='VPAINT_HLT')
else:
sub_row.prop_enum(settings, "color_mode", 'MATERIAL', text="", icon='MATERIAL')
sub_row.prop_enum(settings, "color_mode", 'VERTEXCOLOR', text="", icon='VPAINT_HLT')
show_vertex_color = ((not pin_draw_mode) and settings.color_mode == 'VERTEXCOLOR') or \
(pin_draw_mode and gp_settings.brush_draw_mode == 'VERTEXCOLOR')
if show_vertex_color:
sub_row = row.row(align=True)
sub_row.enabled = show_vertex_color
sub_row.scale_x = 0.8
sub_row.prop_with_popover(brush, "color", text="", panel="TOPBAR_PT_grease_pencil_vertex_color")
row.prop(gp_settings, "pin_draw_mode", text="")
def brush_basic_grease_pencil_paint_settings(layout, context, brush, props, *, compact=False):
gp_settings = brush.gpencil_settings
paint = context.tool_settings.gpencil_paint
tool = context.workspace.tools.from_space_view3d_mode(context.mode, create=False)
if gp_settings is None:
return
grease_pencil_brush_type = brush.gpencil_brush_type
if grease_pencil_brush_type in {'DRAW', 'ERASE', 'TINT'} or tool.idname in {
"builtin.arc",
"builtin.curve",
"builtin.line",
"builtin.box",
"builtin.circle",
"builtin.polyline",
}:
size = "size"
if brush.use_locked_size == 'SCENE' and (grease_pencil_brush_type == 'DRAW' or tool.idname in {
"builtin.arc",
"builtin.curve",
"builtin.line",
"builtin.box",
"builtin.circle",
"builtin.polyline",
}):
size = "unprojected_size"
row = layout.row(align=True)
row.prop(brush, size, slider=True, text="Size")
row.prop(brush, "use_pressure_size", text="")
if not compact:
row.prop(
paint,
"show_size_curve",
text="",
icon='DOWNARROW_HLT' if paint.show_size_curve else 'RIGHTARROW',
emboss=False)
if paint.show_size_curve:
col = layout.column()
col.active = brush.use_pressure_size
col.template_curve_mapping(gp_settings, "curve_sensitivity", brush=True, show_presets=True)
row = layout.row(align=True)
row.prop(brush, "strength", slider=True, text="Strength")
row.prop(brush, "use_pressure_strength", text="")
if not compact:
row.prop(
paint,
"show_strength_curve",
text="",
icon='DOWNARROW_HLT' if paint.show_strength_curve else 'RIGHTARROW',
emboss=False)
if paint.show_strength_curve:
col = layout.column()
col.active = brush.use_pressure_strength
col.template_curve_mapping(gp_settings, "curve_strength", brush=True, show_presets=True)
if props:
layout.prop(props, "subdivision")
# Brush details
if tool.idname in {
"builtin.arc",
"builtin.curve",
"builtin.line",
"builtin.box",
"builtin.circle",
"builtin.polyline",
}:
row = layout.row(align=True)
if context.region.type == 'TOOL_HEADER':
row.prop(gp_settings, "caps_type", text="", expand=True)
else:
row.prop(gp_settings, "caps_type", text="Caps Type")
settings = context.tool_settings.gpencil_sculpt
if compact:
row = layout.row(align=True)
row.prop(settings, "use_thickness_curve", text="", icon='SPHERECURVE')
sub = row.row(align=True)
sub.active = settings.use_thickness_curve
sub.popover(
panel="TOPBAR_PT_gpencil_primitive",
text="Thickness Profile",
)
else:
row = layout.row(align=True)
row.prop(settings, "use_thickness_curve", text="Use Thickness Profile")
sub = row.row(align=True)
if settings.use_thickness_curve:
# Pressure curve.
layout.template_curve_mapping(settings, "thickness_primitive_curve", brush=True)
elif grease_pencil_brush_type == 'DRAW':
row = layout.row(align=True)
if compact:
row.prop(gp_settings, "caps_type", text="", expand=True)
else:
row.prop(gp_settings, "caps_type", text="Caps Type")
elif brush.gpencil_brush_type == 'FILL':
use_property_split_prev = layout.use_property_split
if compact:
row = layout.row(align=True)
row.prop(gp_settings, "fill_direction", text="", expand=True)
else:
layout.use_property_split = False
row = layout.row(align=True)
row.prop(gp_settings, "fill_direction", expand=True)
row = layout.row(align=True)
row.prop(gp_settings, "fill_factor")
row = layout.row(align=True)
row.prop(gp_settings, "dilate")
row = layout.row(align=True)
row.prop(brush, "size", text="Thickness")
layout.use_property_split = use_property_split_prev
elif grease_pencil_brush_type == 'ERASE':
layout.prop(gp_settings, "eraser_mode", expand=True)
layout.prop(gp_settings, "use_active_layer_only")
if gp_settings.eraser_mode in {'HARD', 'SOFT'}:
layout.prop(gp_settings, "use_keep_caps_eraser")
elif grease_pencil_brush_type == 'TINT':
layout.prop(gp_settings, "vertex_mode", text="Mode")
layout.popover("VIEW3D_PT_tools_brush_falloff")
layout.prop(gp_settings, "use_active_layer_only")
def brush_basic_gpencil_vertex_settings(layout, context, brush, *, compact=False):
del compact # UNUSED.
gp_settings = brush.gpencil_settings
ups = context.tool_settings.gpencil_vertex_paint.unified_paint_settings
brush_prop_owner = ups if ups.use_unified_size else brush
# Brush details
row = layout.row(align=True)
row.prop(brush, "size", text="Size")
row.prop(brush, "use_pressure_size", text="", icon='STYLUS_PRESSURE')
if brush.gpencil_vertex_brush_type in {'DRAW', 'BLUR', 'SMEAR'}:
row = layout.row(align=True)
row.prop(brush_prop_owner, "strength", slider=True)
row.prop(brush, "use_pressure_strength", text="", icon='STYLUS_PRESSURE')
row.prop(ups, "use_unified_strength", text="", icon='BRUSHES_ALL')
if brush.gpencil_vertex_brush_type in {'DRAW', 'REPLACE'}:
row = layout.row(align=True)
row.prop(gp_settings, "vertex_mode", text="Mode")
def brush_basic_grease_pencil_weight_settings(layout, context, brush, *, compact=False):
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"size",
pressure_name="use_pressure_size",
unified_name="use_unified_size",
text="Size",
slider=True,
header=compact,
)
capabilities = brush.sculpt_capabilities
pressure_name = "use_pressure_strength" if capabilities.has_strength_pressure else None
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"strength",
pressure_name=pressure_name,
unified_name="use_unified_strength",
text="Strength",
header=compact,
)
if brush.gpencil_weight_brush_type in {'WEIGHT'}:
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"weight",
unified_name="use_unified_weight",
text="Weight",
slider=True,
header=compact,
)
layout.prop(brush, "direction", expand=True, text="" if compact else "Direction")
def brush_basic_grease_pencil_vertex_settings(layout, context, brush, *, compact=False):
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"size",
pressure_name="use_pressure_size",
unified_name="use_unified_size",
text="Size",
slider=True,
header=compact,
)
if brush.gpencil_vertex_brush_type in {'DRAW', 'BLUR', 'SMEAR'}:
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"strength",
pressure_name="use_pressure_strength",
unified_name="use_unified_strength",
text="Strength",
header=compact,
)
gp_settings = brush.gpencil_settings
if brush.gpencil_vertex_brush_type in {'DRAW', 'REPLACE'}:
row = layout.row(align=True)
row.prop(gp_settings, "vertex_mode", text="Mode")
classes = (
VIEW3D_PT_brush_asset_shelf_filter,
VIEW3D_MT_tools_projectpaint_clone,
)
if __name__ == "__main__": # only for live edit.
from bpy.utils import register_class
for cls in classes:
register_class(cls)