Sculpt: Pose Brush Scale/Transform deform mode
[blender.git] / release / scripts / startup / bl_ui / properties_paint_common.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20 from bpy.types import Menu
21
22
23 class UnifiedPaintPanel:
24     # subclass must set
25     # bl_space_type = 'IMAGE_EDITOR'
26     # bl_region_type = 'UI'
27
28     @staticmethod
29     def get_brush_mode(context):
30         """ Get the correct mode for this context. For any context where this returns None,
31             no brush options should be displayed."""
32         mode = context.mode
33
34         if mode == 'PARTICLE':
35             # Particle brush settings currently completely do their own thing.
36             return None
37
38         from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
39         tool = ToolSelectPanelHelper.tool_active_from_context(context)
40
41         if not tool:
42             # If there is no active tool, then there can't be an active brush.
43             return None
44
45         if not tool.has_datablock:
46             # tool.has_datablock is always true for tools that use brushes.
47             return None
48
49         space_data = context.space_data
50         tool_settings = context.tool_settings
51
52         if space_data:
53             space_type = space_data.type
54             if space_type == 'IMAGE_EDITOR':
55                 if space_data.show_uvedit:
56                     return 'UV_SCULPT'
57                 return 'PAINT_2D'
58             elif space_type in {'VIEW_3D', 'PROPERTIES'}:
59                 if mode == 'PAINT_TEXTURE':
60                     if tool_settings.image_paint:
61                         return mode
62                     else:
63                         return None
64                 return mode
65         return None
66
67     @staticmethod
68     def paint_settings(context):
69         tool_settings = context.tool_settings
70
71         mode = UnifiedPaintPanel.get_brush_mode(context)
72
73         # 3D paint settings
74         if mode == 'SCULPT':
75             return tool_settings.sculpt
76         elif mode == 'PAINT_VERTEX':
77             return tool_settings.vertex_paint
78         elif mode == 'PAINT_WEIGHT':
79             return tool_settings.weight_paint
80         elif mode == 'PAINT_TEXTURE':
81             return tool_settings.image_paint
82         elif mode == 'PARTICLE':
83             return tool_settings.particle_edit
84         # 2D paint settings
85         elif mode == 'PAINT_2D':
86             return tool_settings.image_paint
87         elif mode == 'UV_SCULPT':
88             return tool_settings.uv_sculpt
89         # Grease Pencil settings
90         elif mode == 'PAINT_GPENCIL':
91             return tool_settings.gpencil_paint
92         elif mode == 'SCULPT_GPENCIL':
93             return tool_settings.gpencil_sculpt_paint
94         elif mode == 'WEIGHT_GPENCIL':
95             return tool_settings.gpencil_weight_paint
96         elif mode == 'VERTEX_GPENCIL':
97             return tool_settings.gpencil_vertex_paint
98         return None
99
100     @staticmethod
101     def prop_unified(
102             layout,
103             context,
104             brush,
105             prop_name,
106             unified_name=None,
107             pressure_name=None,
108             icon='NONE',
109             text=None,
110             slider=False,
111             header=False,
112     ):
113         """ Generalized way of adding brush options to the UI,
114             along with their pen pressure setting and global toggle, if they exist. """
115         row = layout.row(align=True)
116         ups = context.tool_settings.unified_paint_settings
117         prop_owner = brush
118         if unified_name and getattr(ups, unified_name):
119             prop_owner = ups
120
121         row.prop(prop_owner, prop_name, icon=icon, text=text, slider=slider)
122
123         if pressure_name:
124             row.prop(brush, pressure_name, text="")
125
126         if unified_name and not header:
127             # NOTE: We don't draw UnifiedPaintSettings in the header to reduce clutter. D5928#136281
128             row.prop(ups, unified_name, text="", icon="BRUSHES_ALL")
129
130         return row
131
132     @staticmethod
133     def prop_unified_color(parent, context, brush, prop_name, *, text=None):
134         ups = context.tool_settings.unified_paint_settings
135         prop_owner = ups if ups.use_unified_color else brush
136         parent.prop(prop_owner, prop_name, text=text)
137
138     @staticmethod
139     def prop_unified_color_picker(parent, context, brush, prop_name, value_slider=True):
140         ups = context.tool_settings.unified_paint_settings
141         prop_owner = ups if ups.use_unified_color else brush
142         parent.template_color_picker(prop_owner, prop_name, value_slider=value_slider)
143
144
145 ### Classes to let various paint modes' panels share code, by sub-classing these classes. ###
146 class BrushPanel(UnifiedPaintPanel):
147     @classmethod
148     def poll(cls, context):
149         return cls.get_brush_mode(context) is not None
150
151
152 class BrushSelectPanel(BrushPanel):
153     bl_label = "Brushes"
154
155     def draw(self, context):
156         layout = self.layout
157         settings = self.paint_settings(context)
158         brush = settings.brush
159
160         row = layout.row()
161         large_preview = True
162         if large_preview:
163             row.column().template_ID_preview(settings, "brush", new="brush.add", rows=3, cols=8, hide_buttons=False)
164         else:
165             row.column().template_ID(settings, "brush", new="brush.add")
166         col = row.column()
167         col.menu("VIEW3D_MT_brush_context_menu", icon='DOWNARROW_HLT', text="")
168
169         if brush is not None:
170             col.prop(brush, "use_custom_icon", toggle=True, icon='FILE_IMAGE', text="")
171
172             if brush.use_custom_icon:
173                 layout.prop(brush, "icon_filepath", text="")
174
175
176 class ColorPalettePanel(BrushPanel):
177     bl_label = "Color Palette"
178     bl_options = {'DEFAULT_CLOSED'}
179
180     @classmethod
181     def poll(cls, context):
182         if not super().poll(context):
183             return False
184
185         settings = cls.paint_settings(context)
186         brush = settings.brush
187
188         if context.space_data.type == 'IMAGE_EDITOR' or context.image_paint_object:
189             capabilities = brush.image_paint_capabilities
190             return capabilities.has_color
191
192         elif context.vertex_paint_object:
193             capabilities = brush.vertex_paint_capabilities
194             return capabilities.has_color
195         return False
196
197     def draw(self, context):
198         layout = self.layout
199         settings = self.paint_settings(context)
200
201         layout.template_ID(settings, "palette", new="palette.new")
202         if settings.palette:
203             layout.template_palette(settings, "palette", color=True)
204
205
206 class ClonePanel(BrushPanel):
207     bl_label = "Clone"
208     bl_options = {'DEFAULT_CLOSED'}
209
210     @classmethod
211     def poll(cls, context):
212         if not super().poll(context):
213             return False
214
215         settings = cls.paint_settings(context)
216
217         mode = cls.get_brush_mode(context)
218         if mode == 'PAINT_TEXTURE':
219             brush = settings.brush
220             return brush.image_tool == 'CLONE'
221         return False
222
223     def draw_header(self, context):
224         settings = self.paint_settings(context)
225         self.layout.prop(settings, "use_clone_layer", text="")
226
227     def draw(self, context):
228         layout = self.layout
229         settings = self.paint_settings(context)
230
231         layout.active = settings.use_clone_layer
232
233         ob = context.active_object
234         col = layout.column()
235
236         if settings.mode == 'MATERIAL':
237             if len(ob.material_slots) > 1:
238                 col.label(text="Materials")
239                 col.template_list(
240                     "MATERIAL_UL_matslots", "",
241                     ob, "material_slots",
242                     ob, "active_material_index",
243                     rows=2,
244                 )
245
246             mat = ob.active_material
247             if mat:
248                 col.label(text="Source Clone Slot")
249                 col.template_list(
250                     "TEXTURE_UL_texpaintslots", "",
251                     mat, "texture_paint_images",
252                     mat, "paint_clone_slot",
253                     rows=2,
254                 )
255
256         elif settings.mode == 'IMAGE':
257             mesh = ob.data
258
259             clone_text = mesh.uv_layer_clone.name if mesh.uv_layer_clone else ""
260             col.label(text="Source Clone Image")
261             col.template_ID(settings, "clone_image")
262             col.label(text="Source Clone UV Map")
263             col.menu("VIEW3D_MT_tools_projectpaint_clone", text=clone_text, translate=False)
264
265
266 class TextureMaskPanel(BrushPanel):
267     bl_label = "Texture Mask"
268     bl_options = {'DEFAULT_CLOSED'}
269
270     def draw(self, context):
271         layout = self.layout
272         layout.use_property_split = True
273         layout.use_property_decorate = False
274
275         brush = context.tool_settings.image_paint.brush
276
277         col = layout.column()
278         col.template_ID_preview(brush, "mask_texture", new="texture.new", rows=3, cols=8)
279
280         mask_tex_slot = brush.mask_texture_slot
281
282         # map_mode
283         layout.row().prop(mask_tex_slot, "mask_map_mode", text="Mask Mapping")
284
285         if mask_tex_slot.map_mode == 'STENCIL':
286             if brush.mask_texture and brush.mask_texture.type == 'IMAGE':
287                 layout.operator("brush.stencil_fit_image_aspect").mask = True
288             layout.operator("brush.stencil_reset_transform").mask = True
289
290         col = layout.column()
291         col.prop(brush, "use_pressure_masking", text="Pressure Masking")
292         # angle and texture_angle_source
293         if mask_tex_slot.has_texture_angle:
294             col = layout.column()
295             col.prop(mask_tex_slot, "angle", text="Angle")
296             if mask_tex_slot.has_texture_angle_source:
297                 col.prop(mask_tex_slot, "use_rake", text="Rake")
298
299                 if brush.brush_capabilities.has_random_texture_angle and mask_tex_slot.has_random_texture_angle:
300                     col.prop(mask_tex_slot, "use_random", text="Random")
301                     if mask_tex_slot.use_random:
302                         col.prop(mask_tex_slot, "random_angle", text="Random Angle")
303
304         # scale and offset
305         col.prop(mask_tex_slot, "offset")
306         col.prop(mask_tex_slot, "scale")
307
308
309 class StrokePanel(BrushPanel):
310     bl_label = "Stroke"
311     bl_options = {'DEFAULT_CLOSED'}
312
313     def draw(self, context):
314         layout = self.layout
315         layout.use_property_split = True
316         layout.use_property_decorate = False
317
318         mode = self.get_brush_mode(context)
319         settings = self.paint_settings(context)
320         brush = settings.brush
321
322         col = layout.column()
323
324         col.prop(brush, "stroke_method")
325         col.separator()
326
327         if brush.use_anchor:
328             col.prop(brush, "use_edge_to_edge", text="Edge To Edge")
329
330         if brush.use_airbrush:
331             col.prop(brush, "rate", text="Rate", slider=True)
332
333         if brush.use_space:
334             row = col.row(align=True)
335             row.prop(brush, "spacing", text="Spacing")
336             row.prop(brush, "use_pressure_spacing", toggle=True, text="")
337
338         if brush.use_line or brush.use_curve:
339             row = col.row(align=True)
340             row.prop(brush, "spacing", text="Spacing")
341
342         if mode == 'SCULPT':
343             col.row().prop(brush, "use_scene_spacing", text="Spacing Distance", expand=True)
344
345         if mode in {'PAINT_TEXTURE', 'PAINT_2D', 'SCULPT'}:
346             if brush.image_paint_capabilities.has_space_attenuation or brush.sculpt_capabilities.has_space_attenuation:
347                 col.prop(brush, "use_space_attenuation")
348
349         if brush.use_curve:
350             col.separator()
351             col.template_ID(brush, "paint_curve", new="paintcurve.new")
352             col.operator("paintcurve.draw")
353             col.separator()
354
355         if brush.use_space:
356             col.separator()
357             row = col.row(align=True)
358             col.prop(brush, "dash_ratio", text="Dash Ratio")
359             col.prop(brush, "dash_samples", text="Dash Length")
360
361         if (mode == 'SCULPT' and brush.sculpt_capabilities.has_jitter) or mode != 'SCULPT':
362             col.separator()
363             row = col.row(align=True)
364             if brush.jitter_unit == 'BRUSH':
365                 row.prop(brush, "jitter", slider=True)
366             else:
367                 row.prop(brush, "jitter_absolute")
368             row.prop(brush, "use_pressure_jitter", toggle=True, text="")
369             col.row().prop(brush, "jitter_unit", expand=True)
370
371         col.separator()
372         col.prop(settings, "input_samples")
373
374
375 class SmoothStrokePanel(BrushPanel):
376     bl_label = "Stabilize Stroke"
377     bl_options = {'DEFAULT_CLOSED'}
378
379     @classmethod
380     def poll(cls, context):
381         if not super().poll(context):
382             return False
383         settings = cls.paint_settings(context)
384         brush = settings.brush
385         if brush.brush_capabilities.has_smooth_stroke:
386             return True
387         return False
388
389     def draw_header(self, context):
390         settings = self.paint_settings(context)
391         brush = settings.brush
392
393         self.layout.prop(brush, "use_smooth_stroke", text="")
394
395     def draw(self, context):
396         layout = self.layout
397         layout.use_property_split = True
398         layout.use_property_decorate = False
399
400         settings = self.paint_settings(context)
401         brush = settings.brush
402
403         col = layout.column()
404         col.active = brush.use_smooth_stroke
405         col.prop(brush, "smooth_stroke_radius", text="Radius", slider=True)
406         col.prop(brush, "smooth_stroke_factor", text="Factor", slider=True)
407
408
409 class FalloffPanel(BrushPanel):
410     bl_label = "Falloff"
411     bl_options = {'DEFAULT_CLOSED'}
412
413     @classmethod
414     def poll(cls, context):
415         if not super().poll(context):
416             return False
417         settings = cls.paint_settings(context)
418         return (settings and settings.brush and settings.brush.curve)
419
420     def draw(self, context):
421         layout = self.layout
422         settings = self.paint_settings(context)
423         mode = self.get_brush_mode(context)
424         brush = settings.brush
425
426         if brush is None:
427             return
428
429         col = layout.column(align=True)
430         row = col.row(align=True)
431         row.prop(brush, "curve_preset", text="")
432
433         if brush.curve_preset == 'CUSTOM':
434             layout.template_curve_mapping(brush, "curve", brush=True)
435
436             col = layout.column(align=True)
437             row = col.row(align=True)
438             row.operator("brush.curve_preset", icon='SMOOTHCURVE', text="").shape = 'SMOOTH'
439             row.operator("brush.curve_preset", icon='SPHERECURVE', text="").shape = 'ROUND'
440             row.operator("brush.curve_preset", icon='ROOTCURVE', text="").shape = 'ROOT'
441             row.operator("brush.curve_preset", icon='SHARPCURVE', text="").shape = 'SHARP'
442             row.operator("brush.curve_preset", icon='LINCURVE', text="").shape = 'LINE'
443             row.operator("brush.curve_preset", icon='NOCURVE', text="").shape = 'MAX'
444
445         if mode in {'SCULPT', 'PAINT_VERTEX', 'PAINT_WEIGHT'} and brush.sculpt_tool != 'POSE':
446             col.separator()
447             row = col.row(align=True)
448             row.use_property_split = True
449             row.use_property_decorate = False
450             row.prop(brush, "falloff_shape", expand=True)
451
452
453 class DisplayPanel(BrushPanel):
454     bl_label = "Brush Cursor"
455     bl_options = {'DEFAULT_CLOSED'}
456
457     def draw_header(self, context):
458         settings = self.paint_settings(context)
459         if settings and not self.is_popover:
460             self.layout.prop(settings, "show_brush", text="")
461
462     def draw(self, context):
463         layout = self.layout
464         layout.use_property_split = True
465         layout.use_property_decorate = False
466
467         mode = self.get_brush_mode(context)
468         settings = self.paint_settings(context)
469         brush = settings.brush
470         tex_slot = brush.texture_slot
471         tex_slot_mask = brush.mask_texture_slot
472
473         if self.is_popover:
474             row = layout.row(align=True)
475             row.prop(settings, "show_brush", text="")
476             row.label(text="Display Cursor")
477
478         col = layout.column()
479         col.active = brush.brush_capabilities.has_overlay and settings.show_brush
480
481         col.prop(brush, "cursor_color_add", text="Cursor Color")
482         if mode == 'SCULPT' and brush.sculpt_capabilities.has_secondary_color:
483             col.prop(brush, "cursor_color_subtract", text="Inverse Cursor Color")
484
485         col.separator()
486
487         row = col.row(align=True)
488         row.prop(brush, "cursor_overlay_alpha", text="Falloff Opacity")
489         row.prop(brush, "use_cursor_overlay_override", toggle=True, text="", icon='BRUSH_DATA')
490         row.prop(
491             brush, "use_cursor_overlay", text="", toggle=True,
492             icon='HIDE_OFF' if brush.use_cursor_overlay else 'HIDE_ON',
493         )
494
495         if mode in ['PAINT_2D', 'PAINT_TEXTURE', 'PAINT_VERTEX', 'SCULPT']:
496             row = col.row(align=True)
497             row.prop(brush, "texture_overlay_alpha", text="Texture Opacity")
498             row.prop(brush, "use_primary_overlay_override", toggle=True, text="", icon='BRUSH_DATA')
499             if tex_slot.map_mode != 'STENCIL':
500                 row.prop(
501                     brush, "use_primary_overlay", text="", toggle=True,
502                     icon='HIDE_OFF' if brush.use_primary_overlay else 'HIDE_ON',
503                 )
504
505         if mode in ['PAINT_TEXTURE', 'PAINT_2D']:
506             row = col.row(align=True)
507             row.prop(brush, "mask_overlay_alpha", text="Mask Texture Opacity")
508             row.prop(brush, "use_secondary_overlay_override", toggle=True, text="", icon='BRUSH_DATA')
509             if tex_slot_mask.map_mode != 'STENCIL':
510                 row.prop(
511                     brush, "use_secondary_overlay", text="", toggle=True,
512                     icon='HIDE_OFF' if brush.use_secondary_overlay else 'HIDE_ON',
513                 )
514
515
516 class VIEW3D_MT_tools_projectpaint_clone(Menu):
517     bl_label = "Clone Layer"
518
519     def draw(self, context):
520         layout = self.layout
521
522         for i, uv_layer in enumerate(context.active_object.data.uv_layers):
523             props = layout.operator("wm.context_set_int", text=uv_layer.name, translate=False)
524             props.data_path = "active_object.data.uv_layer_clone_index"
525             props.value = i
526
527
528 def brush_settings(layout, context, brush, popover=False):
529     """ Draw simple brush settings for Sculpt,
530         Texture/Vertex/Weight Paint modes, or skip certain settings for the popover """
531
532     mode = UnifiedPaintPanel.get_brush_mode(context)
533
534     ### Draw simple settings unique to each paint mode. ###
535     brush_shared_settings(layout, context, brush, popover)
536
537     # Sculpt Mode #
538     if mode == 'SCULPT':
539         capabilities = brush.sculpt_capabilities
540
541         # normal_radius_factor
542         layout.prop(brush, "normal_radius_factor", slider=True)
543         layout.prop(brush, "hardness", slider=True)
544
545         # auto_smooth_factor and use_inverse_smooth_pressure
546         if capabilities.has_auto_smooth:
547             UnifiedPaintPanel.prop_unified(
548                 layout,
549                 context,
550                 brush,
551                 "auto_smooth_factor",
552                 pressure_name="use_inverse_smooth_pressure",
553                 slider=True,
554             )
555
556         # topology_rake_factor
557         if (
558                 capabilities.has_topology_rake and
559                 context.sculpt_object.use_dynamic_topology_sculpting
560         ):
561             layout.prop(brush, "topology_rake_factor", slider=True)
562
563         # normal_weight
564         if capabilities.has_normal_weight:
565             layout.prop(brush, "normal_weight", slider=True)
566
567         # crease_pinch_factor
568         if capabilities.has_pinch_factor:
569             text = "Pinch"
570             if brush.sculpt_tool in {'BLOB', 'SNAKE_HOOK'}:
571                 text = "Magnify"
572             layout.prop(brush, "crease_pinch_factor", slider=True, text=text)
573
574         # rake_factor
575         if capabilities.has_rake_factor:
576             layout.prop(brush, "rake_factor", slider=True)
577
578         # plane_offset, use_offset_pressure, use_plane_trim, plane_trim
579         if capabilities.has_plane_offset:
580             layout.separator()
581             UnifiedPaintPanel.prop_unified(
582                 layout,
583                 context,
584                 brush,
585                 "plane_offset",
586                 pressure_name="use_offset_pressure",
587                 slider=True,
588             )
589
590             row = layout.row(heading="Plane Trim")
591             row.prop(brush, "use_plane_trim", text="")
592             sub = row.row()
593             sub.active = brush.use_plane_trim
594             sub.prop(brush, "plane_trim", slider=True, text="")
595
596             layout.separator()
597
598         # height
599         if capabilities.has_height:
600             layout.prop(brush, "height", slider=True, text="Height")
601
602         # use_persistent, set_persistent_base
603         if capabilities.has_persistence:
604             ob = context.sculpt_object
605             layout.separator()
606             layout.prop(brush, "use_persistent")
607             layout.operator("sculpt.set_persistent_base")
608             layout.separator()
609
610         if brush.sculpt_tool == 'CLAY_STRIPS':
611             row = layout.row()
612             row.prop(brush, "tip_roundness")
613
614         if brush.sculpt_tool == 'ELASTIC_DEFORM':
615             layout.separator()
616             layout.prop(brush, "elastic_deform_type")
617             layout.prop(brush, "elastic_deform_volume_preservation", slider=True)
618             layout.separator()
619
620         if brush.sculpt_tool == 'POSE':
621             layout.separator()
622             layout.prop(brush, "pose_deform_type")
623             layout.prop(brush, "pose_origin_type")
624             layout.prop(brush, "pose_offset")
625             layout.prop(brush, "pose_smooth_iterations")
626             if brush.pose_deform_type == 'ROTATE_TWIST':
627               layout.prop(brush, "pose_ik_segments")
628             layout.prop(brush, "use_pose_ik_anchored")
629             layout.separator()
630
631         if brush.sculpt_tool == 'CLOTH':
632             layout.separator()
633             layout.prop(brush, "cloth_sim_limit")
634             layout.prop(brush, "cloth_sim_falloff")
635             layout.separator()
636             layout.prop(brush, "cloth_deform_type")
637             layout.prop(brush, "cloth_force_falloff_type")
638             layout.separator()
639             layout.prop(brush, "cloth_mass")
640             layout.prop(brush, "cloth_damping")
641             layout.separator()
642
643         if brush.sculpt_tool == 'SCRAPE':
644             row = layout.row()
645             row.prop(brush, "area_radius_factor", slider=True)
646             row = layout.row()
647             row.prop(brush, "invert_to_scrape_fill", text="Invert to Fill")
648
649         if brush.sculpt_tool == 'FILL':
650             row = layout.row()
651             row.prop(brush, "area_radius_factor", slider=True)
652             row = layout.row()
653             row.prop(brush, "invert_to_scrape_fill", text="Invert to Scrape")
654
655         if brush.sculpt_tool == 'GRAB':
656             layout.prop(brush, "use_grab_active_vertex")
657
658         if brush.sculpt_tool == 'MULTIPLANE_SCRAPE':
659             col = layout.column()
660             col.prop(brush, "multiplane_scrape_angle")
661             col.prop(brush, "use_multiplane_scrape_dynamic")
662             col.prop(brush, "show_multiplane_scrape_planes_preview")
663
664         if brush.sculpt_tool == 'SMOOTH':
665             col = layout.column()
666             col.prop(brush, "smooth_deform_type")
667             if brush.smooth_deform_type == 'SURFACE':
668                 col.prop(brush, "surface_smooth_shape_preservation")
669                 col.prop(brush, "surface_smooth_current_vertex")
670                 col.prop(brush, "surface_smooth_iterations")
671
672         if brush.sculpt_tool == 'MASK':
673             layout.row().prop(brush, "mask_tool", expand=True)
674
675     # 3D and 2D Texture Paint Mode.
676     elif mode in {'PAINT_TEXTURE', 'PAINT_2D'}:
677         capabilities = brush.image_paint_capabilities
678
679         if brush.image_tool == 'FILL':
680             # For some reason fill threshold only appears to be implemented in 2D paint.
681             if brush.color_type == 'COLOR':
682                 if mode == 'PAINT_2D':
683                     layout.prop(brush, "fill_threshold", text="Fill Threshold", slider=True)
684             elif brush.color_type == 'GRADIENT':
685                 layout.row().prop(brush, "gradient_fill_mode", expand=True)
686
687
688 def brush_shared_settings(layout, context, brush, popover=False):
689     """ Draw simple brush settings that are shared between different paint modes. """
690
691     mode = UnifiedPaintPanel.get_brush_mode(context)
692
693     ### Determine which settings to draw. ###
694     blend_mode = False
695     size = False
696     size_mode = False
697     strength = False
698     strength_pressure = False
699     weight = False
700     direction = False
701
702     # 3D and 2D Texture Paint #
703     if mode in {'PAINT_TEXTURE', 'PAINT_2D'}:
704         if not popover:
705             blend_mode = brush.image_paint_capabilities.has_color
706             size = brush.image_paint_capabilities.has_radius
707             strength = strength_pressure = True
708
709     # Sculpt #
710     if mode == 'SCULPT':
711         size_mode = True
712         if not popover:
713             size = True
714             strength = True
715             strength_pressure = brush.sculpt_capabilities.has_strength_pressure
716             direction = not brush.sculpt_capabilities.has_direction
717
718     # Vertex Paint #
719     if mode == 'PAINT_VERTEX':
720         if not popover:
721             blend_mode = True
722             size = True
723             strength = True
724             strength_pressure = True
725
726     # Weight Paint #
727     if mode == 'PAINT_WEIGHT':
728         if not popover:
729             size = True
730             weight = brush.weight_paint_capabilities.has_weight
731             strength = strength_pressure = True
732         # Only draw blend mode for the Draw tool, because for other tools it is pointless. D5928#137944
733         if brush.weight_tool == 'DRAW':
734             blend_mode = True
735
736     # UV Sculpt #
737     if mode == 'UV_SCULPT':
738         size = True
739         strength = True
740
741     ### Draw settings. ###
742     ups = context.scene.tool_settings.unified_paint_settings
743
744     if blend_mode:
745         layout.prop(brush, "blend", text="Blend")
746         layout.separator()
747
748     if weight:
749         UnifiedPaintPanel.prop_unified(
750             layout,
751             context,
752             brush,
753             "weight",
754             unified_name="use_unified_weight",
755             slider=True,
756         )
757
758     size_owner = ups if ups.use_unified_size else brush
759     size_prop = "size"
760     if size_mode and (size_owner.use_locked_size == 'SCENE'):
761         size_prop = "unprojected_radius"
762     if size or size_mode:
763         if size:
764             UnifiedPaintPanel.prop_unified(
765                 layout,
766                 context,
767                 brush,
768                 size_prop,
769                 unified_name="use_unified_size",
770                 pressure_name="use_pressure_size",
771                 text="Radius",
772                 slider=True,
773             )
774         if size_mode:
775             layout.row().prop(size_owner, "use_locked_size", expand=True)
776             layout.separator()
777
778     if strength:
779         pressure_name = "use_pressure_strength" if strength_pressure else None
780         UnifiedPaintPanel.prop_unified(
781             layout,
782             context,
783             brush,
784             "strength",
785             unified_name="use_unified_strength",
786             pressure_name=pressure_name,
787             slider=True,
788         )
789         layout.separator()
790
791     if direction:
792         layout.row().prop(brush, "direction", expand=True)
793
794
795 def brush_settings_advanced(layout, context, brush, popover=False):
796     """Draw advanced brush settings for Sculpt, Texture/Vertex/Weight Paint modes."""
797
798     mode = UnifiedPaintPanel.get_brush_mode(context)
799
800     # In the popover we want to combine advanced brush settings with non-advanced brush settings.
801     if popover:
802         brush_settings(layout, context, brush, popover=True)
803         layout.separator()
804         layout.label(text="Advanced:")
805
806     # These options are shared across many modes.
807     use_accumulate = False
808     use_frontface = False
809
810     if mode == 'SCULPT':
811         capabilities = brush.sculpt_capabilities
812         use_accumulate = capabilities.has_accumulate
813         use_frontface = True
814
815         col = layout.column(heading="Auto-Masking", align=True)
816
817         # topology automasking
818         col.prop(brush, "use_automasking_topology", text="Topology")
819
820         # face masks automasking
821         col.prop(brush, "use_automasking_face_sets", text="Face Sets")
822
823         # boundary edges/face sets automasking
824         col.prop(brush, "use_automasking_boundary_edges", text="Mesh Boundary")
825         col.prop(brush, "use_automasking_boundary_face_sets", text="Face Sets Boundary")
826         col.prop(brush, "automasking_boundary_edges_propagation_steps")
827
828         layout.separator()
829
830         # sculpt plane settings
831         if capabilities.has_sculpt_plane:
832             layout.prop(brush, "sculpt_plane")
833             col = layout.column(heading="Use Original", align=True)
834             col.prop(brush, "use_original_normal", text="Normal")
835             col.prop(brush, "use_original_plane", text="Plane")
836             layout.separator()
837
838     # 3D and 2D Texture Paint.
839     elif mode in {'PAINT_TEXTURE', 'PAINT_2D'}:
840         capabilities = brush.image_paint_capabilities
841         use_accumulate = capabilities.has_accumulate
842
843         if mode == 'PAINT_2D':
844             layout.prop(brush, "use_paint_antialiasing")
845         else:
846             layout.prop(brush, "use_alpha")
847
848         # Tool specific settings
849         if brush.image_tool == 'SOFTEN':
850             layout.separator()
851             layout.row().prop(brush, "direction", expand=True)
852             layout.prop(brush, "sharp_threshold")
853             if mode == 'PAINT_2D':
854                 layout.prop(brush, "blur_kernel_radius")
855             layout.prop(brush, "blur_mode")
856
857         elif brush.image_tool == 'MASK':
858             layout.prop(brush, "weight", text="Mask Value", slider=True)
859
860         elif brush.image_tool == 'CLONE':
861             if mode == 'PAINT_2D':
862                 layout.prop(brush, "clone_image", text="Image")
863                 layout.prop(brush, "clone_alpha", text="Alpha")
864
865     # Vertex Paint #
866     elif mode == 'PAINT_VERTEX':
867         layout.prop(brush, "use_alpha")
868         if brush.vertex_tool != 'SMEAR':
869             use_accumulate = True
870         use_frontface = True
871
872     # Weight Paint
873     elif mode == 'PAINT_WEIGHT':
874         if brush.weight_tool != 'SMEAR':
875             use_accumulate = True
876         use_frontface = True
877
878     # Draw shared settings.
879     if use_accumulate:
880         layout.prop(brush, "use_accumulate")
881
882     if use_frontface:
883         layout.prop(brush, "use_frontface", text="Front Faces Only")
884
885
886 def draw_color_settings(context, layout, brush, color_type=False):
887     """Draw color wheel and gradient settings."""
888     ups = context.scene.tool_settings.unified_paint_settings
889
890     if color_type:
891         row = layout.row()
892         row.use_property_split = False
893         row.prop(brush, "color_type", expand=True)
894
895     # Color wheel
896     if brush.color_type == 'COLOR':
897         UnifiedPaintPanel.prop_unified_color_picker(layout, context, brush, "color", value_slider=True)
898
899         row = layout.row(align=True)
900         UnifiedPaintPanel.prop_unified_color(row, context, brush, "color", text="")
901         UnifiedPaintPanel.prop_unified_color(row, context, brush, "secondary_color", text="")
902         row.separator()
903         row.operator("paint.brush_colors_flip", icon='FILE_REFRESH', text="", emboss=False)
904         row.prop(ups, "use_unified_color", text="", icon='BRUSHES_ALL')
905     # Gradient
906     elif brush.color_type == 'GRADIENT':
907         layout.template_color_ramp(brush, "gradient", expand=True)
908
909         layout.use_property_split = True
910
911         col = layout.column()
912
913         if brush.image_tool == 'DRAW':
914             UnifiedPaintPanel.prop_unified(
915                 col,
916                 context,
917                 brush,
918                 "secondary_color",
919                 unified_name="use_unified_color",
920                 text="Background Color",
921                 header=True,
922             )
923
924             col.prop(brush, "gradient_stroke_mode", text="Gradient Mapping")
925             if brush.gradient_stroke_mode in {'SPACING_REPEAT', 'SPACING_CLAMP'}:
926                 col.prop(brush, "grad_spacing")
927
928
929 # Used in both the View3D toolbar and texture properties
930 def brush_texture_settings(layout, brush, sculpt):
931     tex_slot = brush.texture_slot
932
933     layout.use_property_split = True
934     layout.use_property_decorate = False
935
936     # map_mode
937     if sculpt:
938         layout.prop(tex_slot, "map_mode", text="Mapping")
939     else:
940         layout.prop(tex_slot, "tex_paint_map_mode", text="Mapping")
941
942     layout.separator()
943
944     if tex_slot.map_mode == 'STENCIL':
945         if brush.texture and brush.texture.type == 'IMAGE':
946             layout.operator("brush.stencil_fit_image_aspect")
947         layout.operator("brush.stencil_reset_transform")
948
949     # angle and texture_angle_source
950     if tex_slot.has_texture_angle:
951         col = layout.column()
952         col.prop(tex_slot, "angle", text="Angle")
953         if tex_slot.has_texture_angle_source:
954             col.prop(tex_slot, "use_rake", text="Rake")
955
956             if brush.brush_capabilities.has_random_texture_angle and tex_slot.has_random_texture_angle:
957                 if sculpt:
958                     if brush.sculpt_capabilities.has_random_texture_angle:
959                         col.prop(tex_slot, "use_random", text="Random")
960                         if tex_slot.use_random:
961                             col.prop(tex_slot, "random_angle", text="Random Angle")
962                 else:
963                     col.prop(tex_slot, "use_random", text="Random")
964                     if tex_slot.use_random:
965                         col.prop(tex_slot, "random_angle", text="Random Angle")
966
967     # scale and offset
968     layout.prop(tex_slot, "offset")
969     layout.prop(tex_slot, "scale")
970
971     if sculpt:
972         # texture_sample_bias
973         layout.prop(brush, "texture_sample_bias", slider=True, text="Sample Bias")
974
975
976 def brush_mask_texture_settings(layout, brush):
977     mask_tex_slot = brush.mask_texture_slot
978
979     layout.use_property_split = True
980     layout.use_property_decorate = False
981
982     # map_mode
983     layout.row().prop(mask_tex_slot, "mask_map_mode", text="Mask Mapping")
984
985     if mask_tex_slot.map_mode == 'STENCIL':
986         if brush.mask_texture and brush.mask_texture.type == 'IMAGE':
987             layout.operator("brush.stencil_fit_image_aspect").mask = True
988         layout.operator("brush.stencil_reset_transform").mask = True
989
990     col = layout.column()
991     col.prop(brush, "use_pressure_masking", text="Pressure Masking")
992     # angle and texture_angle_source
993     if mask_tex_slot.has_texture_angle:
994         col = layout.column()
995         col.prop(mask_tex_slot, "angle", text="Angle")
996         if mask_tex_slot.has_texture_angle_source:
997             col.prop(mask_tex_slot, "use_rake", text="Rake")
998
999             if brush.brush_capabilities.has_random_texture_angle and mask_tex_slot.has_random_texture_angle:
1000                 col.prop(mask_tex_slot, "use_random", text="Random")
1001                 if mask_tex_slot.use_random:
1002                     col.prop(mask_tex_slot, "random_angle", text="Random Angle")
1003
1004     # scale and offset
1005     col.prop(mask_tex_slot, "offset")
1006     col.prop(mask_tex_slot, "scale")
1007
1008
1009 def brush_basic_texpaint_settings(layout, context, brush, *, compact=False):
1010     """Draw Tool Settings header for Vertex Paint and 2D and 3D Texture Paint modes."""
1011     capabilities = brush.image_paint_capabilities
1012
1013     if capabilities.has_color:
1014         UnifiedPaintPanel.prop_unified_color(layout, context, brush, "color", text="")
1015         layout.prop(brush, "blend", text="" if compact else "Blend")
1016
1017     UnifiedPaintPanel.prop_unified(
1018         layout,
1019         context,
1020         brush,
1021         "size",
1022         pressure_name="use_pressure_size",
1023         unified_name="use_unified_size",
1024         slider=True,
1025         text="Radius",
1026         header=True
1027     )
1028     UnifiedPaintPanel.prop_unified(
1029         layout,
1030         context,
1031         brush,
1032         "strength",
1033         pressure_name="use_pressure_strength",
1034         unified_name="use_unified_strength",
1035         header=True
1036     )
1037
1038
1039 def brush_basic_gpencil_paint_settings(layout, context, brush, *, compact=False):
1040     tool_settings = context.tool_settings
1041     settings = tool_settings.gpencil_paint
1042     gp_settings = brush.gpencil_settings
1043     tool = context.workspace.tools.from_space_view3d_mode(context.mode, create=False)
1044     if gp_settings is None:
1045         return
1046
1047     # Brush details
1048     if brush.gpencil_tool == 'ERASE':
1049         row = layout.row(align=True)
1050         row.prop(brush, "size", text="Radius")
1051         row.prop(gp_settings, "use_pressure", text="", icon='STYLUS_PRESSURE')
1052         row.prop(gp_settings, "use_occlude_eraser", text="", icon='XRAY')
1053
1054         row = layout.row(align=True)
1055         row.prop(gp_settings, "eraser_mode", expand=True)
1056         if gp_settings.eraser_mode == 'SOFT':
1057             row = layout.row(align=True)
1058             row.prop(gp_settings, "pen_strength", slider=True)
1059             row.prop(gp_settings, "use_strength_pressure", text="", icon='STYLUS_PRESSURE')
1060             row = layout.row(align=True)
1061             row.prop(gp_settings, "eraser_strength_factor")
1062             row = layout.row(align=True)
1063             row.prop(gp_settings, "eraser_thickness_factor")
1064
1065         row = layout.row(align=True)
1066         row.prop(settings, "show_brush", text="Display Cursor")
1067
1068     # FIXME: tools must use their own UI drawing!
1069     elif brush.gpencil_tool == 'FILL':
1070         row = layout.row(align=True)
1071         row.prop(gp_settings, "fill_leak", text="Leak Size")
1072         row = layout.row(align=True)
1073         row.prop(brush, "size", text="Thickness")
1074         row = layout.row(align=True)
1075         row.prop(gp_settings, "fill_simplify_level", text="Simplify")
1076
1077     else:  # brush.gpencil_tool == 'DRAW/TINT':
1078         row = layout.row(align=True)
1079         row.prop(brush, "size", text="Radius")
1080         row.prop(gp_settings, "use_pressure", text="", icon='STYLUS_PRESSURE')
1081
1082         if gp_settings.use_pressure and context.area.type == 'PROPERTIES':
1083             col = layout.column()
1084             col.template_curve_mapping(gp_settings, "curve_sensitivity", brush=True,
1085                                       use_negative_slope=True)
1086
1087         row = layout.row(align=True)
1088         row.prop(gp_settings, "pen_strength", slider=True)
1089         row.prop(gp_settings, "use_strength_pressure", text="", icon='STYLUS_PRESSURE')
1090
1091         if gp_settings.use_strength_pressure and context.area.type == 'PROPERTIES':
1092             col = layout.column()
1093             col.template_curve_mapping(gp_settings, "curve_strength", brush=True,
1094                                         use_negative_slope=True)
1095
1096         if brush.gpencil_tool == 'TINT':
1097             row = layout.row(align=True)
1098             row.prop(gp_settings, "vertex_mode", text="Mode")
1099
1100     # FIXME: tools must use their own UI drawing!
1101     if tool.idname in {
1102             "builtin.arc",
1103             "builtin.curve",
1104             "builtin.line",
1105             "builtin.box",
1106             "builtin.circle",
1107             "builtin.polyline"
1108     }:
1109         settings = context.tool_settings.gpencil_sculpt
1110         if compact:
1111             row = layout.row(align=True)
1112             row.prop(settings, "use_thickness_curve", text="", icon='SPHERECURVE')
1113             sub = row.row(align=True)
1114             sub.active = settings.use_thickness_curve
1115             sub.popover(
1116                 panel="TOPBAR_PT_gpencil_primitive",
1117                 text="Thickness Profile",
1118             )
1119         else:
1120             row = layout.row(align=True)
1121             row.prop(settings, "use_thickness_curve", text="Use Thickness Profile")
1122             sub = row.row(align=True)
1123             if settings.use_thickness_curve:
1124                 # Curve
1125                 layout.template_curve_mapping(settings, "thickness_primitive_curve", brush=True)
1126
1127
1128 def brush_basic_gpencil_sculpt_settings(layout, context, brush, *, compact=False):
1129     gp_settings = brush.gpencil_settings
1130     tool = brush.gpencil_sculpt_tool
1131
1132     row = layout.row(align=True)
1133     row.prop(brush, "size", slider=True)
1134     sub = row.row(align=True)
1135     sub.enabled = tool not in {'GRAB', 'CLONE'}
1136     sub.prop(gp_settings, "use_pressure", text="")
1137
1138     row = layout.row(align=True)
1139     row.prop(brush, "strength", slider=True)
1140     row.prop(brush, "use_pressure_strength", text="")
1141
1142     if compact:
1143         if tool in {'THICKNESS', 'STRENGTH', 'PINCH', 'TWIST'}:
1144             row.separator()
1145             row.prop(gp_settings, "direction", expand=True, text="")
1146     else:
1147         use_property_split_prev = layout.use_property_split
1148         layout.use_property_split = False
1149         if tool in {'THICKNESS', 'STRENGTH'}:
1150             layout.row().prop(gp_settings, "direction", expand=True)
1151         elif tool == 'PINCH':
1152             row = layout.row(align=True)
1153             row.prop_enum(gp_settings, "direction", value='ADD', text="Pinch")
1154             row.prop_enum(gp_settings, "direction", value='SUBTRACT', text="Inflate")
1155         elif tool == 'TWIST':
1156             row = layout.row(align=True)
1157             row.prop_enum(gp_settings, "direction", value='ADD', text="CCW")
1158             row.prop_enum(gp_settings, "direction", value='SUBTRACT', text="CW")
1159         layout.use_property_split = use_property_split_prev
1160
1161
1162 def brush_basic_gpencil_weight_settings(layout, _context, brush, *, compact=False):
1163     gp_settings = brush.gpencil_settings
1164     layout.prop(brush, "size", slider=True)
1165
1166     row = layout.row(align=True)
1167     row.prop(brush, "strength", slider=True)
1168     row.prop(brush, "use_pressure_strength", text="")
1169
1170     layout.prop(brush, "weight", slider=True)
1171
1172
1173 def brush_basic_gpencil_vertex_settings(layout, _context, brush, *, compact=False):
1174     gp_settings = brush.gpencil_settings
1175
1176     # Brush details
1177     row = layout.row(align=True)
1178     row.prop(brush, "size", text="Radius")
1179     row.prop(gp_settings, "use_pressure", text="", icon='STYLUS_PRESSURE')
1180
1181     if brush.gpencil_vertex_tool in {'DRAW', 'BLUR', 'SMEAR'}:
1182         row = layout.row(align=True)
1183         row.prop(gp_settings, "pen_strength", slider=True)
1184         row.prop(gp_settings, "use_strength_pressure", text="", icon='STYLUS_PRESSURE')
1185
1186     if brush.gpencil_vertex_tool in {'DRAW', 'REPLACE'}:
1187         row = layout.row(align=True)
1188         row.prop(gp_settings, "vertex_mode", text="Mode")
1189
1190
1191 classes = (
1192     VIEW3D_MT_tools_projectpaint_clone,
1193 )
1194
1195 if __name__ == "__main__":  # only for live edit.
1196     from bpy.utils import register_class
1197     for cls in classes:
1198         register_class(cls)