Files
blender/release/scripts/startup/bl_ui/properties_paint_common.py
Pablo Dobarro 9120191fe2 Sculpt: Pose Brush Face Sets origin mode
This commit introduces a new mode for calculating the positions and
weights of the IK segments in the Pose Brush based on the Face Sets.

The first segment of the chain will always include all face sets inside
the brush radius and it will propagate until the boundary of the last
face sets added in the flood fill. Then consecutive connected face sets
are added to the chain until the chain length limit is reached or all
face sets of the mesh are already part of the chain.

This feature enables complete control over the pose brush origins in
case that is needed. Also, with this mode, the user can have a library
of base meshes with face sets already configured to get to the initial
pose as fast as possible.

Reviewed By: jbakker

Differential Revision: https://developer.blender.org/D7235
2020-03-27 18:15:42 +01:00

1187 lines
41 KiB
Python

# ##### 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 #####
# <pep8 compliant>
from bpy.types import Menu
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.has_datablock:
# tool.has_datablock is always true for tools that 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':
if space_data.show_uvedit:
return 'UV_SCULPT'
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 == 'UV_SCULPT':
return tool_settings.uv_sculpt
# Grease Pencil settings
elif mode == 'PAINT_GPENCIL':
return tool_settings.gpencil_paint
elif mode == 'SCULPT_GPENCIL':
return tool_settings.gpencil_sculpt_paint
elif mode == 'WEIGHT_GPENCIL':
return tool_settings.gpencil_weight_paint
elif mode == 'VERTEX_GPENCIL':
return tool_settings.gpencil_vertex_paint
return None
@staticmethod
def prop_unified(
layout,
context,
brush,
prop_name,
unified_name=None,
pressure_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. """
row = layout.row(align=True)
ups = context.tool_settings.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 pressure_name:
row.prop(brush, pressure_name, text="")
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")
return row
@staticmethod
def prop_unified_color(parent, context, brush, prop_name, *, text=None):
ups = context.tool_settings.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 = context.tool_settings.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 = "Brushes"
def draw(self, context):
layout = self.layout
settings = self.paint_settings(context)
brush = settings.brush
row = layout.row()
large_preview = True
if large_preview:
row.column().template_ID_preview(settings, "brush", new="brush.add", rows=3, cols=8, hide_buttons=False)
else:
row.column().template_ID(settings, "brush", new="brush.add")
col = row.column()
col.menu("VIEW3D_MT_brush_context_menu", icon='DOWNARROW_HLT', text="")
if brush is not None:
col.prop(brush, "use_custom_icon", toggle=True, icon='FILE_IMAGE', text="")
if brush.use_custom_icon:
layout.prop(brush, "icon_filepath", 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)
brush = settings.brush
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
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_tool == '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_images",
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
col = layout.column()
col.template_ID_preview(brush, "mask_texture", new="texture.new", rows=3, cols=8)
mask_tex_slot = brush.mask_texture_slot
# 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'}
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:
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="")
col.row().prop(brush, "jitter_unit", expand=True)
col.separator()
col.prop(settings, "input_samples")
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.prop(brush, "use_smooth_stroke", text="")
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)
return (settings and settings.brush and settings.brush.curve)
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)
row = col.row(align=True)
row.prop(brush, "curve_preset", text="")
if brush.curve_preset == 'CUSTOM':
layout.template_curve_mapping(brush, "curve", brush=True)
col = layout.column(align=True)
row = col.row(align=True)
row.operator("brush.curve_preset", icon='SMOOTHCURVE', text="").shape = 'SMOOTH'
row.operator("brush.curve_preset", icon='SPHERECURVE', text="").shape = 'ROUND'
row.operator("brush.curve_preset", icon='ROOTCURVE', text="").shape = 'ROOT'
row.operator("brush.curve_preset", icon='SHARPCURVE', text="").shape = 'SHARP'
row.operator("brush.curve_preset", icon='LINCURVE', text="").shape = 'LINE'
row.operator("brush.curve_preset", icon='NOCURVE', text="").shape = 'MAX'
if mode in {'SCULPT', 'PAINT_VERTEX', 'PAINT_WEIGHT'} and brush.sculpt_tool != 'POSE':
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.prop(settings, "show_brush", text="")
row.label(text="Display Cursor")
col = layout.column()
col.active = brush.brush_capabilities.has_overlay and 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 Cursor Color")
col.separator()
row = col.row(align=True)
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',
)
if mode in ['PAINT_2D', 'PAINT_TEXTURE', 'PAINT_VERTEX', 'SCULPT']:
row = col.row(align=True)
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.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
# normal_radius_factor
layout.prop(brush, "normal_radius_factor", slider=True)
layout.prop(brush, "hardness", slider=True)
# auto_smooth_factor and use_inverse_smooth_pressure
if capabilities.has_auto_smooth:
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"auto_smooth_factor",
pressure_name="use_inverse_smooth_pressure",
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 = "Pinch"
if brush.sculpt_tool in {'BLOB', 'SNAKE_HOOK'}:
text = "Magnify"
layout.prop(brush, "crease_pinch_factor", slider=True, text=text)
# 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,
)
layout.prop(brush, "use_plane_trim", text="Plane Trim")
row = layout.row()
row.active = brush.use_plane_trim
row.prop(brush, "plane_trim", slider=True, text="Distance")
layout.separator()
# height
if capabilities.has_height:
layout.prop(brush, "height", slider=True, text="Height")
# use_persistent, set_persistent_base
if capabilities.has_persistence:
ob = context.sculpt_object
do_persistent = True
# not supported yet for this case
for md in ob.modifiers:
if md.type == 'MULTIRES':
do_persistent = False
break
if do_persistent:
layout.separator()
layout.prop(brush, "use_persistent")
layout.operator("sculpt.set_persistent_base")
layout.separator()
if brush.sculpt_tool == 'CLAY_STRIPS':
row = layout.row()
row.prop(brush, "tip_roundness")
if brush.sculpt_tool == 'ELASTIC_DEFORM':
layout.separator()
layout.prop(brush, "elastic_deform_type")
layout.prop(brush, "elastic_deform_volume_preservation", slider=True)
layout.separator()
if brush.sculpt_tool == 'POSE':
layout.separator()
layout.prop(brush, "pose_origin_type")
layout.prop(brush, "pose_offset")
layout.prop(brush, "pose_smooth_iterations")
layout.prop(brush, "pose_ik_segments")
layout.prop(brush, "use_pose_ik_anchored")
layout.separator()
if brush.sculpt_tool == 'CLOTH':
layout.separator()
layout.prop(brush, "cloth_sim_limit")
layout.prop(brush, "cloth_sim_falloff")
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.separator()
if brush.sculpt_tool == 'SCRAPE':
row = layout.row()
row.prop(brush, "area_radius_factor", slider=True)
row = layout.row()
row.prop(brush, "invert_to_scrape_fill", text="Invert to Fill")
if brush.sculpt_tool == 'FILL':
row = layout.row()
row.prop(brush, "area_radius_factor", slider=True)
row = layout.row()
row.prop(brush, "invert_to_scrape_fill", text="Invert to Scrape")
if brush.sculpt_tool == 'GRAB':
layout.prop(brush, "use_grab_active_vertex")
if brush.sculpt_tool == '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")
if brush.sculpt_tool == '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")
if brush.sculpt_tool == 'MASK':
layout.row().prop(brush, "mask_tool", expand=True)
# 3D and 2D Texture Paint Mode.
elif mode in {'PAINT_TEXTURE', 'PAINT_2D'}:
capabilities = brush.image_paint_capabilities
if brush.image_tool == '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)
def brush_shared_settings(layout, context, brush, popover=False):
""" Draw simple brush settings that are shared between different paint modes. """
mode = UnifiedPaintPanel.get_brush_mode(context)
### Determine which settings to draw. ###
blend_mode = False
size = False
size_mode = False
strength = False
strength_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
# Sculpt #
if mode == 'SCULPT':
size_mode = True
if not popover:
size = True
strength = True
strength_pressure = brush.sculpt_capabilities.has_strength_pressure
direction = not brush.sculpt_capabilities.has_direction
# Vertex Paint #
if mode == 'PAINT_VERTEX':
if not popover:
blend_mode = True
size = True
strength = True
strength_pressure = True
# Weight Paint #
if mode == 'PAINT_WEIGHT':
if not popover:
size = True
weight = brush.weight_paint_capabilities.has_weight
strength = strength_pressure = True
# Only draw blend mode for the Draw tool, because for other tools it is pointless. D5928#137944
if brush.weight_tool == 'DRAW':
blend_mode = True
# UV Sculpt #
if mode == 'UV_SCULPT':
size = True
strength = True
### Draw settings. ###
ups = context.scene.tool_settings.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_radius"
if size or size_mode:
if size:
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
size_prop,
unified_name="use_unified_size",
pressure_name="use_pressure_size",
text="Radius",
slider=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
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"strength",
unified_name="use_unified_strength",
pressure_name=pressure_name,
slider=True,
)
layout.separator()
if direction:
layout.row().prop(brush, "direction", expand=True)
def brush_settings_advanced(layout, context, 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':
capabilities = brush.sculpt_capabilities
use_accumulate = capabilities.has_accumulate
use_frontface = True
# topology automasking
layout.prop(brush, "use_automasking_topology")
# face masks automasking
layout.prop(brush, "use_automasking_face_sets")
# boundary edges automasking
layout.prop(brush, "use_automasking_boundary_edges")
layout.prop(brush, "automasking_boundary_edges_propagation_steps")
# sculpt plane settings
if capabilities.has_sculpt_plane:
layout.prop(brush, "sculpt_plane")
layout.prop(brush, "use_original_normal")
layout.prop(brush, "use_original_plane")
layout.separator()
# 3D and 2D Texture Paint.
elif mode in {'PAINT_TEXTURE', 'PAINT_2D'}:
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_tool == '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_tool == 'MASK':
layout.prop(brush, "weight", text="Mask Value", slider=True)
elif brush.image_tool == 'CLONE':
if mode == 'PAINT_2D':
layout.prop(brush, "clone_image", text="Image")
layout.prop(brush, "clone_alpha", text="Alpha")
# Vertex Paint #
elif mode == 'PAINT_VERTEX':
layout.prop(brush, "use_alpha")
if brush.vertex_tool != 'SMEAR':
use_accumulate = True
use_frontface = True
# Weight Paint
elif mode == 'PAINT_WEIGHT':
if brush.weight_tool != 'SMEAR':
use_accumulate = True
use_frontface = True
# Draw shared settings.
if use_accumulate:
layout.prop(brush, "use_accumulate")
if use_frontface:
layout.prop(brush, "use_frontface", text="Front Faces Only")
def draw_color_settings(context, layout, brush, color_type=False):
"""Draw color wheel and gradient settings."""
ups = context.scene.tool_settings.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='WORLD')
# 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_tool == '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
if sculpt:
layout.prop(tex_slot, "map_mode", text="Mapping")
else:
layout.prop(tex_slot, "tex_paint_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")
layout.operator("brush.stencil_reset_transform")
# 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")
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:
UnifiedPaintPanel.prop_unified_color(layout, context, brush, "color", text="")
layout.prop(brush, "blend", text="" if compact else "Blend")
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"size",
pressure_name="use_pressure_size",
unified_name="use_unified_size",
slider=True,
text="Radius",
header=True
)
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"strength",
pressure_name="use_pressure_strength",
unified_name="use_unified_strength",
header=True
)
def brush_basic_gpencil_paint_settings(layout, context, brush, *, compact=False):
tool_settings = context.tool_settings
settings = tool_settings.gpencil_paint
gp_settings = brush.gpencil_settings
tool = context.workspace.tools.from_space_view3d_mode(context.mode, create=False)
if gp_settings is None:
return
# Brush details
if brush.gpencil_tool == 'ERASE':
row = layout.row(align=True)
row.prop(brush, "size", text="Radius")
row.prop(gp_settings, "use_pressure", text="", icon='STYLUS_PRESSURE')
row.prop(gp_settings, "use_occlude_eraser", text="", icon='XRAY')
row = layout.row(align=True)
row.prop(gp_settings, "eraser_mode", expand=True)
if gp_settings.eraser_mode == 'SOFT':
row = layout.row(align=True)
row.prop(gp_settings, "pen_strength", slider=True)
row.prop(gp_settings, "use_strength_pressure", text="", icon='STYLUS_PRESSURE')
row = layout.row(align=True)
row.prop(gp_settings, "eraser_strength_factor")
row = layout.row(align=True)
row.prop(gp_settings, "eraser_thickness_factor")
row = layout.row(align=True)
row.prop(settings, "show_brush", text="Display Cursor")
# FIXME: tools must use their own UI drawing!
elif brush.gpencil_tool == 'FILL':
row = layout.row(align=True)
row.prop(gp_settings, "fill_leak", text="Leak Size")
row = layout.row(align=True)
row.prop(brush, "size", text="Thickness")
row = layout.row(align=True)
row.prop(gp_settings, "fill_simplify_level", text="Simplify")
else: # brush.gpencil_tool == 'DRAW/TINT':
row = layout.row(align=True)
row.prop(brush, "size", text="Radius")
row.prop(gp_settings, "use_pressure", text="", icon='STYLUS_PRESSURE')
row = layout.row(align=True)
row.prop(gp_settings, "pen_strength", slider=True)
row.prop(gp_settings, "use_strength_pressure", text="", icon='STYLUS_PRESSURE')
if brush.gpencil_tool == 'TINT':
row = layout.row(align=True)
row.prop(gp_settings, "vertex_mode", text="Mode")
# FIXME: tools must use their own UI drawing!
if tool.idname in {
"builtin.arc",
"builtin.curve",
"builtin.line",
"builtin.box",
"builtin.circle",
"builtin.polyline"
}:
settings = context.tool_settings.gpencil_sculpt
if compact:
row = layout.row(align=True)
row.prop(settings, "use_thickness_curve", text="", icon='CURVE_DATA')
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:
# Curve
layout.template_curve_mapping(settings, "thickness_primitive_curve", brush=True)
def brush_basic_gpencil_sculpt_settings(layout, context, brush, *, compact=False):
gp_settings = brush.gpencil_settings
tool = brush.gpencil_sculpt_tool
row = layout.row(align=True)
row.prop(brush, "size", slider=True)
sub = row.row(align=True)
sub.enabled = tool not in {'GRAB', 'CLONE'}
sub.prop(gp_settings, "use_pressure", text="")
row = layout.row(align=True)
row.prop(brush, "strength", slider=True)
row.prop(brush, "use_pressure_strength", text="")
if compact:
if tool in {'THICKNESS', 'STRENGTH', 'PINCH', 'TWIST'}:
row.separator()
row.prop(gp_settings, "direction", expand=True, text="")
else:
use_property_split_prev = layout.use_property_split
layout.use_property_split = False
if tool in {'THICKNESS', 'STRENGTH'}:
layout.row().prop(gp_settings, "direction", expand=True)
elif tool == 'PINCH':
row = layout.row(align=True)
row.prop_enum(gp_settings, "direction", value='ADD', text="Pinch")
row.prop_enum(gp_settings, "direction", value='SUBTRACT', text="Inflate")
elif tool == 'TWIST':
row = layout.row(align=True)
row.prop_enum(gp_settings, "direction", value='ADD', text="CCW")
row.prop_enum(gp_settings, "direction", value='SUBTRACT', text="CW")
layout.use_property_split = use_property_split_prev
def brush_basic_gpencil_weight_settings(layout, _context, brush, *, compact=False):
gp_settings = brush.gpencil_settings
layout.prop(brush, "size", slider=True)
row = layout.row(align=True)
row.prop(brush, "strength", slider=True)
row.prop(brush, "use_pressure_strength", text="")
layout.prop(brush, "weight", slider=True)
def brush_basic_gpencil_vertex_settings(layout, _context, brush, *, compact=False):
gp_settings = brush.gpencil_settings
# Brush details
row = layout.row(align=True)
row.prop(brush, "size", text="Radius")
row.prop(gp_settings, "use_pressure", text="", icon='STYLUS_PRESSURE')
if brush.gpencil_vertex_tool in {'DRAW', 'BLUR', 'SMEAR'}:
row = layout.row(align=True)
row.prop(gp_settings, "pen_strength", slider=True)
row.prop(gp_settings, "use_strength_pressure", text="", icon='STYLUS_PRESSURE')
if brush.gpencil_vertex_tool in {'DRAW', 'REPLACE'}:
row = layout.row(align=True)
row.prop(gp_settings, "vertex_mode", text="Mode")
classes = (
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)