1 # SPDX-License-Identifier: GPL-2.0-or-later
2 # Copyright 2011 Dany Lebel (Axon_D)
5 "name": "Paint Palettes",
6 "author": "Dany Lebel (Axon D)",
9 "location": "Image Editor and 3D View > Any Paint mode > Color Palette or Weight Palette panel",
10 "description": "Palettes for color and weight paint modes",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/paint/paint_palettes.html",
17 This add-on brings palettes to the paint modes.
19 * Color Palette for Image Painting, Texture Paint and Vertex Paint modes.
20 * Weight Palette for the Weight Paint mode.
22 Set a number of colors (or weights according to the mode) and then associate it
23 with the brush by using the button under the color.
27 from bpy
.types
import (
33 from bpy
.props
import (
45 pp
= bpy
.context
.scene
.palette_props
46 current_color
= pp
.colors
[pp
.current_color_index
].color
47 pp
.color_name
= pp
.colors
[pp
.current_color_index
].name
48 brush
= current_brush()
49 brush
.color
= current_color
50 pp
.index
= pp
.current_color_index
54 pp
= bpy
.context
.scene
.palette_props
55 current_color
= pp
.colors
[pp
.current_color_index
]
56 brush
= current_brush()
57 current_color
.color
= brush
.color
63 if context
.area
.type == 'VIEW_3D' and context
.vertex_paint_object
:
64 brush
= context
.tool_settings
.vertex_paint
.brush
65 elif context
.area
.type == 'VIEW_3D' and context
.image_paint_object
:
66 brush
= context
.tool_settings
.image_paint
.brush
67 elif context
.area
.type == 'IMAGE_EDITOR' and context
.space_data
.mode
== 'PAINT':
68 brush
= context
.tool_settings
.image_paint
.brush
74 def update_weight_value():
75 pp
= bpy
.context
.scene
.palette_props
76 tt
= bpy
.context
.tool_settings
77 tt
.unified_paint_settings
.weight
= pp
.weight_value
81 def check_path_return():
82 from os
.path
import normpath
83 preset_path
= bpy
.path
.abspath(bpy
.context
.scene
.palette_props
.presets_folder
)
84 paths
= normpath(preset_path
)
86 return paths
if paths
else ""
89 class PALETTE_MT_menu(Menu
):
92 preset_operator
= "palette.load_gimp_palette"
94 def path_menu(self
, searchpaths
, operator
, props_default
={}):
96 # hard coded to set the operators 'filepath' to the filename.
102 if bpy
.data
.filepath
== "":
103 layout
.label(text
="*Please save the .blend file first*")
106 if not searchpaths
[0]:
107 layout
.label(text
="* Missing Paths *")
112 for directory
in searchpaths
:
113 files
.extend([(f
, os
.path
.join(directory
, f
)) for f
in os
.listdir(directory
)])
117 for f
, filepath
in files
:
119 if f
.startswith("."):
121 # do not load everything from the given folder, only .gpl files
125 preset_name
= bpy
.path
.display_name(f
)
126 props
= layout
.operator(operator
, text
=preset_name
)
128 for attr
, value
in props_default
.items():
129 setattr(props
, attr
, value
)
131 props
.filepath
= filepath
132 if operator
== "palette.load_gimp_palette":
133 props
.menu_idname
= self
.bl_idname
135 def draw_preset(self
, context
):
136 paths
= check_path_return()
137 self
.path_menu([paths
], self
.preset_operator
)
142 class PALETTE_OT_load_gimp_palette(Operator
):
143 """Execute a preset"""
144 bl_idname
= "palette.load_gimp_palette"
145 bl_label
= "Load a Gimp palette"
147 filepath
: StringProperty(
149 description
="Path of the .gpl file to load",
152 menu_idname
: StringProperty(
154 description
="ID name of the menu this was called from",
158 def execute(self
, context
):
159 from os
.path
import basename
161 filepath
= self
.filepath
163 palette_props
= bpy
.context
.scene
.palette_props
164 palette_props
.current_color_index
= 0
166 # change the menu title to the most recently chosen option
167 preset_class
= getattr(bpy
.types
, self
.menu_idname
)
168 preset_class
.bl_label
= bpy
.path
.display_name(basename(filepath
))
170 palette_props
.columns
= 0
171 error_palette
= False # errors found
172 error_import
= [] # collect exception messages
173 start_color_index
= 0 # store the starting line for color definitions
175 if filepath
[-4:] != ".gpl":
178 gpl
= open(filepath
, "r")
179 lines
= gpl
.readlines()
180 palette_props
.notes
= ''
182 for index_0
, line
in enumerate(lines
):
183 if not line
or (line
[:12] == "GIMP Palette"):
185 elif line
[:5] == "Name:":
186 palette_props
.palette_name
= line
[5:]
187 elif line
[:8] == "Columns:":
188 palette_props
.columns
= int(line
[8:])
190 palette_props
.notes
+= line
191 elif line
[0] == "\n":
195 start_color_index
= index_0
199 for i
, ln
in enumerate(lines
[start_color_index
:]):
201 palette_props
.colors
[i
]
203 palette_props
.colors
.add()
205 # get line - find keywords with re.split, remove the empty ones with filter
206 get_line
= list(filter(None, re
.split(r
'\t+|\s+', ln
.rstrip('\n'))))
207 extract_colors
= get_line
[:3]
208 get_color_name
= [str(name
) for name
in get_line
[3:]]
209 color
= [float(rgb
) / 255 for rgb
in extract_colors
]
210 palette_props
.colors
[i
].color
= color
211 palette_props
.colors
[i
].name
= " ".join(get_color_name
) or "Color " + str(i
)
212 except Exception as e
:
214 error_import
.append(".gpl file line: {}, error: {}".format(i
+ 1 + start_color_index
, e
))
218 while palette_props
.colors
.__len
__() > exceeding
:
219 palette_props
.colors
.remove(exceeding
)
226 message
= "Loaded palette from file: {}".format(filepath
)
229 message
= "Not supported palette format for file: {}".format(filepath
)
231 message
= "Some of the .gpl palette data can not be parsed. See Console for more info"
232 print("\n[Paint Palette]\nOperator: palette.load_gimp_palette\nErrors: %s\n" %
233 ('\n'.join(error_import
)))
235 self
.report({'INFO'}, message
)
240 class WriteGimpPalette():
241 """Base preset class, only for subclassing
242 subclasses must define
245 bl_options
= {'REGISTER'} # only because invoke_props_popup requires
247 name
: StringProperty(
249 description
="Name of the preset, used to make the path name",
251 options
={'SKIP_SAVE'},
254 remove_active
: BoolProperty(
260 def as_filename(name
): # could reuse for other presets
261 for char
in " !@#$%^&*(){}:\";'[]<>,.\\/?":
262 name
= name
.replace(char
, '_')
263 return name
.lower().strip()
265 def execute(self
, context
):
267 pp
= bpy
.context
.scene
.palette_props
269 if hasattr(self
, "pre_cb"):
272 preset_menu_class
= getattr(bpy
.types
, self
.preset_menu
)
273 target_path
= check_path_return()
276 self
.report({'WARNING'}, "Failed to create presets path")
279 if not os
.path
.exists(target_path
):
280 self
.report({'WARNING'},
281 "Failure to open the saved Palettes Folder. Check if the path exists")
284 if not self
.remove_active
:
286 self
.report({'INFO'},
287 "No name is given for the preset entry. Operation Cancelled")
290 filename
= self
.as_filename(self
.name
)
291 filepath
= os
.path
.join(target_path
, filename
) + ".gpl"
292 file_preset
= open(filepath
, 'wb')
293 gpl
= "GIMP Palette\n"
294 gpl
+= "Name: %s\n" % filename
295 gpl
+= "Columns: %d\n" % pp
.columns
297 if pp
.colors
.items():
298 for i
, color
in enumerate(pp
.colors
):
299 gpl
+= "%3d%4d%4d %s" % (color
.color
.r
* 255, color
.color
.g
* 255,
300 color
.color
.b
* 255, color
.name
+ '\n')
301 file_preset
.write(bytes(gpl
, 'UTF-8'))
305 pp
.palette_name
= filename
306 preset_menu_class
.bl_label
= bpy
.path
.display_name(filename
)
308 self
.report({'INFO'}, "Created Palette: {}".format(filepath
))
311 preset_active
= preset_menu_class
.bl_label
312 filename
= self
.as_filename(preset_active
)
314 filepath
= os
.path
.join(target_path
, filename
) + ".gpl"
316 if not filepath
or not os
.path
.exists(filepath
):
317 self
.report({'WARNING'}, "Preset could not be found. Operation Cancelled")
318 self
.reset_preset_name(preset_menu_class
, pp
)
321 if hasattr(self
, "remove"):
322 self
.remove(context
, filepath
)
326 self
.report({'INFO'}, "Deleted palette: {}".format(filepath
))
329 traceback
.print_exc()
331 self
.reset_preset_name(preset_menu_class
, pp
)
333 if hasattr(self
, "post_cb"):
334 self
.post_cb(context
)
339 def reset_preset_name(presets
, props
):
341 presets
.bl_label
= "Presets"
342 props
.palette_name
= ""
344 def check(self
, context
):
345 self
.name
= self
.as_filename(self
.name
)
347 def invoke(self
, context
, event
):
348 if not self
.remove_active
:
349 wm
= context
.window_manager
350 return wm
.invoke_props_dialog(self
)
352 return self
.execute(context
)
355 class PALETTE_OT_preset_add(WriteGimpPalette
, Operator
):
356 bl_idname
= "palette.preset_add"
357 bl_label
= "Add Palette Preset"
358 preset_menu
= "PALETTE_MT_menu"
359 bl_description
= "Add a Palette Preset"
363 preset_subdir
= "palette"
366 class PALETTE_OT_add_color(Operator
):
367 bl_idname
= "palette_props.add_color"
369 bl_description
= "Add a Color to the Palette"
371 def execute(self
, context
):
372 pp
= bpy
.context
.scene
.palette_props
374 if pp
.colors
.items():
375 new_index
= pp
.current_color_index
+ 1
378 last
= pp
.colors
.__len
__() - 1
380 pp
.colors
.move(last
, new_index
)
381 pp
.current_color_index
= new_index
388 class PALETTE_OT_remove_color(Operator
):
389 bl_idname
= "palette_props.remove_color"
391 bl_description
= "Remove Selected Color"
394 def poll(cls
, context
):
395 pp
= bpy
.context
.scene
.palette_props
396 return bool(pp
.colors
.items())
398 def execute(self
, context
):
399 pp
= context
.scene
.palette_props
400 i
= pp
.current_color_index
403 if pp
.current_color_index
>= pp
.colors
.__len
__():
404 pp
.index
= pp
.current_color_index
= pp
.colors
.__len
__() - 1
409 class PALETTE_OT_sample_tool_color(Operator
):
410 bl_idname
= "palette_props.sample_tool_color"
412 bl_description
= "Sample Tool Color"
414 def execute(self
, context
):
415 pp
= context
.scene
.palette_props
416 brush
= current_brush()
417 pp
.colors
[pp
.current_color_index
].color
= brush
.color
422 class IMAGE_OT_select_color(Operator
):
423 bl_idname
= "paint.select_color"
425 bl_description
= "Select this color"
426 bl_options
= {'UNDO'}
428 color_index
: IntProperty()
430 def invoke(self
, context
, event
):
431 palette_props
= context
.scene
.palette_props
432 palette_props
.current_color_index
= self
.color_index
439 def color_palette_draw(self
, context
):
440 palette_props
= context
.scene
.palette_props
444 row
= layout
.row(align
=True)
445 row
.menu("PALETTE_MT_menu", text
=PALETTE_MT_menu
.bl_label
)
446 row
.operator("palette.preset_add", text
="", icon
='ADD').remove_active
= False
447 row
.operator("palette.preset_add", text
="", icon
='REMOVE').remove_active
= True
449 col
= layout
.column(align
=True)
450 row
= col
.row(align
=True)
451 row
.operator("palette_props.add_color", icon
='ADD')
452 row
.prop(palette_props
, "index")
453 row
.operator("palette_props.remove_color", icon
="PANEL_CLOSE")
455 row
= col
.row(align
=True)
456 row
.prop(palette_props
, "columns")
457 if palette_props
.colors
.items():
459 row
= layout
.row(align
=True)
460 row
.prop(palette_props
, "color_name")
461 row
.operator("palette_props.sample_tool_color", icon
="COLOR")
463 laycol
= layout
.column(align
=False)
465 if palette_props
.columns
:
466 columns
= palette_props
.columns
470 for i
, color
in enumerate(palette_props
.colors
):
472 row1
= laycol
.row(align
=True)
474 row2
= laycol
.row(align
=True)
477 active
= True if i
== palette_props
.current_color_index
else False
478 icons
= "LAYER_ACTIVE" if active
else "LAYER_USED"
479 row1
.prop(palette_props
.colors
[i
], "color", event
=True, toggle
=True)
480 row2
.operator("paint.select_color", text
=" ",
481 emboss
=active
, icon
=icons
).color_index
= i
485 row
.prop(palette_props
, "presets_folder", text
="")
488 class BrushButtonsPanel():
489 bl_space_type
= 'IMAGE_EDITOR'
490 bl_region_type
= 'UI'
493 def poll(cls
, context
):
494 sima
= context
.space_data
495 toolsettings
= context
.tool_settings
.image_paint
496 return sima
.show_paint
and toolsettings
.brush
500 bl_space_type
= 'VIEW_3D'
501 bl_region_type
= 'UI'
502 bl_category
= 'Paint'
505 def paint_settings(context
):
506 ts
= context
.tool_settings
508 if context
.vertex_paint_object
:
509 return ts
.vertex_paint
510 elif context
.weight_paint_object
:
511 return ts
.weight_paint
512 elif context
.texture_paint_object
:
513 return ts
.image_paint
517 class IMAGE_PT_color_palette(BrushButtonsPanel
, Panel
):
518 bl_label
= "Color Palette"
519 bl_options
= {'DEFAULT_CLOSED'}
521 def draw(self
, context
):
522 color_palette_draw(self
, context
)
525 class VIEW3D_PT_color_palette(PaintPanel
, Panel
):
526 bl_label
= "Color Palette"
527 bl_options
= {'DEFAULT_CLOSED'}
530 def poll(cls
, context
):
531 return (context
.image_paint_object
or context
.vertex_paint_object
)
533 def draw(self
, context
):
534 color_palette_draw(self
, context
)
537 class VIEW3D_OT_select_weight(Operator
):
538 bl_idname
= "paint.select_weight"
540 bl_description
= "Select this weight value slot"
541 bl_options
= {'UNDO'}
543 weight_index
: IntProperty()
545 def current_weight(self
):
546 pp
= bpy
.context
.scene
.palette_props
547 if self
.weight_index
== 0:
549 elif self
.weight_index
== 1:
551 elif self
.weight_index
== 2:
553 elif self
.weight_index
== 3:
555 elif self
.weight_index
== 4:
557 elif self
.weight_index
== 5:
559 elif self
.weight_index
== 6:
561 elif self
.weight_index
== 7:
563 elif self
.weight_index
== 8:
565 elif self
.weight_index
== 9:
567 elif self
.weight_index
== 10:
568 weight
= pp
.weight_10
571 def invoke(self
, context
, event
):
572 palette_props
= context
.scene
.palette_props
573 palette_props
.current_weight_index
= self
.weight_index
575 if self
.weight_index
== 0:
576 weight
= palette_props
.weight_0
577 elif self
.weight_index
== 1:
578 weight
= palette_props
.weight_1
579 elif self
.weight_index
== 2:
580 weight
= palette_props
.weight_2
581 elif self
.weight_index
== 3:
582 weight
= palette_props
.weight_3
583 elif self
.weight_index
== 4:
584 weight
= palette_props
.weight_4
585 elif self
.weight_index
== 5:
586 weight
= palette_props
.weight_5
587 elif self
.weight_index
== 6:
588 weight
= palette_props
.weight_6
589 elif self
.weight_index
== 7:
590 weight
= palette_props
.weight_7
591 elif self
.weight_index
== 8:
592 weight
= palette_props
.weight_8
593 elif self
.weight_index
== 9:
594 weight
= palette_props
.weight_9
595 elif self
.weight_index
== 10:
596 weight
= palette_props
.weight_10
597 palette_props
.weight
= weight
602 class VIEW3D_OT_reset_weight_palette(Operator
):
603 bl_idname
= "paint.reset_weight_palette"
605 bl_description
= "Reset the active Weight slot to it's default value"
607 def execute(self
, context
):
609 palette_props
= context
.scene
.palette_props
611 0: 0.0, 1: 0.1, 2: 0.25,
612 3: 0.333, 4: 0.4, 5: 0.5,
613 6: 0.6, 7: 0.6666, 8: 0.75,
616 current_idx
= palette_props
.current_weight_index
617 palette_props
.weight
= dict_defs
[current_idx
]
619 var_name
= "weight_" + str(current_idx
)
620 var_to_change
= getattr(palette_props
, var_name
, None)
622 var_to_change
= dict_defs
[current_idx
]
626 except Exception as e
:
627 self
.report({'WARNING'},
628 "Reset Weight palette could not be completed (See Console for more info)")
629 print("\n[Paint Palette]\nOperator: paint.reset_weight_palette\nError: %s\n" % e
)
634 class VIEW3D_PT_weight_palette(PaintPanel
, Panel
):
635 bl_label
= "Weight Palette"
636 bl_options
= {'DEFAULT_CLOSED'}
639 def poll(cls
, context
):
640 return context
.weight_paint_object
642 def draw(self
, context
):
643 palette_props
= context
.scene
.palette_props
647 row
.prop(palette_props
, "weight", slider
=True)
650 selected_weight
= palette_props
.current_weight_index
651 for props
in range(0, 11):
652 embossed
= False if props
== selected_weight
else True
653 prop_name
= "weight_" + str(props
)
654 prop_value
= getattr(palette_props
, prop_name
, "")
656 row
= box
.row(align
=True)
657 elif (props
+ 2) % 3 == 0:
658 col
= box
.column(align
=True)
659 row
= col
.row(align
=True)
662 row
= box
.row(align
=True)
663 row
= row
.row(align
=True)
665 row
.operator("paint.select_weight", text
="%.2f" % prop_value
,
666 emboss
=embossed
).weight_index
= props
669 row
.operator("paint.reset_weight_palette", text
="Reset")
672 class PALETTE_Colors(PropertyGroup
):
673 """Class for colors CollectionProperty"""
674 color
: FloatVectorProperty(
677 default
=(0.8, 0.8, 0.8),
680 subtype
='COLOR_GAMMA',
685 class PALETTE_Props(PropertyGroup
):
687 def update_color_name(self
, context
):
688 pp
= bpy
.context
.scene
.palette_props
689 pp
.colors
[pp
.current_color_index
].name
= pp
.color_name
692 def move_color(self
, context
):
693 pp
= bpy
.context
.scene
.palette_props
694 if pp
.colors
.items() and pp
.current_color_index
!= pp
.index
:
695 if pp
.index
>= pp
.colors
.__len
__():
696 pp
.index
= pp
.colors
.__len
__() - 1
698 pp
.colors
.move(pp
.current_color_index
, pp
.index
)
699 pp
.current_color_index
= pp
.index
702 def update_weight(self
, context
):
703 pp
= context
.scene
.palette_props
705 if pp
.current_weight_index
== 0:
707 elif pp
.current_weight_index
== 1:
709 elif pp
.current_weight_index
== 2:
711 elif pp
.current_weight_index
== 3:
713 elif pp
.current_weight_index
== 4:
715 elif pp
.current_weight_index
== 5:
717 elif pp
.current_weight_index
== 6:
719 elif pp
.current_weight_index
== 7:
721 elif pp
.current_weight_index
== 8:
723 elif pp
.current_weight_index
== 9:
725 elif pp
.current_weight_index
== 10:
726 pp
.weight_10
= weight
727 bpy
.context
.tool_settings
.unified_paint_settings
.weight
= weight
730 palette_name
: StringProperty(
735 color_name
: StringProperty(
737 description
="Color Name",
739 update
=update_color_name
741 columns
: IntProperty(
743 description
="Number of Columns",
749 description
="Move Selected Color",
753 notes
: StringProperty(
754 name
="Palette Notes",
757 current_color_index
: IntProperty(
758 name
="Current Color Index",
763 current_weight_index
: IntProperty(
764 name
="Current Color Index",
769 presets_folder
: StringProperty(name
="",
770 description
="Palettes Folder",
774 colors
: CollectionProperty(
777 weight
: FloatProperty(
779 description
="Modify the active Weight preset slot value",
785 weight_0
: FloatProperty(
790 weight_1
: FloatProperty(
795 weight_2
: FloatProperty(
800 weight_3
: FloatProperty(
805 weight_4
: FloatProperty(
810 weight_5
: FloatProperty(
815 weight_6
: FloatProperty(
820 weight_7
: FloatProperty(
825 weight_8
: FloatProperty(
830 weight_9
: FloatProperty(
835 weight_10
: FloatProperty(
844 PALETTE_OT_load_gimp_palette
,
845 PALETTE_OT_preset_add
,
846 PALETTE_OT_add_color
,
847 PALETTE_OT_remove_color
,
848 PALETTE_OT_sample_tool_color
,
849 IMAGE_OT_select_color
,
850 IMAGE_PT_color_palette
,
851 VIEW3D_PT_color_palette
,
852 VIEW3D_OT_select_weight
,
853 VIEW3D_OT_reset_weight_palette
,
854 VIEW3D_PT_weight_palette
,
862 bpy
.utils
.register_class(cls
)
864 bpy
.types
.Scene
.palette_props
= PointerProperty(
866 name
="Palette Props",
872 for cls
in reversed(classes
):
873 bpy
.utils
.unregister_class(cls
)
875 del bpy
.types
.Scene
.palette_props
878 if __name__
== "__main__":