92d421f63a865fd23369f460f77ca5061260f60c
[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_origin_type")
623             layout.prop(brush, "pose_offset")
624             layout.prop(brush, "pose_smooth_iterations")
625             layout.prop(brush, "pose_ik_segments")
626             layout.prop(brush, "use_pose_ik_anchored")
627             layout.separator()
628
629         if brush.sculpt_tool == 'CLOTH':
630             layout.separator()
631             layout.prop(brush, "cloth_sim_limit")
632             layout.prop(brush, "cloth_sim_falloff")
633             layout.separator()
634             layout.prop(brush, "cloth_deform_type")
635             layout.prop(brush, "cloth_force_falloff_type")
636             layout.separator()
637             layout.prop(brush, "cloth_mass")
638             layout.prop(brush, "cloth_damping")
639             layout.separator()
640
641         if brush.sculpt_tool == 'SCRAPE':
642             row = layout.row()
643             row.prop(brush, "area_radius_factor", slider=True)
644             row = layout.row()
645             row.prop(brush, "invert_to_scrape_fill", text="Invert to Fill")
646
647         if brush.sculpt_tool == 'FILL':
648             row = layout.row()
649             row.prop(brush, "area_radius_factor", slider=True)
650             row = layout.row()
651             row.prop(brush, "invert_to_scrape_fill", text="Invert to Scrape")
652
653         if brush.sculpt_tool == 'GRAB':
654             layout.prop(brush, "use_grab_active_vertex")
655
656         if brush.sculpt_tool == 'MULTIPLANE_SCRAPE':
657             col = layout.column()
658             col.prop(brush, "multiplane_scrape_angle")
659             col.prop(brush, "use_multiplane_scrape_dynamic")
660             col.prop(brush, "show_multiplane_scrape_planes_preview")
661
662         if brush.sculpt_tool == 'SMOOTH':
663             col = layout.column()
664             col.prop(brush, "smooth_deform_type")
665             if brush.smooth_deform_type == 'SURFACE':
666                 col.prop(brush, "surface_smooth_shape_preservation")
667                 col.prop(brush, "surface_smooth_current_vertex")
668                 col.prop(brush, "surface_smooth_iterations")
669
670         if brush.sculpt_tool == 'MASK':
671             layout.row().prop(brush, "mask_tool", expand=True)
672
673     # 3D and 2D Texture Paint Mode.
674     elif mode in {'PAINT_TEXTURE', 'PAINT_2D'}:
675         capabilities = brush.image_paint_capabilities
676
677         if brush.image_tool == 'FILL':
678             # For some reason fill threshold only appears to be implemented in 2D paint.
679             if brush.color_type == 'COLOR':
680                 if mode == 'PAINT_2D':
681                     layout.prop(brush, "fill_threshold", text="Fill Threshold", slider=True)
682             elif brush.color_type == 'GRADIENT':
683                 layout.row().prop(brush, "gradient_fill_mode", expand=True)
684
685
686 def brush_shared_settings(layout, context, brush, popover=False):
687     """ Draw simple brush settings that are shared between different paint modes. """
688
689     mode = UnifiedPaintPanel.get_brush_mode(context)
690
691     ### Determine which settings to draw. ###
692     blend_mode = False
693     size = False
694     size_mode = False
695     strength = False
696     strength_pressure = False
697     weight = False
698     direction = False
699
700     # 3D and 2D Texture Paint #
701     if mode in {'PAINT_TEXTURE', 'PAINT_2D'}:
702         if not popover:
703             blend_mode = brush.image_paint_capabilities.has_color
704             size = brush.image_paint_capabilities.has_radius
705             strength = strength_pressure = True
706
707     # Sculpt #
708     if mode == 'SCULPT':
709         size_mode = True
710         if not popover:
711             size = True
712             strength = True
713             strength_pressure = brush.sculpt_capabilities.has_strength_pressure
714             direction = not brush.sculpt_capabilities.has_direction
715
716     # Vertex Paint #
717     if mode == 'PAINT_VERTEX':
718         if not popover:
719             blend_mode = True
720             size = True
721             strength = True
722             strength_pressure = True
723
724     # Weight Paint #
725     if mode == 'PAINT_WEIGHT':
726         if not popover:
727             size = True
728             weight = brush.weight_paint_capabilities.has_weight
729             strength = strength_pressure = True
730         # Only draw blend mode for the Draw tool, because for other tools it is pointless. D5928#137944
731         if brush.weight_tool == 'DRAW':
732             blend_mode = True
733
734     # UV Sculpt #
735     if mode == 'UV_SCULPT':
736         size = True
737         strength = True
738
739     ### Draw settings. ###
740     ups = context.scene.tool_settings.unified_paint_settings
741
742     if blend_mode:
743         layout.prop(brush, "blend", text="Blend")
744         layout.separator()
745
746     if weight:
747         UnifiedPaintPanel.prop_unified(
748             layout,
749             context,
750             brush,
751             "weight",
752             unified_name="use_unified_weight",
753             slider=True,
754         )
755
756     size_owner = ups if ups.use_unified_size else brush
757     size_prop = "size"
758     if size_mode and (size_owner.use_locked_size == 'SCENE'):
759         size_prop = "unprojected_radius"
760     if size or size_mode:
761         if size:
762             UnifiedPaintPanel.prop_unified(
763                 layout,
764                 context,
765                 brush,
766                 size_prop,
767                 unified_name="use_unified_size",
768                 pressure_name="use_pressure_size",
769                 text="Radius",
770                 slider=True,
771             )
772         if size_mode:
773             layout.row().prop(size_owner, "use_locked_size", expand=True)
774             layout.separator()
775
776     if strength:
777         pressure_name = "use_pressure_strength" if strength_pressure else None
778         UnifiedPaintPanel.prop_unified(
779             layout,
780             context,
781             brush,
782             "strength",
783             unified_name="use_unified_strength",
784             pressure_name=pressure_name,
785             slider=True,
786         )
787         layout.separator()
788
789     if direction:
790         layout.row().prop(brush, "direction", expand=True)
791
792
793 def brush_settings_advanced(layout, context, brush, popover=False):
794     """Draw advanced brush settings for Sculpt, Texture/Vertex/Weight Paint modes."""
795
796     mode = UnifiedPaintPanel.get_brush_mode(context)
797
798     # In the popover we want to combine advanced brush settings with non-advanced brush settings.
799     if popover:
800         brush_settings(layout, context, brush, popover=True)
801         layout.separator()
802         layout.label(text="Advanced:")
803
804     # These options are shared across many modes.
805     use_accumulate = False
806     use_frontface = False
807
808     if mode == 'SCULPT':
809         capabilities = brush.sculpt_capabilities
810         use_accumulate = capabilities.has_accumulate
811         use_frontface = True
812
813         col = layout.column(heading="Auto-Masking", align=True)
814
815         # topology automasking
816         col.prop(brush, "use_automasking_topology", text="Topology")
817
818         # face masks automasking
819         col.prop(brush, "use_automasking_face_sets", text="Face Sets")
820
821         # boundary edges/face sets automasking
822         col.prop(brush, "use_automasking_boundary_edges", text="Mesh Boundary")
823         col.prop(brush, "use_automasking_boundary_face_sets", text="Face Sets Boundary")
824         col.prop(brush, "automasking_boundary_edges_propagation_steps")
825
826         layout.separator()
827
828         # sculpt plane settings
829         if capabilities.has_sculpt_plane:
830             layout.prop(brush, "sculpt_plane")
831             col = layout.column(heading="Use Original", align=True)
832             col.prop(brush, "use_original_normal", text="Normal")
833             col.prop(brush, "use_original_plane", text="Plane")
834             layout.separator()
835
836     # 3D and 2D Texture Paint.
837     elif mode in {'PAINT_TEXTURE', 'PAINT_2D'}:
838         capabilities = brush.image_paint_capabilities
839         use_accumulate = capabilities.has_accumulate
840
841         if mode == 'PAINT_2D':
842             layout.prop(brush, "use_paint_antialiasing")
843         else:
844             layout.prop(brush, "use_alpha")
845
846         # Tool specific settings
847         if brush.image_tool == 'SOFTEN':
848             layout.separator()
849             layout.row().prop(brush, "direction", expand=True)
850             layout.prop(brush, "sharp_threshold")
851             if mode == 'PAINT_2D':
852                 layout.prop(brush, "blur_kernel_radius")
853             layout.prop(brush, "blur_mode")
854
855         elif brush.image_tool == 'MASK':
856             layout.prop(brush, "weight", text="Mask Value", slider=True)
857
858         elif brush.image_tool == 'CLONE':
859             if mode == 'PAINT_2D':
860                 layout.prop(brush, "clone_image", text="Image")
861                 layout.prop(brush, "clone_alpha", text="Alpha")
862
863     # Vertex Paint #
864     elif mode == 'PAINT_VERTEX':
865         layout.prop(brush, "use_alpha")
866         if brush.vertex_tool != 'SMEAR':
867             use_accumulate = True
868         use_frontface = True
869
870     # Weight Paint
871     elif mode == 'PAINT_WEIGHT':
872         if brush.weight_tool != 'SMEAR':
873             use_accumulate = True
874         use_frontface = True
875
876     # Draw shared settings.
877     if use_accumulate:
878         layout.prop(brush, "use_accumulate")
879
880     if use_frontface:
881         layout.prop(brush, "use_frontface", text="Front Faces Only")
882
883
884 def draw_color_settings(context, layout, brush, color_type=False):
885     """Draw color wheel and gradient settings."""
886     ups = context.scene.tool_settings.unified_paint_settings
887
888     if color_type:
889         row = layout.row()
890         row.use_property_split = False
891         row.prop(brush, "color_type", expand=True)
892
893     # Color wheel
894     if brush.color_type == 'COLOR':
895         UnifiedPaintPanel.prop_unified_color_picker(layout, context, brush, "color", value_slider=True)
896
897         row = layout.row(align=True)
898         UnifiedPaintPanel.prop_unified_color(row, context, brush, "color", text="")
899         UnifiedPaintPanel.prop_unified_color(row, context, brush, "secondary_color", text="")
900         row.separator()
901         row.operator("paint.brush_colors_flip", icon='FILE_REFRESH', text="", emboss=False)
902         row.prop(ups, "use_unified_color", text="", icon='BRUSHES_ALL')
903     # Gradient
904     elif brush.color_type == 'GRADIENT':
905         layout.template_color_ramp(brush, "gradient", expand=True)
906
907         layout.use_property_split = True
908
909         col = layout.column()
910
911         if brush.image_tool == 'DRAW':
912             UnifiedPaintPanel.prop_unified(
913                 col,
914                 context,
915                 brush,
916                 "secondary_color",
917                 unified_name="use_unified_color",
918                 text="Background Color",
919                 header=True,
920             )
921
922             col.prop(brush, "gradient_stroke_mode", text="Gradient Mapping")
923             if brush.gradient_stroke_mode in {'SPACING_REPEAT', 'SPACING_CLAMP'}:
924                 col.prop(brush, "grad_spacing")
925
926
927 # Used in both the View3D toolbar and texture properties
928 def brush_texture_settings(layout, brush, sculpt):
929     tex_slot = brush.texture_slot
930
931     layout.use_property_split = True
932     layout.use_property_decorate = False
933
934     # map_mode
935     if sculpt:
936         layout.prop(tex_slot, "map_mode", text="Mapping")
937     else:
938         layout.prop(tex_slot, "tex_paint_map_mode", text="Mapping")
939
940     layout.separator()
941
942     if tex_slot.map_mode == 'STENCIL':
943         if brush.texture and brush.texture.type == 'IMAGE':
944             layout.operator("brush.stencil_fit_image_aspect")
945         layout.operator("brush.stencil_reset_transform")
946
947     # angle and texture_angle_source
948     if tex_slot.has_texture_angle:
949         col = layout.column()
950         col.prop(tex_slot, "angle", text="Angle")
951         if tex_slot.has_texture_angle_source:
952             col.prop(tex_slot, "use_rake", text="Rake")
953
954             if brush.brush_capabilities.has_random_texture_angle and tex_slot.has_random_texture_angle:
955                 if sculpt:
956                     if brush.sculpt_capabilities.has_random_texture_angle:
957                         col.prop(tex_slot, "use_random", text="Random")
958                         if tex_slot.use_random:
959                             col.prop(tex_slot, "random_angle", text="Random Angle")
960                 else:
961                     col.prop(tex_slot, "use_random", text="Random")
962                     if tex_slot.use_random:
963                         col.prop(tex_slot, "random_angle", text="Random Angle")
964
965     # scale and offset
966     layout.prop(tex_slot, "offset")
967     layout.prop(tex_slot, "scale")
968
969     if sculpt:
970         # texture_sample_bias
971         layout.prop(brush, "texture_sample_bias", slider=True, text="Sample Bias")
972
973
974 def brush_mask_texture_settings(layout, brush):
975     mask_tex_slot = brush.mask_texture_slot
976
977     layout.use_property_split = True
978     layout.use_property_decorate = False
979
980     # map_mode
981     layout.row().prop(mask_tex_slot, "mask_map_mode", text="Mask Mapping")
982
983     if mask_tex_slot.map_mode == 'STENCIL':
984         if brush.mask_texture and brush.mask_texture.type == 'IMAGE':
985             layout.operator("brush.stencil_fit_image_aspect").mask = True
986         layout.operator("brush.stencil_reset_transform").mask = True
987
988     col = layout.column()
989     col.prop(brush, "use_pressure_masking", text="Pressure Masking")
990     # angle and texture_angle_source
991     if mask_tex_slot.has_texture_angle:
992         col = layout.column()
993         col.prop(mask_tex_slot, "angle", text="Angle")
994         if mask_tex_slot.has_texture_angle_source:
995             col.prop(mask_tex_slot, "use_rake", text="Rake")
996
997             if brush.brush_capabilities.has_random_texture_angle and mask_tex_slot.has_random_texture_angle:
998                 col.prop(mask_tex_slot, "use_random", text="Random")
999                 if mask_tex_slot.use_random:
1000                     col.prop(mask_tex_slot, "random_angle", text="Random Angle")
1001
1002     # scale and offset
1003     col.prop(mask_tex_slot, "offset")
1004     col.prop(mask_tex_slot, "scale")
1005
1006
1007 def brush_basic_texpaint_settings(layout, context, brush, *, compact=False):
1008     """Draw Tool Settings header for Vertex Paint and 2D and 3D Texture Paint modes."""
1009     capabilities = brush.image_paint_capabilities
1010
1011     if capabilities.has_color:
1012         UnifiedPaintPanel.prop_unified_color(layout, context, brush, "color", text="")
1013         layout.prop(brush, "blend", text="" if compact else "Blend")
1014
1015     UnifiedPaintPanel.prop_unified(
1016         layout,
1017         context,
1018         brush,
1019         "size",
1020         pressure_name="use_pressure_size",
1021         unified_name="use_unified_size",
1022         slider=True,
1023         text="Radius",
1024         header=True
1025     )
1026     UnifiedPaintPanel.prop_unified(
1027         layout,
1028         context,
1029         brush,
1030         "strength",
1031         pressure_name="use_pressure_strength",
1032         unified_name="use_unified_strength",
1033         header=True
1034     )
1035
1036
1037 def brush_basic_gpencil_paint_settings(layout, context, brush, *, compact=False):
1038     tool_settings = context.tool_settings
1039     settings = tool_settings.gpencil_paint
1040     gp_settings = brush.gpencil_settings
1041     tool = context.workspace.tools.from_space_view3d_mode(context.mode, create=False)
1042     if gp_settings is None:
1043         return
1044
1045     # Brush details
1046     if brush.gpencil_tool == 'ERASE':
1047         row = layout.row(align=True)
1048         row.prop(brush, "size", text="Radius")
1049         row.prop(gp_settings, "use_pressure", text="", icon='STYLUS_PRESSURE')
1050         row.prop(gp_settings, "use_occlude_eraser", text="", icon='XRAY')
1051
1052         row = layout.row(align=True)
1053         row.prop(gp_settings, "eraser_mode", expand=True)
1054         if gp_settings.eraser_mode == 'SOFT':
1055             row = layout.row(align=True)
1056             row.prop(gp_settings, "pen_strength", slider=True)
1057             row.prop(gp_settings, "use_strength_pressure", text="", icon='STYLUS_PRESSURE')
1058             row = layout.row(align=True)
1059             row.prop(gp_settings, "eraser_strength_factor")
1060             row = layout.row(align=True)
1061             row.prop(gp_settings, "eraser_thickness_factor")
1062
1063         row = layout.row(align=True)
1064         row.prop(settings, "show_brush", text="Display Cursor")
1065
1066     # FIXME: tools must use their own UI drawing!
1067     elif brush.gpencil_tool == 'FILL':
1068         row = layout.row(align=True)
1069         row.prop(gp_settings, "fill_leak", text="Leak Size")
1070         row = layout.row(align=True)
1071         row.prop(brush, "size", text="Thickness")
1072         row = layout.row(align=True)
1073         row.prop(gp_settings, "fill_simplify_level", text="Simplify")
1074
1075     else:  # brush.gpencil_tool == 'DRAW/TINT':
1076         row = layout.row(align=True)
1077         row.prop(brush, "size", text="Radius")
1078         row.prop(gp_settings, "use_pressure", text="", icon='STYLUS_PRESSURE')
1079
1080         if gp_settings.use_pressure and context.area.type == 'PROPERTIES':
1081             col = layout.column()
1082             col.template_curve_mapping(gp_settings, "curve_sensitivity", brush=True,
1083                                       use_negative_slope=True)
1084
1085         row = layout.row(align=True)
1086         row.prop(gp_settings, "pen_strength", slider=True)
1087         row.prop(gp_settings, "use_strength_pressure", text="", icon='STYLUS_PRESSURE')
1088
1089         if gp_settings.use_strength_pressure and context.area.type == 'PROPERTIES':
1090             col = layout.column()
1091             col.template_curve_mapping(gp_settings, "curve_strength", brush=True,
1092                                         use_negative_slope=True)
1093
1094         if brush.gpencil_tool == 'TINT':
1095             row = layout.row(align=True)
1096             row.prop(gp_settings, "vertex_mode", text="Mode")
1097
1098     # FIXME: tools must use their own UI drawing!
1099     if tool.idname in {
1100             "builtin.arc",
1101             "builtin.curve",
1102             "builtin.line",
1103             "builtin.box",
1104             "builtin.circle",
1105             "builtin.polyline"
1106     }:
1107         settings = context.tool_settings.gpencil_sculpt
1108         if compact:
1109             row = layout.row(align=True)
1110             row.prop(settings, "use_thickness_curve", text="", icon='SPHERECURVE')
1111             sub = row.row(align=True)
1112             sub.active = settings.use_thickness_curve
1113             sub.popover(
1114                 panel="TOPBAR_PT_gpencil_primitive",
1115                 text="Thickness Profile",
1116             )
1117         else:
1118             row = layout.row(align=True)
1119             row.prop(settings, "use_thickness_curve", text="Use Thickness Profile")
1120             sub = row.row(align=True)
1121             if settings.use_thickness_curve:
1122                 # Curve
1123                 layout.template_curve_mapping(settings, "thickness_primitive_curve", brush=True)
1124
1125
1126 def brush_basic_gpencil_sculpt_settings(layout, context, brush, *, compact=False):
1127     gp_settings = brush.gpencil_settings
1128     tool = brush.gpencil_sculpt_tool
1129
1130     row = layout.row(align=True)
1131     row.prop(brush, "size", slider=True)
1132     sub = row.row(align=True)
1133     sub.enabled = tool not in {'GRAB', 'CLONE'}
1134     sub.prop(gp_settings, "use_pressure", text="")
1135
1136     row = layout.row(align=True)
1137     row.prop(brush, "strength", slider=True)
1138     row.prop(brush, "use_pressure_strength", text="")
1139
1140     if compact:
1141         if tool in {'THICKNESS', 'STRENGTH', 'PINCH', 'TWIST'}:
1142             row.separator()
1143             row.prop(gp_settings, "direction", expand=True, text="")
1144     else:
1145         use_property_split_prev = layout.use_property_split
1146         layout.use_property_split = False
1147         if tool in {'THICKNESS', 'STRENGTH'}:
1148             layout.row().prop(gp_settings, "direction", expand=True)
1149         elif tool == 'PINCH':
1150             row = layout.row(align=True)
1151             row.prop_enum(gp_settings, "direction", value='ADD', text="Pinch")
1152             row.prop_enum(gp_settings, "direction", value='SUBTRACT', text="Inflate")
1153         elif tool == 'TWIST':
1154             row = layout.row(align=True)
1155             row.prop_enum(gp_settings, "direction", value='ADD', text="CCW")
1156             row.prop_enum(gp_settings, "direction", value='SUBTRACT', text="CW")
1157         layout.use_property_split = use_property_split_prev
1158
1159
1160 def brush_basic_gpencil_weight_settings(layout, _context, brush, *, compact=False):
1161     gp_settings = brush.gpencil_settings
1162     layout.prop(brush, "size", slider=True)
1163
1164     row = layout.row(align=True)
1165     row.prop(brush, "strength", slider=True)
1166     row.prop(brush, "use_pressure_strength", text="")
1167
1168     layout.prop(brush, "weight", slider=True)
1169
1170
1171 def brush_basic_gpencil_vertex_settings(layout, _context, brush, *, compact=False):
1172     gp_settings = brush.gpencil_settings
1173
1174     # Brush details
1175     row = layout.row(align=True)
1176     row.prop(brush, "size", text="Radius")
1177     row.prop(gp_settings, "use_pressure", text="", icon='STYLUS_PRESSURE')
1178
1179     if brush.gpencil_vertex_tool in {'DRAW', 'BLUR', 'SMEAR'}:
1180         row = layout.row(align=True)
1181         row.prop(gp_settings, "pen_strength", slider=True)
1182         row.prop(gp_settings, "use_strength_pressure", text="", icon='STYLUS_PRESSURE')
1183
1184     if brush.gpencil_vertex_tool in {'DRAW', 'REPLACE'}:
1185         row = layout.row(align=True)
1186         row.prop(gp_settings, "vertex_mode", text="Mode")
1187
1188
1189 classes = (
1190     VIEW3D_MT_tools_projectpaint_clone,
1191 )
1192
1193 if __name__ == "__main__":  # only for live edit.
1194     from bpy.utils import register_class
1195     for cls in classes:
1196         register_class(cls)