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 "doc_url": "{BLENDER_MANUAL_URL}/addons/paint/paint_palettes.html",
35 This add-on brings palettes to the paint modes.
37 * Color Palette for Image Painting, Texture Paint and Vertex Paint modes.
38 * Weight Palette for the Weight Paint mode.
40 Set a number of colors (or weights according to the mode) and then associate it
41 with the brush by using the button under the color.
45 from bpy
.types
import (
51 from bpy
.props
import (
63 pp
= bpy
.context
.scene
.palette_props
64 current_color
= pp
.colors
[pp
.current_color_index
].color
65 pp
.color_name
= pp
.colors
[pp
.current_color_index
].name
66 brush
= current_brush()
67 brush
.color
= current_color
68 pp
.index
= pp
.current_color_index
72 pp
= bpy
.context
.scene
.palette_props
73 current_color
= pp
.colors
[pp
.current_color_index
]
74 brush
= current_brush()
75 current_color
.color
= brush
.color
81 if context
.area
.type == 'VIEW_3D' and context
.vertex_paint_object
:
82 brush
= context
.tool_settings
.vertex_paint
.brush
83 elif context
.area
.type == 'VIEW_3D' and context
.image_paint_object
:
84 brush
= context
.tool_settings
.image_paint
.brush
85 elif context
.area
.type == 'IMAGE_EDITOR' and context
.space_data
.mode
== 'PAINT':
86 brush
= context
.tool_settings
.image_paint
.brush
92 def update_weight_value():
93 pp
= bpy
.context
.scene
.palette_props
94 tt
= bpy
.context
.tool_settings
95 tt
.unified_paint_settings
.weight
= pp
.weight_value
99 def check_path_return():
100 from os
.path
import normpath
101 preset_path
= bpy
.path
.abspath(bpy
.context
.scene
.palette_props
.presets_folder
)
102 paths
= normpath(preset_path
)
104 return paths
if paths
else ""
107 class PALETTE_MT_menu(Menu
):
110 preset_operator
= "palette.load_gimp_palette"
112 def path_menu(self
, searchpaths
, operator
, props_default
={}):
114 # hard coded to set the operators 'filepath' to the filename.
120 if bpy
.data
.filepath
== "":
121 layout
.label(text
="*Please save the .blend file first*")
124 if not searchpaths
[0]:
125 layout
.label(text
="* Missing Paths *")
130 for directory
in searchpaths
:
131 files
.extend([(f
, os
.path
.join(directory
, f
)) for f
in os
.listdir(directory
)])
135 for f
, filepath
in files
:
137 if f
.startswith("."):
139 # do not load everything from the given folder, only .gpl files
143 preset_name
= bpy
.path
.display_name(f
)
144 props
= layout
.operator(operator
, text
=preset_name
)
146 for attr
, value
in props_default
.items():
147 setattr(props
, attr
, value
)
149 props
.filepath
= filepath
150 if operator
== "palette.load_gimp_palette":
151 props
.menu_idname
= self
.bl_idname
153 def draw_preset(self
, context
):
154 paths
= check_path_return()
155 self
.path_menu([paths
], self
.preset_operator
)
160 class PALETTE_OT_load_gimp_palette(Operator
):
161 """Execute a preset"""
162 bl_idname
= "palette.load_gimp_palette"
163 bl_label
= "Load a Gimp palette"
165 filepath
: StringProperty(
167 description
="Path of the .gpl file to load",
170 menu_idname
: StringProperty(
172 description
="ID name of the menu this was called from",
176 def execute(self
, context
):
177 from os
.path
import basename
179 filepath
= self
.filepath
181 palette_props
= bpy
.context
.scene
.palette_props
182 palette_props
.current_color_index
= 0
184 # change the menu title to the most recently chosen option
185 preset_class
= getattr(bpy
.types
, self
.menu_idname
)
186 preset_class
.bl_label
= bpy
.path
.display_name(basename(filepath
))
188 palette_props
.columns
= 0
189 error_palette
= False # errors found
190 error_import
= [] # collect exception messages
191 start_color_index
= 0 # store the starting line for color definitions
193 if filepath
[-4:] != ".gpl":
196 gpl
= open(filepath
, "r")
197 lines
= gpl
.readlines()
198 palette_props
.notes
= ''
200 for index_0
, line
in enumerate(lines
):
201 if not line
or (line
[:12] == "GIMP Palette"):
203 elif line
[:5] == "Name:":
204 palette_props
.palette_name
= line
[5:]
205 elif line
[:8] == "Columns:":
206 palette_props
.columns
= int(line
[8:])
208 palette_props
.notes
+= line
209 elif line
[0] == "\n":
213 start_color_index
= index_0
217 for i
, ln
in enumerate(lines
[start_color_index
:]):
219 palette_props
.colors
[i
]
221 palette_props
.colors
.add()
223 # get line - find keywords with re.split, remove the empty ones with filter
224 get_line
= list(filter(None, re
.split(r
'\t+|\s+', ln
.rstrip('\n'))))
225 extract_colors
= get_line
[:3]
226 get_color_name
= [str(name
) for name
in get_line
[3:]]
227 color
= [float(rgb
) / 255 for rgb
in extract_colors
]
228 palette_props
.colors
[i
].color
= color
229 palette_props
.colors
[i
].name
= " ".join(get_color_name
) or "Color " + str(i
)
230 except Exception as e
:
232 error_import
.append(".gpl file line: {}, error: {}".format(i
+ 1 + start_color_index
, e
))
236 while palette_props
.colors
.__len
__() > exceeding
:
237 palette_props
.colors
.remove(exceeding
)
244 message
= "Loaded palette from file: {}".format(filepath
)
247 message
= "Not supported palette format for file: {}".format(filepath
)
249 message
= "Some of the .gpl palette data can not be parsed. See Console for more info"
250 print("\n[Paint Palette]\nOperator: palette.load_gimp_palette\nErrors: %s\n" %
251 ('\n'.join(error_import
)))
253 self
.report({'INFO'}, message
)
258 class WriteGimpPalette():
259 """Base preset class, only for subclassing
260 subclasses must define
263 bl_options
= {'REGISTER'} # only because invoke_props_popup requires
265 name
: StringProperty(
267 description
="Name of the preset, used to make the path name",
269 options
={'SKIP_SAVE'},
272 remove_active
: BoolProperty(
278 def as_filename(name
): # could reuse for other presets
279 for char
in " !@#$%^&*(){}:\";'[]<>,.\\/?":
280 name
= name
.replace(char
, '_')
281 return name
.lower().strip()
283 def execute(self
, context
):
285 pp
= bpy
.context
.scene
.palette_props
287 if hasattr(self
, "pre_cb"):
290 preset_menu_class
= getattr(bpy
.types
, self
.preset_menu
)
291 target_path
= check_path_return()
294 self
.report({'WARNING'}, "Failed to create presets path")
297 if not os
.path
.exists(target_path
):
298 self
.report({'WARNING'},
299 "Failure to open the saved Palettes Folder. Check if the path exists")
302 if not self
.remove_active
:
304 self
.report({'INFO'},
305 "No name is given for the preset entry. Operation Cancelled")
308 filename
= self
.as_filename(self
.name
)
309 filepath
= os
.path
.join(target_path
, filename
) + ".gpl"
310 file_preset
= open(filepath
, 'wb')
311 gpl
= "GIMP Palette\n"
312 gpl
+= "Name: %s\n" % filename
313 gpl
+= "Columns: %d\n" % pp
.columns
315 if pp
.colors
.items():
316 for i
, color
in enumerate(pp
.colors
):
317 gpl
+= "%3d%4d%4d %s" % (color
.color
.r
* 255, color
.color
.g
* 255,
318 color
.color
.b
* 255, color
.name
+ '\n')
319 file_preset
.write(bytes(gpl
, 'UTF-8'))
323 pp
.palette_name
= filename
324 preset_menu_class
.bl_label
= bpy
.path
.display_name(filename
)
326 self
.report({'INFO'}, "Created Palette: {}".format(filepath
))
329 preset_active
= preset_menu_class
.bl_label
330 filename
= self
.as_filename(preset_active
)
332 filepath
= os
.path
.join(target_path
, filename
) + ".gpl"
334 if not filepath
or not os
.path
.exists(filepath
):
335 self
.report({'WARNING'}, "Preset could not be found. Operation Cancelled")
336 self
.reset_preset_name(preset_menu_class
, pp
)
339 if hasattr(self
, "remove"):
340 self
.remove(context
, filepath
)
344 self
.report({'INFO'}, "Deleted palette: {}".format(filepath
))
347 traceback
.print_exc()
349 self
.reset_preset_name(preset_menu_class
, pp
)
351 if hasattr(self
, "post_cb"):
352 self
.post_cb(context
)
357 def reset_preset_name(presets
, props
):
359 presets
.bl_label
= "Presets"
360 props
.palette_name
= ""
362 def check(self
, context
):
363 self
.name
= self
.as_filename(self
.name
)
365 def invoke(self
, context
, event
):
366 if not self
.remove_active
:
367 wm
= context
.window_manager
368 return wm
.invoke_props_dialog(self
)
370 return self
.execute(context
)
373 class PALETTE_OT_preset_add(WriteGimpPalette
, Operator
):
374 bl_idname
= "palette.preset_add"
375 bl_label
= "Add Palette Preset"
376 preset_menu
= "PALETTE_MT_menu"
377 bl_description
= "Add a Palette Preset"
381 preset_subdir
= "palette"
384 class PALETTE_OT_add_color(Operator
):
385 bl_idname
= "palette_props.add_color"
387 bl_description
= "Add a Color to the Palette"
389 def execute(self
, context
):
390 pp
= bpy
.context
.scene
.palette_props
392 if pp
.colors
.items():
393 new_index
= pp
.current_color_index
+ 1
396 last
= pp
.colors
.__len
__() - 1
398 pp
.colors
.move(last
, new_index
)
399 pp
.current_color_index
= new_index
406 class PALETTE_OT_remove_color(Operator
):
407 bl_idname
= "palette_props.remove_color"
409 bl_description
= "Remove Selected Color"
412 def poll(cls
, context
):
413 pp
= bpy
.context
.scene
.palette_props
414 return bool(pp
.colors
.items())
416 def execute(self
, context
):
417 pp
= context
.scene
.palette_props
418 i
= pp
.current_color_index
421 if pp
.current_color_index
>= pp
.colors
.__len
__():
422 pp
.index
= pp
.current_color_index
= pp
.colors
.__len
__() - 1
427 class PALETTE_OT_sample_tool_color(Operator
):
428 bl_idname
= "palette_props.sample_tool_color"
430 bl_description
= "Sample Tool Color"
432 def execute(self
, context
):
433 pp
= context
.scene
.palette_props
434 brush
= current_brush()
435 pp
.colors
[pp
.current_color_index
].color
= brush
.color
440 class IMAGE_OT_select_color(Operator
):
441 bl_idname
= "paint.select_color"
443 bl_description
= "Select this color"
444 bl_options
= {'UNDO'}
446 color_index
: IntProperty()
448 def invoke(self
, context
, event
):
449 palette_props
= context
.scene
.palette_props
450 palette_props
.current_color_index
= self
.color_index
457 def color_palette_draw(self
, context
):
458 palette_props
= context
.scene
.palette_props
462 row
= layout
.row(align
=True)
463 row
.menu("PALETTE_MT_menu", text
=PALETTE_MT_menu
.bl_label
)
464 row
.operator("palette.preset_add", text
="", icon
='ADD').remove_active
= False
465 row
.operator("palette.preset_add", text
="", icon
='REMOVE').remove_active
= True
467 col
= layout
.column(align
=True)
468 row
= col
.row(align
=True)
469 row
.operator("palette_props.add_color", icon
='ADD')
470 row
.prop(palette_props
, "index")
471 row
.operator("palette_props.remove_color", icon
="PANEL_CLOSE")
473 row
= col
.row(align
=True)
474 row
.prop(palette_props
, "columns")
475 if palette_props
.colors
.items():
477 row
= layout
.row(align
=True)
478 row
.prop(palette_props
, "color_name")
479 row
.operator("palette_props.sample_tool_color", icon
="COLOR")
481 laycol
= layout
.column(align
=False)
483 if palette_props
.columns
:
484 columns
= palette_props
.columns
488 for i
, color
in enumerate(palette_props
.colors
):
490 row1
= laycol
.row(align
=True)
492 row2
= laycol
.row(align
=True)
495 active
= True if i
== palette_props
.current_color_index
else False
496 icons
= "LAYER_ACTIVE" if active
else "LAYER_USED"
497 row1
.prop(palette_props
.colors
[i
], "color", event
=True, toggle
=True)
498 row2
.operator("paint.select_color", text
=" ",
499 emboss
=active
, icon
=icons
).color_index
= i
503 row
.prop(palette_props
, "presets_folder", text
="")
506 class BrushButtonsPanel():
507 bl_space_type
= 'IMAGE_EDITOR'
508 bl_region_type
= 'UI'
511 def poll(cls
, context
):
512 sima
= context
.space_data
513 toolsettings
= context
.tool_settings
.image_paint
514 return sima
.show_paint
and toolsettings
.brush
518 bl_space_type
= 'VIEW_3D'
519 bl_region_type
= 'UI'
520 bl_category
= 'Paint'
523 def paint_settings(context
):
524 ts
= context
.tool_settings
526 if context
.vertex_paint_object
:
527 return ts
.vertex_paint
528 elif context
.weight_paint_object
:
529 return ts
.weight_paint
530 elif context
.texture_paint_object
:
531 return ts
.image_paint
535 class IMAGE_PT_color_palette(BrushButtonsPanel
, Panel
):
536 bl_label
= "Color Palette"
537 bl_options
= {'DEFAULT_CLOSED'}
539 def draw(self
, context
):
540 color_palette_draw(self
, context
)
543 class VIEW3D_PT_color_palette(PaintPanel
, Panel
):
544 bl_label
= "Color Palette"
545 bl_options
= {'DEFAULT_CLOSED'}
548 def poll(cls
, context
):
549 return (context
.image_paint_object
or context
.vertex_paint_object
)
551 def draw(self
, context
):
552 color_palette_draw(self
, context
)
555 class VIEW3D_OT_select_weight(Operator
):
556 bl_idname
= "paint.select_weight"
558 bl_description
= "Select this weight value slot"
559 bl_options
= {'UNDO'}
561 weight_index
: IntProperty()
563 def current_weight(self
):
564 pp
= bpy
.context
.scene
.palette_props
565 if self
.weight_index
== 0:
567 elif self
.weight_index
== 1:
569 elif self
.weight_index
== 2:
571 elif self
.weight_index
== 3:
573 elif self
.weight_index
== 4:
575 elif self
.weight_index
== 5:
577 elif self
.weight_index
== 6:
579 elif self
.weight_index
== 7:
581 elif self
.weight_index
== 8:
583 elif self
.weight_index
== 9:
585 elif self
.weight_index
== 10:
586 weight
= pp
.weight_10
589 def invoke(self
, context
, event
):
590 palette_props
= context
.scene
.palette_props
591 palette_props
.current_weight_index
= self
.weight_index
593 if self
.weight_index
== 0:
594 weight
= palette_props
.weight_0
595 elif self
.weight_index
== 1:
596 weight
= palette_props
.weight_1
597 elif self
.weight_index
== 2:
598 weight
= palette_props
.weight_2
599 elif self
.weight_index
== 3:
600 weight
= palette_props
.weight_3
601 elif self
.weight_index
== 4:
602 weight
= palette_props
.weight_4
603 elif self
.weight_index
== 5:
604 weight
= palette_props
.weight_5
605 elif self
.weight_index
== 6:
606 weight
= palette_props
.weight_6
607 elif self
.weight_index
== 7:
608 weight
= palette_props
.weight_7
609 elif self
.weight_index
== 8:
610 weight
= palette_props
.weight_8
611 elif self
.weight_index
== 9:
612 weight
= palette_props
.weight_9
613 elif self
.weight_index
== 10:
614 weight
= palette_props
.weight_10
615 palette_props
.weight
= weight
620 class VIEW3D_OT_reset_weight_palette(Operator
):
621 bl_idname
= "paint.reset_weight_palette"
623 bl_description
= "Reset the active Weight slot to it's default value"
625 def execute(self
, context
):
627 palette_props
= context
.scene
.palette_props
629 0: 0.0, 1: 0.1, 2: 0.25,
630 3: 0.333, 4: 0.4, 5: 0.5,
631 6: 0.6, 7: 0.6666, 8: 0.75,
634 current_idx
= palette_props
.current_weight_index
635 palette_props
.weight
= dict_defs
[current_idx
]
637 var_name
= "weight_" + str(current_idx
)
638 var_to_change
= getattr(palette_props
, var_name
, None)
640 var_to_change
= dict_defs
[current_idx
]
644 except Exception as e
:
645 self
.report({'WARNING'},
646 "Reset Weight palette could not be completed (See Console for more info)")
647 print("\n[Paint Palette]\nOperator: paint.reset_weight_palette\nError: %s\n" % e
)
652 class VIEW3D_PT_weight_palette(PaintPanel
, Panel
):
653 bl_label
= "Weight Palette"
654 bl_options
= {'DEFAULT_CLOSED'}
657 def poll(cls
, context
):
658 return context
.weight_paint_object
660 def draw(self
, context
):
661 palette_props
= context
.scene
.palette_props
665 row
.prop(palette_props
, "weight", slider
=True)
668 selected_weight
= palette_props
.current_weight_index
669 for props
in range(0, 11):
670 embossed
= False if props
== selected_weight
else True
671 prop_name
= "weight_" + str(props
)
672 prop_value
= getattr(palette_props
, prop_name
, "")
674 row
= box
.row(align
=True)
675 elif (props
+ 2) % 3 == 0:
676 col
= box
.column(align
=True)
677 row
= col
.row(align
=True)
680 row
= box
.row(align
=True)
681 row
= row
.row(align
=True)
683 row
.operator("paint.select_weight", text
="%.2f" % prop_value
,
684 emboss
=embossed
).weight_index
= props
687 row
.operator("paint.reset_weight_palette", text
="Reset")
690 class PALETTE_Colors(PropertyGroup
):
691 """Class for colors CollectionProperty"""
692 color
: FloatVectorProperty(
695 default
=(0.8, 0.8, 0.8),
698 subtype
='COLOR_GAMMA',
703 class PALETTE_Props(PropertyGroup
):
705 def update_color_name(self
, context
):
706 pp
= bpy
.context
.scene
.palette_props
707 pp
.colors
[pp
.current_color_index
].name
= pp
.color_name
710 def move_color(self
, context
):
711 pp
= bpy
.context
.scene
.palette_props
712 if pp
.colors
.items() and pp
.current_color_index
!= pp
.index
:
713 if pp
.index
>= pp
.colors
.__len
__():
714 pp
.index
= pp
.colors
.__len
__() - 1
716 pp
.colors
.move(pp
.current_color_index
, pp
.index
)
717 pp
.current_color_index
= pp
.index
720 def update_weight(self
, context
):
721 pp
= context
.scene
.palette_props
723 if pp
.current_weight_index
== 0:
725 elif pp
.current_weight_index
== 1:
727 elif pp
.current_weight_index
== 2:
729 elif pp
.current_weight_index
== 3:
731 elif pp
.current_weight_index
== 4:
733 elif pp
.current_weight_index
== 5:
735 elif pp
.current_weight_index
== 6:
737 elif pp
.current_weight_index
== 7:
739 elif pp
.current_weight_index
== 8:
741 elif pp
.current_weight_index
== 9:
743 elif pp
.current_weight_index
== 10:
744 pp
.weight_10
= weight
745 bpy
.context
.tool_settings
.unified_paint_settings
.weight
= weight
748 palette_name
: StringProperty(
753 color_name
: StringProperty(
755 description
="Color Name",
757 update
=update_color_name
759 columns
: IntProperty(
761 description
="Number of Columns",
767 description
="Move Selected Color",
771 notes
: StringProperty(
772 name
="Palette Notes",
775 current_color_index
: IntProperty(
776 name
="Current Color Index",
781 current_weight_index
: IntProperty(
782 name
="Current Color Index",
787 presets_folder
: StringProperty(name
="",
788 description
="Palettes Folder",
792 colors
: CollectionProperty(
795 weight
: FloatProperty(
797 description
="Modify the active Weight preset slot value",
803 weight_0
: FloatProperty(
808 weight_1
: FloatProperty(
813 weight_2
: FloatProperty(
818 weight_3
: FloatProperty(
823 weight_4
: FloatProperty(
828 weight_5
: FloatProperty(
833 weight_6
: FloatProperty(
838 weight_7
: FloatProperty(
843 weight_8
: FloatProperty(
848 weight_9
: FloatProperty(
853 weight_10
: FloatProperty(
862 PALETTE_OT_load_gimp_palette
,
863 PALETTE_OT_preset_add
,
864 PALETTE_OT_add_color
,
865 PALETTE_OT_remove_color
,
866 PALETTE_OT_sample_tool_color
,
867 IMAGE_OT_select_color
,
868 IMAGE_PT_color_palette
,
869 VIEW3D_PT_color_palette
,
870 VIEW3D_OT_select_weight
,
871 VIEW3D_OT_reset_weight_palette
,
872 VIEW3D_PT_weight_palette
,
880 bpy
.utils
.register_class(cls
)
882 bpy
.types
.Scene
.palette_props
= PointerProperty(
884 name
="Palette Props",
890 for cls
in reversed(classes
):
891 bpy
.utils
.unregister_class(cls
)
893 del bpy
.types
.Scene
.palette_props
896 if __name__
== "__main__":