1 # paint_palette.py (c) 2011 Dany Lebel (Axon_D)
3 # ##### BEGIN GPL LICENSE BLOCK #####
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 # ##### END GPL LICENSE BLOCK #####
23 "name": "Paint Palettes",
24 "author": "Dany Lebel (Axon D)",
26 "blender": (2, 80, 0),
27 "location": "Image Editor and 3D View > Any Paint mode > Color Palette or Weight Palette panel",
28 "description": "Palettes for color and weight paint modes",
30 "wiki_url": "https://docs.blender.org/manual/en/dev/addons/"
31 "paint_palettes.html",
36 This add-on brings palettes to the paint modes.
38 * Color Palette for Image Painting, Texture Paint and Vertex Paint modes.
39 * Weight Palette for the Weight Paint mode.
41 Set a number of colors (or weights according to the mode) and then associate it
42 with the brush by using the button under the color.
46 from bpy
.types
import (
52 from bpy
.props
import (
64 pp
= bpy
.context
.scene
.palette_props
65 current_color
= pp
.colors
[pp
.current_color_index
].color
66 pp
.color_name
= pp
.colors
[pp
.current_color_index
].name
67 brush
= current_brush()
68 brush
.color
= current_color
69 pp
.index
= pp
.current_color_index
73 pp
= bpy
.context
.scene
.palette_props
74 current_color
= pp
.colors
[pp
.current_color_index
]
75 brush
= current_brush()
76 current_color
.color
= brush
.color
82 if context
.area
.type == 'VIEW_3D' and context
.vertex_paint_object
:
83 brush
= context
.tool_settings
.vertex_paint
.brush
84 elif context
.area
.type == 'VIEW_3D' and context
.image_paint_object
:
85 brush
= context
.tool_settings
.image_paint
.brush
86 elif context
.area
.type == 'IMAGE_EDITOR' and context
.space_data
.mode
== 'PAINT':
87 brush
= context
.tool_settings
.image_paint
.brush
93 def update_weight_value():
94 pp
= bpy
.context
.scene
.palette_props
95 tt
= bpy
.context
.tool_settings
96 tt
.unified_paint_settings
.weight
= pp
.weight_value
100 def check_path_return():
101 from os
.path
import normpath
102 preset_path
= bpy
.path
.abspath(bpy
.context
.scene
.palette_props
.presets_folder
)
103 paths
= normpath(preset_path
)
105 return paths
if paths
else ""
108 class PALETTE_MT_menu(Menu
):
111 preset_operator
= "palette.load_gimp_palette"
113 def path_menu(self
, searchpaths
, operator
, props_default
={}):
115 # hard coded to set the operators 'filepath' to the filename.
121 if bpy
.data
.filepath
== "":
122 layout
.label(text
="*Please save the .blend file first*")
125 if not searchpaths
[0]:
126 layout
.label(text
="* Missing Paths *")
131 for directory
in searchpaths
:
132 files
.extend([(f
, os
.path
.join(directory
, f
)) for f
in os
.listdir(directory
)])
136 for f
, filepath
in files
:
138 if f
.startswith("."):
140 # do not load everything from the given folder, only .gpl files
144 preset_name
= bpy
.path
.display_name(f
)
145 props
= layout
.operator(operator
, text
=preset_name
)
147 for attr
, value
in props_default
.items():
148 setattr(props
, attr
, value
)
150 props
.filepath
= filepath
151 if operator
== "palette.load_gimp_palette":
152 props
.menu_idname
= self
.bl_idname
154 def draw_preset(self
, context
):
155 paths
= check_path_return()
156 self
.path_menu([paths
], self
.preset_operator
)
161 class PALETTE_OT_load_gimp_palette(Operator
):
162 """Execute a preset"""
163 bl_idname
= "palette.load_gimp_palette"
164 bl_label
= "Load a Gimp palette"
166 filepath
: StringProperty(
168 description
="Path of the .gpl file to load",
171 menu_idname
: StringProperty(
173 description
="ID name of the menu this was called from",
177 def execute(self
, context
):
178 from os
.path
import basename
180 filepath
= self
.filepath
182 palette_props
= bpy
.context
.scene
.palette_props
183 palette_props
.current_color_index
= 0
185 # change the menu title to the most recently chosen option
186 preset_class
= getattr(bpy
.types
, self
.menu_idname
)
187 preset_class
.bl_label
= bpy
.path
.display_name(basename(filepath
))
189 palette_props
.columns
= 0
190 error_palette
= False # errors found
191 error_import
= [] # collect exception messages
192 start_color_index
= 0 # store the starting line for color definitions
194 if filepath
[-4:] != ".gpl":
197 gpl
= open(filepath
, "r")
198 lines
= gpl
.readlines()
199 palette_props
.notes
= ''
201 for index_0
, line
in enumerate(lines
):
202 if not line
or (line
[:12] == "GIMP Palette"):
204 elif line
[:5] == "Name:":
205 palette_props
.palette_name
= line
[5:]
206 elif line
[:8] == "Columns:":
207 palette_props
.columns
= int(line
[8:])
209 palette_props
.notes
+= line
210 elif line
[0] == "\n":
214 start_color_index
= index_0
218 for i
, ln
in enumerate(lines
[start_color_index
:]):
220 palette_props
.colors
[i
]
222 palette_props
.colors
.add()
224 # get line - find keywords with re.split, remove the empty ones with filter
225 get_line
= list(filter(None, re
.split(r
'\t+|\s+', ln
.rstrip('\n'))))
226 extract_colors
= get_line
[:3]
227 get_color_name
= [str(name
) for name
in get_line
[3:]]
228 color
= [float(rgb
) / 255 for rgb
in extract_colors
]
229 palette_props
.colors
[i
].color
= color
230 palette_props
.colors
[i
].name
= " ".join(get_color_name
) or "Color " + str(i
)
231 except Exception as e
:
233 error_import
.append(".gpl file line: {}, error: {}".format(i
+ 1 + start_color_index
, e
))
237 while palette_props
.colors
.__len
__() > exceeding
:
238 palette_props
.colors
.remove(exceeding
)
245 message
= "Loaded palette from file: {}".format(filepath
)
248 message
= "Not supported palette format for file: {}".format(filepath
)
250 message
= "Some of the .gpl palette data can not be parsed. See Console for more info"
251 print("\n[Paint Palette]\nOperator: palette.load_gimp_palette\nErrors: %s\n" %
252 ('\n'.join(error_import
)))
254 self
.report({'INFO'}, message
)
259 class WriteGimpPalette():
260 """Base preset class, only for subclassing
261 subclasses must define
264 bl_options
= {'REGISTER'} # only because invoke_props_popup requires
266 name
: StringProperty(
268 description
="Name of the preset, used to make the path name",
270 options
={'SKIP_SAVE'},
273 remove_active
: BoolProperty(
279 def as_filename(name
): # could reuse for other presets
280 for char
in " !@#$%^&*(){}:\";'[]<>,.\\/?":
281 name
= name
.replace(char
, '_')
282 return name
.lower().strip()
284 def execute(self
, context
):
286 pp
= bpy
.context
.scene
.palette_props
288 if hasattr(self
, "pre_cb"):
291 preset_menu_class
= getattr(bpy
.types
, self
.preset_menu
)
292 target_path
= check_path_return()
295 self
.report({'WARNING'}, "Failed to create presets path")
298 if not os
.path
.exists(target_path
):
299 self
.report({'WARNING'},
300 "Failure to open the saved Palettes Folder. Check if the path exists")
303 if not self
.remove_active
:
305 self
.report({'INFO'},
306 "No name is given for the preset entry. Operation Cancelled")
309 filename
= self
.as_filename(self
.name
)
310 filepath
= os
.path
.join(target_path
, filename
) + ".gpl"
311 file_preset
= open(filepath
, 'wb')
312 gpl
= "GIMP Palette\n"
313 gpl
+= "Name: %s\n" % filename
314 gpl
+= "Columns: %d\n" % pp
.columns
316 if pp
.colors
.items():
317 for i
, color
in enumerate(pp
.colors
):
318 gpl
+= "%3d%4d%4d %s" % (color
.color
.r
* 255, color
.color
.g
* 255,
319 color
.color
.b
* 255, color
.name
+ '\n')
320 file_preset
.write(bytes(gpl
, 'UTF-8'))
324 pp
.palette_name
= filename
325 preset_menu_class
.bl_label
= bpy
.path
.display_name(filename
)
327 self
.report({'INFO'}, "Created Palette: {}".format(filepath
))
330 preset_active
= preset_menu_class
.bl_label
331 filename
= self
.as_filename(preset_active
)
333 filepath
= os
.path
.join(target_path
, filename
) + ".gpl"
335 if not filepath
or not os
.path
.exists(filepath
):
336 self
.report({'WARNING'}, "Preset could not be found. Operation Cancelled")
337 self
.reset_preset_name(preset_menu_class
, pp
)
340 if hasattr(self
, "remove"):
341 self
.remove(context
, filepath
)
345 self
.report({'INFO'}, "Deleted palette: {}".format(filepath
))
348 traceback
.print_exc()
350 self
.reset_preset_name(preset_menu_class
, pp
)
352 if hasattr(self
, "post_cb"):
353 self
.post_cb(context
)
358 def reset_preset_name(presets
, props
):
360 presets
.bl_label
= "Presets"
361 props
.palette_name
= ""
363 def check(self
, context
):
364 self
.name
= self
.as_filename(self
.name
)
366 def invoke(self
, context
, event
):
367 if not self
.remove_active
:
368 wm
= context
.window_manager
369 return wm
.invoke_props_dialog(self
)
371 return self
.execute(context
)
374 class PALETTE_OT_preset_add(WriteGimpPalette
, Operator
):
375 bl_idname
= "palette.preset_add"
376 bl_label
= "Add Palette Preset"
377 preset_menu
= "PALETTE_MT_menu"
378 bl_description
= "Add a Palette Preset"
382 preset_subdir
= "palette"
385 class PALETTE_OT_add_color(Operator
):
386 bl_idname
= "palette_props.add_color"
388 bl_description
= "Add a Color to the Palette"
390 def execute(self
, context
):
391 pp
= bpy
.context
.scene
.palette_props
393 if pp
.colors
.items():
394 new_index
= pp
.current_color_index
+ 1
397 last
= pp
.colors
.__len
__() - 1
399 pp
.colors
.move(last
, new_index
)
400 pp
.current_color_index
= new_index
407 class PALETTE_OT_remove_color(Operator
):
408 bl_idname
= "palette_props.remove_color"
410 bl_description
= "Remove Selected Color"
413 def poll(cls
, context
):
414 pp
= bpy
.context
.scene
.palette_props
415 return bool(pp
.colors
.items())
417 def execute(self
, context
):
418 pp
= context
.scene
.palette_props
419 i
= pp
.current_color_index
422 if pp
.current_color_index
>= pp
.colors
.__len
__():
423 pp
.index
= pp
.current_color_index
= pp
.colors
.__len
__() - 1
428 class PALETTE_OT_sample_tool_color(Operator
):
429 bl_idname
= "palette_props.sample_tool_color"
431 bl_description
= "Sample Tool Color"
433 def execute(self
, context
):
434 pp
= context
.scene
.palette_props
435 brush
= current_brush()
436 pp
.colors
[pp
.current_color_index
].color
= brush
.color
441 class IMAGE_OT_select_color(Operator
):
442 bl_idname
= "paint.select_color"
444 bl_description
= "Select this color"
445 bl_options
= {'UNDO'}
447 color_index
: IntProperty()
449 def invoke(self
, context
, event
):
450 palette_props
= context
.scene
.palette_props
451 palette_props
.current_color_index
= self
.color_index
458 def color_palette_draw(self
, context
):
459 palette_props
= context
.scene
.palette_props
463 row
= layout
.row(align
=True)
464 row
.menu("PALETTE_MT_menu", text
=PALETTE_MT_menu
.bl_label
)
465 row
.operator("palette.preset_add", text
="", icon
='ADD').remove_active
= False
466 row
.operator("palette.preset_add", text
="", icon
='REMOVE').remove_active
= True
468 col
= layout
.column(align
=True)
469 row
= col
.row(align
=True)
470 row
.operator("palette_props.add_color", icon
='ADD')
471 row
.prop(palette_props
, "index")
472 row
.operator("palette_props.remove_color", icon
="PANEL_CLOSE")
474 row
= col
.row(align
=True)
475 row
.prop(palette_props
, "columns")
476 if palette_props
.colors
.items():
478 row
= layout
.row(align
=True)
479 row
.prop(palette_props
, "color_name")
480 row
.operator("palette_props.sample_tool_color", icon
="COLOR")
482 laycol
= layout
.column(align
=False)
484 if palette_props
.columns
:
485 columns
= palette_props
.columns
489 for i
, color
in enumerate(palette_props
.colors
):
491 row1
= laycol
.row(align
=True)
493 row2
= laycol
.row(align
=True)
496 active
= True if i
== palette_props
.current_color_index
else False
497 icons
= "LAYER_ACTIVE" if active
else "LAYER_USED"
498 row1
.prop(palette_props
.colors
[i
], "color", event
=True, toggle
=True)
499 row2
.operator("paint.select_color", text
=" ",
500 emboss
=active
, icon
=icons
).color_index
= i
504 row
.prop(palette_props
, "presets_folder", text
="")
507 class BrushButtonsPanel():
508 bl_space_type
= 'IMAGE_EDITOR'
509 bl_region_type
= 'UI'
512 def poll(cls
, context
):
513 sima
= context
.space_data
514 toolsettings
= context
.tool_settings
.image_paint
515 return sima
.show_paint
and toolsettings
.brush
519 bl_space_type
= 'VIEW_3D'
520 bl_region_type
= 'UI'
521 bl_category
= 'Paint'
524 def paint_settings(context
):
525 ts
= context
.tool_settings
527 if context
.vertex_paint_object
:
528 return ts
.vertex_paint
529 elif context
.weight_paint_object
:
530 return ts
.weight_paint
531 elif context
.texture_paint_object
:
532 return ts
.image_paint
536 class IMAGE_PT_color_palette(BrushButtonsPanel
, Panel
):
537 bl_label
= "Color Palette"
538 bl_options
= {'DEFAULT_CLOSED'}
540 def draw(self
, context
):
541 color_palette_draw(self
, context
)
544 class VIEW3D_PT_color_palette(PaintPanel
, Panel
):
545 bl_label
= "Color Palette"
546 bl_options
= {'DEFAULT_CLOSED'}
549 def poll(cls
, context
):
550 return (context
.image_paint_object
or context
.vertex_paint_object
)
552 def draw(self
, context
):
553 color_palette_draw(self
, context
)
556 class VIEW3D_OT_select_weight(Operator
):
557 bl_idname
= "paint.select_weight"
559 bl_description
= "Select this weight value slot"
560 bl_options
= {'UNDO'}
562 weight_index
: IntProperty()
564 def current_weight(self
):
565 pp
= bpy
.context
.scene
.palette_props
566 if self
.weight_index
== 0:
568 elif self
.weight_index
== 1:
570 elif self
.weight_index
== 2:
572 elif self
.weight_index
== 3:
574 elif self
.weight_index
== 4:
576 elif self
.weight_index
== 5:
578 elif self
.weight_index
== 6:
580 elif self
.weight_index
== 7:
582 elif self
.weight_index
== 8:
584 elif self
.weight_index
== 9:
586 elif self
.weight_index
== 10:
587 weight
= pp
.weight_10
590 def invoke(self
, context
, event
):
591 palette_props
= context
.scene
.palette_props
592 palette_props
.current_weight_index
= self
.weight_index
594 if self
.weight_index
== 0:
595 weight
= palette_props
.weight_0
596 elif self
.weight_index
== 1:
597 weight
= palette_props
.weight_1
598 elif self
.weight_index
== 2:
599 weight
= palette_props
.weight_2
600 elif self
.weight_index
== 3:
601 weight
= palette_props
.weight_3
602 elif self
.weight_index
== 4:
603 weight
= palette_props
.weight_4
604 elif self
.weight_index
== 5:
605 weight
= palette_props
.weight_5
606 elif self
.weight_index
== 6:
607 weight
= palette_props
.weight_6
608 elif self
.weight_index
== 7:
609 weight
= palette_props
.weight_7
610 elif self
.weight_index
== 8:
611 weight
= palette_props
.weight_8
612 elif self
.weight_index
== 9:
613 weight
= palette_props
.weight_9
614 elif self
.weight_index
== 10:
615 weight
= palette_props
.weight_10
616 palette_props
.weight
= weight
621 class VIEW3D_OT_reset_weight_palette(Operator
):
622 bl_idname
= "paint.reset_weight_palette"
624 bl_description
= "Reset the active Weight slot to it's default value"
626 def execute(self
, context
):
628 palette_props
= context
.scene
.palette_props
630 0: 0.0, 1: 0.1, 2: 0.25,
631 3: 0.333, 4: 0.4, 5: 0.5,
632 6: 0.6, 7: 0.6666, 8: 0.75,
635 current_idx
= palette_props
.current_weight_index
636 palette_props
.weight
= dict_defs
[current_idx
]
638 var_name
= "weight_" + str(current_idx
)
639 var_to_change
= getattr(palette_props
, var_name
, None)
641 var_to_change
= dict_defs
[current_idx
]
645 except Exception as e
:
646 self
.report({'WARNING'},
647 "Reset Weight palette could not be completed (See Console for more info)")
648 print("\n[Paint Palette]\nOperator: paint.reset_weight_palette\nError: %s\n" % e
)
653 class VIEW3D_PT_weight_palette(PaintPanel
, Panel
):
654 bl_label
= "Weight Palette"
655 bl_options
= {'DEFAULT_CLOSED'}
658 def poll(cls
, context
):
659 return context
.weight_paint_object
661 def draw(self
, context
):
662 palette_props
= context
.scene
.palette_props
666 row
.prop(palette_props
, "weight", slider
=True)
669 selected_weight
= palette_props
.current_weight_index
670 for props
in range(0, 11):
671 embossed
= False if props
== selected_weight
else True
672 prop_name
= "weight_" + str(props
)
673 prop_value
= getattr(palette_props
, prop_name
, "")
675 row
= box
.row(align
=True)
676 elif (props
+ 2) % 3 == 0:
677 col
= box
.column(align
=True)
678 row
= col
.row(align
=True)
681 row
= box
.row(align
=True)
682 row
= row
.row(align
=True)
684 row
.operator("paint.select_weight", text
="%.2f" % prop_value
,
685 emboss
=embossed
).weight_index
= props
688 row
.operator("paint.reset_weight_palette", text
="Reset")
691 class PALETTE_Colors(PropertyGroup
):
692 """Class for colors CollectionProperty"""
693 color
: FloatVectorProperty(
696 default
=(0.8, 0.8, 0.8),
699 subtype
='COLOR_GAMMA',
704 class PALETTE_Props(PropertyGroup
):
706 def update_color_name(self
, context
):
707 pp
= bpy
.context
.scene
.palette_props
708 pp
.colors
[pp
.current_color_index
].name
= pp
.color_name
711 def move_color(self
, context
):
712 pp
= bpy
.context
.scene
.palette_props
713 if pp
.colors
.items() and pp
.current_color_index
!= pp
.index
:
714 if pp
.index
>= pp
.colors
.__len
__():
715 pp
.index
= pp
.colors
.__len
__() - 1
717 pp
.colors
.move(pp
.current_color_index
, pp
.index
)
718 pp
.current_color_index
= pp
.index
721 def update_weight(self
, context
):
722 pp
= context
.scene
.palette_props
724 if pp
.current_weight_index
== 0:
726 elif pp
.current_weight_index
== 1:
728 elif pp
.current_weight_index
== 2:
730 elif pp
.current_weight_index
== 3:
732 elif pp
.current_weight_index
== 4:
734 elif pp
.current_weight_index
== 5:
736 elif pp
.current_weight_index
== 6:
738 elif pp
.current_weight_index
== 7:
740 elif pp
.current_weight_index
== 8:
742 elif pp
.current_weight_index
== 9:
744 elif pp
.current_weight_index
== 10:
745 pp
.weight_10
= weight
746 bpy
.context
.tool_settings
.unified_paint_settings
.weight
= weight
749 palette_name
: StringProperty(
754 color_name
: StringProperty(
756 description
="Color Name",
758 update
=update_color_name
760 columns
: IntProperty(
762 description
="Number of Columns",
768 description
="Move Selected Color",
772 notes
: StringProperty(
773 name
="Palette Notes",
776 current_color_index
: IntProperty(
777 name
="Current Color Index",
782 current_weight_index
: IntProperty(
783 name
="Current Color Index",
788 presets_folder
: StringProperty(name
="",
789 description
="Palettes Folder",
793 colors
: CollectionProperty(
796 weight
: FloatProperty(
798 description
="Modify the active Weight preset slot value",
804 weight_0
: FloatProperty(
809 weight_1
: FloatProperty(
814 weight_2
: FloatProperty(
819 weight_3
: FloatProperty(
824 weight_4
: FloatProperty(
829 weight_5
: FloatProperty(
834 weight_6
: FloatProperty(
839 weight_7
: FloatProperty(
844 weight_8
: FloatProperty(
849 weight_9
: FloatProperty(
854 weight_10
: FloatProperty(
863 PALETTE_OT_load_gimp_palette
,
864 PALETTE_OT_preset_add
,
865 PALETTE_OT_add_color
,
866 PALETTE_OT_remove_color
,
867 PALETTE_OT_sample_tool_color
,
868 IMAGE_OT_select_color
,
869 IMAGE_PT_color_palette
,
870 VIEW3D_PT_color_palette
,
871 VIEW3D_OT_select_weight
,
872 VIEW3D_OT_reset_weight_palette
,
873 VIEW3D_PT_weight_palette
,
881 bpy
.utils
.register_class(cls
)
883 bpy
.types
.Scene
.palette_props
= PointerProperty(
885 name
="Palette Props",
891 for cls
in reversed(classes
):
892 bpy
.utils
.unregister_class(cls
)
894 del bpy
.types
.Scene
.palette_props
897 if __name__
== "__main__":