# SPDX-FileCopyrightText: 2012-2023 Blender Authors # # SPDX-License-Identifier: GPL-2.0-or-later import bpy from bpy.types import ( Menu, ) 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" filter_brush = True 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): """ Test if any tool active in the current space matches *brush_type*. :param context: The context. :type context: :class:`bpy.types.Context` :param brush_type: Brush type identifier to match against tool brush types. :type brush_type: int :return: True when a registered tool uses this brush type. :rtype: bool """ 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): """ Test if *asset* is compatible with the active tool's brush type. :param context: The context. :type context: :class:`bpy.types.Context` :param asset: Brush asset to test. :type asset: :class:`bpy.types.AssetRepresentation` :return: True when the asset's brush type matches the active tool. :rtype: bool """ 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_from_active_tool(bpy.context) return paint_settings.brush_asset_reference if paint_settings else None @classmethod def draw_context_menu(self, context, asset, layout): del context, asset # 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): """ Look up the brush asset-shelf identifier for the current paint mode. :param context: The context. :type context: :class:`bpy.types.Context` :return: The asset-shelf ``bl_idname``, or ``None`` when no paint mode is active. :rtype: str | None """ 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): """ Draw a brush asset-shelf popover into *layout* for the active paint mode. :param layout: Layout to draw into. :type layout: :class:`bpy.types.UILayout` :param context: The context. :type context: :class:`bpy.types.Context` :param brush: Brush whose preview/name is shown on the button. :type brush: :class:`bpy.types.Brush` | None :param show_name: Display the brush name next to the preview. :type show_name: bool """ 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: # Show "*" to the left for consistency with unsaved files in the title bar. 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, ) def brush_asset_shelf_filter_draw(panel, context): if context.asset_shelf.bl_idname != BrushAssetShelf.get_shelf_name_from_context(context): return layout = panel.layout prefs = context.preferences layout.prop(prefs.view, "use_filter_brushes_by_tool", text="By Active Tool") def show_experimental_texture_paint(brush): if not bpy.context.preferences.experimental.use_3d_texture_paint or not brush: return False return brush.image_brush_type in {'DRAW'} 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(tool_settings, mode): 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 elif mode == 'PAINT_2D': return tool_settings.image_paint elif mode == 'SCULPT_CURVES': return tool_settings.curves_sculpt 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 paint_settings_from_active_tool(context): """Retrieve the Paint settings based on the current active tool, may return None for tools with no associated brush""" tool_settings = context.tool_settings mode = UnifiedPaintPanel.get_brush_mode(context) return UnifiedPaintPanel._paint_settings(tool_settings, mode) @staticmethod def paint_settings_from_mode(context, mode): """Retrieve the Paint settings based on a hardcoded 'mode' string.""" return UnifiedPaintPanel._paint_settings(context.tool_settings, mode) @staticmethod def prop_unified( layout, context, brush, prop_name, unified_paint_settings_override=None, unified_name=None, pressure_name=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) if unified_paint_settings_override: ups = unified_paint_settings_override else: ups = UnifiedPaintPanel.paint_settings_from_active_tool(context).unified_paint_settings prop_owner = brush if unified_name and getattr(brush, unified_name): prop_owner = ups row.prop(prop_owner, prop_name, icon='NONE', 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(brush, unified_name, text="", icon='BRUSHES_ALL') if pressure_name: row.prop(brush, pressure_name, text="") return row @staticmethod def prop_custom_pressure( layout, context, parent_row, brush, *, pressure_name, curve_visibility_name, custom_curve_name, ): paint = UnifiedPaintPanel.paint_settings_from_active_tool(context) is_active = getattr(paint, curve_visibility_name) parent_row.prop( paint, curve_visibility_name, text="", icon='DOWNARROW_HLT' if is_active else 'RIGHTARROW', emboss=False, ) if is_active: subcol = layout.column() subcol.active = getattr(brush, pressure_name) subcol.template_curve_mapping(brush, custom_curve_name, brush=True, show_presets=True) @staticmethod def prop_unified_color(parent, context, brush, prop_name, *, text=None): ups = UnifiedPaintPanel.paint_settings_from_active_tool(context).unified_paint_settings prop_owner = ups if brush.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_from_active_tool(context).unified_paint_settings prop_owner = ups if brush.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_from_active_tool(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_from_active_tool(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_from_active_tool(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_from_active_tool(context) layout.template_ID(settings, "palette", new="palette.new") if settings.palette: layout.template_palette(settings, "palette") 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_from_active_tool(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_from_active_tool(context) self.layout.prop(settings, "use_clone_layer", text="") def draw(self, context): layout = self.layout settings = self.paint_settings_from_active_tool(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_from_active_tool(context) brush = settings.brush col = layout.column() col.prop(brush, "stroke_method") col.separator() if brush.stroke_method == 'ANCHORED': col.prop(brush, "use_edge_to_edge", text="Edge to Edge") if brush.stroke_method == 'AIRBRUSH': col.prop(brush, "rate", text="Rate", slider=True) if brush.stroke_method == 'SPACE': row = col.row(align=True) row.prop(brush, "spacing", text="Spacing") row.prop(brush, "use_pressure_spacing", toggle=True, text="") if not self.is_popover: UnifiedPaintPanel.prop_custom_pressure( layout, context, row, brush, pressure_name="use_pressure_spacing", curve_visibility_name="show_spacing_curve", custom_curve_name="curve_spacing", ) col = layout.column() if brush.stroke_method in {'LINE', 'CURVE'}: row = col.row(align=True) row.prop(brush, "spacing", text="Spacing") if mode == 'SCULPT' or (mode == 'PAINT_TEXTURE' and show_experimental_texture_paint(brush)): 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.stroke_method == 'CURVE': col.separator() col.template_ID(brush, "paint_curve", new="paintcurve.new") col.operator("paintcurve.draw") col.separator() if brush.stroke_method in {'SPACE', 'LINE', '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_from_active_tool(context) brush = settings.brush if brush.brush_capabilities.has_smooth_stroke: return True return False def draw_header(self, context): settings = self.paint_settings_from_active_tool(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_from_active_tool(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_from_active_tool(context) if not (settings and settings.brush and settings.brush.curve_distance_falloff): return False mode = cls.get_brush_mode(context) if mode == 'SCULPT_CURVES': brush = settings.brush if brush.curves_sculpt_brush_type in {'ADD', 'DELETE', 'CUT'}: return False return True def draw(self, context): layout = self.layout settings = self.paint_settings_from_active_tool(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) class ShapePanel(BrushPanel): bl_label = "Shape" bl_options = {'DEFAULT_CLOSED'} @classmethod def poll(cls, context): if not super().poll(context): return False settings = cls.paint_settings_from_active_tool(context) if not settings: return False brush = settings.brush if not (brush and brush.curve_distance_falloff): return False return True def draw(self, context): layout = self.layout settings = self.paint_settings_from_active_tool(context) mode = self.get_brush_mode(context) brush = settings.brush experimental_texture_paint_enabled = show_experimental_texture_paint(brush) if brush is None: return layout.use_property_split = True layout.use_property_decorate = False if mode == 'SCULPT': if brush.sculpt_capabilities.has_hardness: row = layout.row(align=True) row.prop(brush, "hardness", slider=True) if brush.sculpt_capabilities.has_hardness_pressure: row.prop(brush, "use_hardness_pressure", text="") if not self.is_popover: UnifiedPaintPanel.prop_custom_pressure( layout, context, row, brush, pressure_name="use_hardness_pressure", curve_visibility_name="show_hardness_curve", custom_curve_name="curve_hardness", ) layout.separator() if brush.sculpt_capabilities.has_tip_roundness: layout.row(align=True) layout.prop(brush, "tip_roundness") layout.prop(brush, "tip_scale_x") layout.separator() elif mode == 'PAINT_TEXTURE' and experimental_texture_paint_enabled: # TODO: Update this once the "capabilities" block has been updated row = layout.row(align=True) row.prop(brush, "hardness", slider=True) row.prop(brush, "use_hardness_pressure", text="") if not self.is_popover: UnifiedPaintPanel.prop_custom_pressure( layout, context, row, brush, pressure_name="use_hardness_pressure", curve_visibility_name="show_hardness_curve", custom_curve_name="curve_hardness", ) layout.separator() layout.use_property_split = False 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 mode == 'PAINT_TEXTURE' and experimental_texture_paint_enabled: 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_from_active_tool(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_from_active_tool(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 if capabilities.has_normal_radius: layout.prop(brush, "normal_radius_factor", slider=True) if capabilities.has_tilt: layout.prop(brush, "tilt_strength_factor", slider=True) # auto_smooth_factor and use_smooth_pressure if capabilities.has_auto_smooth: pressure_name = "use_smooth_pressure" if capabilities.has_auto_smooth_pressure else None unified_row = UnifiedPaintPanel.prop_unified( layout, context, brush, "auto_smooth_factor", pressure_name=pressure_name, slider=True, ) if capabilities.has_auto_smooth_pressure and not popover: UnifiedPaintPanel.prop_custom_pressure( layout, context, unified_row, brush, pressure_name="use_smooth_pressure", curve_visibility_name="show_auto_smooth_curve", custom_curve_name="curve_auto_smooth", ) # 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() col = layout.column() # Persistent base is not supported when Dyntopo is enabled. if context.sculpt_object and context.sculpt_object.use_dynamic_topology_sculpting: col.enabled = False col.prop(brush, "use_persistent") col.operator("sculpt.set_persistent_base") layout.separator() if capabilities.has_color: ups = UnifiedPaintPanel.paint_settings_from_mode(context, 'SCULPT').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(brush, "use_unified_color", text="", icon='BRUSHES_ALL') layout.prop(brush, "blend", text="Blend Mode") # Per sculpt tool options. if 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 == 'SCENE_PROJECT': layout.separator() layout.prop(brush, "project_ray_direction_type") layout.prop(brush, "minimum_distance") layout.prop(brush, "use_bidirectional") 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="") 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 paint = UnifiedPaintPanel.paint_settings_from_active_tool(context) # UNUSED. 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 mode == 'PAINT_TEXTURE' and show_experimental_texture_paint(brush): size_mode = True 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_from_active_tool(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 brush.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 unified_row = UnifiedPaintPanel.prop_unified( layout, context, brush, size_prop, unified_name="use_unified_size", pressure_name=pressure_name, text="Size", slider=True, ) if not popover and size_pressure and mode in { 'PAINT_TEXTURE', 'PAINT_2D', 'SCULPT', 'PAINT_VERTEX', 'PAINT_WEIGHT', 'SCULPT_CURVES'}: UnifiedPaintPanel.prop_custom_pressure( layout, context, unified_row, brush, pressure_name=pressure_name, curve_visibility_name="show_size_curve", custom_curve_name="curve_size", ) 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 unified_row = UnifiedPaintPanel.prop_unified( layout, context, brush, "strength", unified_name="use_unified_strength", pressure_name=pressure_name, slider=True, ) if not popover and strength_pressure and mode in { 'PAINT_TEXTURE', 'PAINT_2D', 'SCULPT', 'PAINT_VERTEX', 'PAINT_WEIGHT', 'SCULPT_CURVES'}: UnifiedPaintPanel.prop_custom_pressure( layout, context, unified_row, brush, pressure_name=pressure_name, curve_visibility_name="show_strength_curve", custom_curve_name="curve_strength", ) layout.separator() if direction: layout.row().prop(brush, "direction", expand=True) def draw_color_jitter_panel(layout, context, brush): ups = UnifiedPaintPanel.paint_settings_from_active_tool(context).unified_paint_settings prop_owner = ups if brush.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_ctxt=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) container = layout # 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() header, panel = layout.panel("advanced_panel", default_closed=False) header.label(text="Advanced") container = panel if panel is None: return if mode == 'SCULPT': container.prop(brush, "sculpt_brush_type") capabilities = brush.sculpt_capabilities if capabilities.has_accumulate: container.prop(brush, "use_accumulate") container.prop(brush, "use_frontface", text="Front Faces Only") # sculpt plane settings if capabilities.has_sculpt_plane: container.prop(brush, "sculpt_plane") if brush.sculpt_brush_type != 'PLANE': col = container.column(heading="Original", align=True) col.prop(brush, "use_original_normal", text="Normal") col.prop(brush, "use_original_plane", text="Plane") draw_mesh_automasking_settings( container, brush.mesh_automasking_settings, use_face_set=True, use_operators=True) if capabilities.has_color and popover: draw_color_jitter_panel(container, context, brush) # 3D and 2D Texture Paint. elif mode in {'PAINT_TEXTURE', 'PAINT_2D'}: container.prop(brush, "image_brush_type") capabilities = brush.image_paint_capabilities if mode == 'PAINT_2D': container.prop(brush, "use_paint_antialiasing") else: container.prop(brush, "use_alpha") if capabilities.has_accumulate: container.prop(brush, "use_accumulate") # Tool specific settings if brush.image_brush_type == 'SOFTEN': container.row().prop(brush, "direction", expand=True) container.prop(brush, "sharp_threshold") if mode == 'PAINT_2D': container.prop(brush, "blur_kernel_radius") container.prop(brush, "blur_mode") elif brush.image_brush_type == 'MASK': container.prop(brush, "weight", text="Mask Value", slider=True) elif brush.image_brush_type == 'CLONE': if mode == 'PAINT_2D': container.prop(settings, "clone_image", text="Image") container.prop(settings, "clone_alpha", text="Alpha") if popover: draw_color_jitter_panel(container, context, brush) # Vertex Paint # elif mode == 'PAINT_VERTEX': container.prop(brush, "vertex_brush_type") container.prop(brush, "use_alpha") # TODO: Make this a "Capability" if brush.vertex_brush_type != 'SMEAR': container.prop(brush, "use_accumulate") container.prop(brush, "use_frontface", text="Front Faces Only") draw_mesh_automasking_settings(container, brush.mesh_automasking_settings) if popover: draw_color_jitter_panel(container, context, brush) # Weight Paint elif mode == 'PAINT_WEIGHT': container.prop(brush, "weight_brush_type") # TODO: Make this a "Capability" if brush.weight_brush_type != 'SMEAR': container.prop(brush, "use_accumulate") container.prop(brush, "use_frontface", text="Front Faces Only") draw_mesh_automasking_settings(container, brush.mesh_automasking_settings) # Sculpt Curves elif mode == 'SCULPT_CURVES': container.prop(brush, "curves_sculpt_brush_type") def draw_mesh_automasking_settings(layout, settings, *, topbar=False, use_face_set=False, use_operators=False): if topbar: layout.label(text="Auto-Masking") parent = layout.column(align=True) else: header, panel = layout.panel("auto_masking_panel", default_closed=True) header.label(text="Auto-Masking") if panel is None: return parent = panel col = parent.column(align=True) col.prop(settings, "use_automasking_topology", text="Topology") if use_face_set: col.prop(settings, "use_automasking_face_sets", text="Face Sets") parent.separator() col = parent.column(align=True) row = col.row() row.prop(settings, "use_automasking_boundary_edges", text="Mesh Boundary") if use_operators and settings.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() if use_face_set: row.prop(settings, "use_automasking_boundary_face_sets", text="Face Sets Boundary") if use_operators and settings.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 settings.use_automasking_boundary_edges or settings.use_automasking_boundary_face_sets: # Odd hack needed to get this to display consistently... if topbar: col = parent.column() col.use_property_split = False col.prop(settings, "boundary_edges_propagation_steps") else: col = parent.column() col.use_property_split = False split = col.split(factor=col.property_split_factor) col = split.column() split.prop(settings, "boundary_edges_propagation_steps") col.separator() col = parent.column(align=True) row = col.row() row.prop(settings, "use_automasking_cavity", text="Cavity") is_cavity_active = settings.use_automasking_cavity or settings.use_automasking_cavity_inverted if use_operators and is_cavity_active: props = row.operator("sculpt.mask_from_cavity", text="Create Mask") props.settings_source = 'BRUSH' col.prop(settings, "use_automasking_cavity_inverted", text="Cavity (inverted)") if is_cavity_active: col = parent.column(align=True) col.prop(settings, "cavity_factor", text="Factor") col.prop(settings, "cavity_blur_steps", text="Blur") col = parent.column() col.prop(settings, "use_automasking_custom_cavity_curve", text="Custom Curve") if settings.use_automasking_custom_cavity_curve: col.template_curve_mapping(settings, "cavity_curve", brush=True) col.separator() col = parent.column(align=True) col.prop(settings, "use_automasking_view_normal", text="View Normal") if settings.use_automasking_view_normal: col.prop(settings, "use_automasking_view_occlusion", text="Occlusion") subcol = col.column(align=True) subcol.active = not settings.use_automasking_view_occlusion subcol.prop(settings, "view_normal_limit", text="Limit") subcol.prop(settings, "view_normal_falloff", text="Falloff") col = parent.column() col.prop(settings, "use_automasking_start_normal", text="Area Normal") if settings.use_automasking_start_normal: col = parent.column(align=True) col.prop(settings, "start_normal_limit", text="Limit") col.prop(settings, "start_normal_falloff", text="Falloff") def draw_color_settings(context, layout, brush, color_type=False): """Draw color wheel and gradient settings.""" ups = UnifiedPaintPanel.paint_settings_from_active_tool(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(brush, "use_unified_color", text="", icon='BRUSHES_ALL') draw_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.""" # TODO: This shared method is incorrect and unnecessary, remove this layer of abstraction 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 ups = settings.unified_paint_settings prop_owner = ups if brush.use_unified_color else brush sub_row.prop_with_popover(prop_owner, "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 is_primitive_tool = tool.idname in { "builtin.arc", "builtin.curve", "builtin.line", "builtin.box", "builtin.circle", "builtin.polyline", } grease_pencil_brush_type = brush.gpencil_brush_type if grease_pencil_brush_type in {'DRAW', 'ERASE', 'TINT'} or is_primitive_tool: size = "size" if brush.use_locked_size == 'SCENE' and (grease_pencil_brush_type == 'DRAW' or is_primitive_tool): size = "unprojected_size" UnifiedPaintPanel.prop_unified( layout, context, brush, size, pressure_name="use_pressure_size", unified_name="use_unified_size", text="Size", slider=True, header=compact, ) UnifiedPaintPanel.prop_unified( layout, context, brush, "strength", pressure_name="use_pressure_strength", unified_name="use_unified_strength", text="Strength", header=compact, ) if props: layout.prop(props, "subdivision") # Brush details if is_primitive_tool: row = layout.row(align=True) if context.region.type == 'TOOL_HEADER': row.prop_enum(brush.gpencil_settings, "stroke_type", 'STROKE', text="", icon='GP_DRAW_STROKE') row.prop_enum(brush.gpencil_settings, "stroke_type", 'FILL', text="", icon='GP_DRAW_FILL') row.prop_enum(brush.gpencil_settings, "stroke_type", 'BOTH', text="", icon='GP_DRAW_BOTH') else: row.prop(brush.gpencil_settings, "stroke_type") 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") row = layout.row(align=True) settings = context.tool_settings.gpencil_sculpt if compact: 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.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_enum(brush.gpencil_settings, "stroke_type", 'STROKE', text="", icon='GP_DRAW_STROKE') row.prop_enum(brush.gpencil_settings, "stroke_type", 'FILL', text="", icon='GP_DRAW_FILL') row.prop_enum(brush.gpencil_settings, "stroke_type", 'BOTH', text="", icon='GP_DRAW_BOTH') else: row.prop(brush.gpencil_settings, "stroke_type") row = layout.row(align=True) if compact: row.prop(gp_settings, "use_cyclic_stroke", text="") else: row.prop(gp_settings, "use_cyclic_stroke", text="Cyclic") 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) if gp_settings.fill_solver == 'PIXEL': 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 else: size = "size" if brush.use_locked_size == 'SCENE': size = "unprojected_size" row = layout.row(align=True) row.prop(brush, size, slider=True, text="Size") 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': if context.region.type == 'TOOL_HEADER': row = layout.row(align=True) row.prop_enum(gp_settings, "vertex_mode", 'STROKE', text="", icon='GP_DRAW_STROKE') row.prop_enum(gp_settings, "vertex_mode", 'FILL', text="", icon='GP_DRAW_FILL') row.prop_enum(gp_settings, "vertex_mode", 'BOTH', text="", icon='GP_DRAW_BOTH') else: layout.prop(gp_settings, "vertex_mode", text="Stroke Mode") layout.popover("VIEW3D_PT_tools_brush_falloff") layout.prop(gp_settings, "use_active_layer_only") def brush_basic_grease_pencil_sculpt_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, ) UnifiedPaintPanel.prop_unified( layout, context, brush, "strength", pressure_name="use_pressure_strength", unified_name="use_unified_strength", text="Strength", header=compact, ) 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, ) UnifiedPaintPanel.prop_unified( layout, context, brush, "strength", pressure_name="use_pressure_strength", 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): if brush.gpencil_vertex_brush_type == 'DRAW': layout.prop(brush, "blend", text="Blend") layout.separator() 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'}: if context.region.type == 'TOOL_HEADER': row = layout.row(align=True) row.prop_enum(gp_settings, "vertex_mode", 'STROKE', text="", icon='GP_DRAW_STROKE') row.prop_enum(gp_settings, "vertex_mode", 'FILL', text="", icon='GP_DRAW_FILL') row.prop_enum(gp_settings, "vertex_mode", 'BOTH', text="", icon='GP_DRAW_BOTH') else: layout.prop(gp_settings, "vertex_mode", text="Stroke Mode") def supports_shape_panel(mode): return mode in {'SCULPT', 'PAINT_VERTEX', 'PAINT_WEIGHT', 'PAINT_TEXTURE'} classes = ( VIEW3D_MT_tools_projectpaint_clone, ) def register(): bpy.types.ASSETSHELF_PT_filter.append(brush_asset_shelf_filter_draw) def unregister(): bpy.types.ASSETSHELF_PT_filter.remove(brush_asset_shelf_filter_draw) if __name__ == "__main__": # only for live edit. from bpy.utils import register_class for cls in classes: register_class(cls)