1 # SPDX-FileCopyrightText: 2011 Dany Lebel (Axon_D)
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 "name": "Paint Palettes",
7 "author": "Dany Lebel (Axon D)",
10 "location": "Image Editor and 3D View > Any Paint mode > Color Palette or Weight Palette panel",
11 "description": "Palettes for color and weight paint modes",
13 "doc_url": "{BLENDER_MANUAL_URL}/addons/paint/paint_palettes.html",
18 This add-on brings palettes to the paint modes.
20 * Color Palette for Image Painting, Texture Paint and Vertex Paint modes.
21 * Weight Palette for the Weight Paint mode.
23 Set a number of colors (or weights according to the mode) and then associate it
24 with the brush by using the button under the color.
28 from bpy
.types
import (
34 from bpy
.props
import (
46 pp
= bpy
.context
.scene
.palette_props
47 current_color
= pp
.colors
[pp
.current_color_index
].color
48 pp
.color_name
= pp
.colors
[pp
.current_color_index
].name
49 brush
= current_brush()
50 brush
.color
= current_color
51 pp
.index
= pp
.current_color_index
55 pp
= bpy
.context
.scene
.palette_props
56 current_color
= pp
.colors
[pp
.current_color_index
]
57 brush
= current_brush()
58 current_color
.color
= brush
.color
64 if context
.area
.type == 'VIEW_3D' and context
.vertex_paint_object
:
65 brush
= context
.tool_settings
.vertex_paint
.brush
66 elif context
.area
.type == 'VIEW_3D' and context
.image_paint_object
:
67 brush
= context
.tool_settings
.image_paint
.brush
68 elif context
.area
.type == 'IMAGE_EDITOR' and context
.space_data
.mode
== 'PAINT':
69 brush
= context
.tool_settings
.image_paint
.brush
75 def update_weight_value():
76 pp
= bpy
.context
.scene
.palette_props
77 tt
= bpy
.context
.tool_settings
78 tt
.unified_paint_settings
.weight
= pp
.weight_value
82 def check_path_return():
83 from os
.path
import normpath
84 preset_path
= bpy
.path
.abspath(bpy
.context
.scene
.palette_props
.presets_folder
)
85 paths
= normpath(preset_path
)
87 return paths
if paths
else ""
90 class PALETTE_MT_menu(Menu
):
93 preset_operator
= "palette.load_gimp_palette"
95 def path_menu(self
, searchpaths
, operator
, props_default
={}):
97 # hard coded to set the operators 'filepath' to the filename.
103 if bpy
.data
.filepath
== "":
104 layout
.label(text
="*Please save the .blend file first*")
107 if not searchpaths
[0]:
108 layout
.label(text
="* Missing Paths *")
113 for directory
in searchpaths
:
114 files
.extend([(f
, os
.path
.join(directory
, f
)) for f
in os
.listdir(directory
)])
118 for f
, filepath
in files
:
120 if f
.startswith("."):
122 # do not load everything from the given folder, only .gpl files
126 preset_name
= bpy
.path
.display_name(f
)
127 props
= layout
.operator(operator
, text
=preset_name
)
129 for attr
, value
in props_default
.items():
130 setattr(props
, attr
, value
)
132 props
.filepath
= filepath
133 if operator
== "palette.load_gimp_palette":
134 props
.menu_idname
= self
.bl_idname
136 def draw_preset(self
, context
):
137 paths
= check_path_return()
138 self
.path_menu([paths
], self
.preset_operator
)
143 class PALETTE_OT_load_gimp_palette(Operator
):
144 """Execute a preset"""
145 bl_idname
= "palette.load_gimp_palette"
146 bl_label
= "Load a Gimp palette"
148 filepath
: StringProperty(
150 description
="Path of the .gpl file to load",
153 menu_idname
: StringProperty(
155 description
="ID name of the menu this was called from",
159 def execute(self
, context
):
160 from os
.path
import basename
162 filepath
= self
.filepath
164 palette_props
= bpy
.context
.scene
.palette_props
165 palette_props
.current_color_index
= 0
167 # change the menu title to the most recently chosen option
168 preset_class
= getattr(bpy
.types
, self
.menu_idname
)
169 preset_class
.bl_label
= bpy
.path
.display_name(basename(filepath
))
171 palette_props
.columns
= 0
172 error_palette
= False # errors found
173 error_import
= [] # collect exception messages
174 start_color_index
= 0 # store the starting line for color definitions
176 if filepath
[-4:] != ".gpl":
179 gpl
= open(filepath
, "r")
180 lines
= gpl
.readlines()
181 palette_props
.notes
= ''
183 for index_0
, line
in enumerate(lines
):
184 if not line
or (line
[:12] == "GIMP Palette"):
186 elif line
[:5] == "Name:":
187 palette_props
.palette_name
= line
[5:]
188 elif line
[:8] == "Columns:":
189 palette_props
.columns
= int(line
[8:])
191 palette_props
.notes
+= line
192 elif line
[0] == "\n":
196 start_color_index
= index_0
200 for i
, ln
in enumerate(lines
[start_color_index
:]):
202 palette_props
.colors
[i
]
204 palette_props
.colors
.add()
206 # get line - find keywords with re.split, remove the empty ones with filter
207 get_line
= list(filter(None, re
.split(r
'\t+|\s+', ln
.rstrip('\n'))))
208 extract_colors
= get_line
[:3]
209 get_color_name
= [str(name
) for name
in get_line
[3:]]
210 color
= [float(rgb
) / 255 for rgb
in extract_colors
]
211 palette_props
.colors
[i
].color
= color
212 palette_props
.colors
[i
].name
= " ".join(get_color_name
) or "Color " + str(i
)
213 except Exception as e
:
215 error_import
.append(".gpl file line: {}, error: {}".format(i
+ 1 + start_color_index
, e
))
219 while palette_props
.colors
.__len
__() > exceeding
:
220 palette_props
.colors
.remove(exceeding
)
227 message
= "Loaded palette from file: {}".format(filepath
)
230 message
= "Not supported palette format for file: {}".format(filepath
)
232 message
= "Some of the .gpl palette data can not be parsed. See Console for more info"
233 print("\n[Paint Palette]\nOperator: palette.load_gimp_palette\nErrors: %s\n" %
234 ('\n'.join(error_import
)))
236 self
.report({'INFO'}, message
)
241 class WriteGimpPalette():
242 """Base preset class, only for subclassing
243 subclasses must define
246 bl_options
= {'REGISTER'} # only because invoke_props_popup requires
248 name
: StringProperty(
250 description
="Name of the preset, used to make the path name",
252 options
={'SKIP_SAVE'},
255 remove_active
: BoolProperty(
261 def as_filename(name
): # could reuse for other presets
262 for char
in " !@#$%^&*(){}:\";'[]<>,.\\/?":
263 name
= name
.replace(char
, '_')
264 return name
.lower().strip()
266 def execute(self
, context
):
268 pp
= bpy
.context
.scene
.palette_props
270 if hasattr(self
, "pre_cb"):
273 preset_menu_class
= getattr(bpy
.types
, self
.preset_menu
)
274 target_path
= check_path_return()
277 self
.report({'WARNING'}, "Failed to create presets path")
280 if not os
.path
.exists(target_path
):
281 self
.report({'WARNING'},
282 "Failure to open the saved Palettes Folder. Check if the path exists")
285 if not self
.remove_active
:
287 self
.report({'INFO'},
288 "No name is given for the preset entry. Operation Cancelled")
291 filename
= self
.as_filename(self
.name
)
292 filepath
= os
.path
.join(target_path
, filename
) + ".gpl"
293 file_preset
= open(filepath
, 'wb')
294 gpl
= "GIMP Palette\n"
295 gpl
+= "Name: %s\n" % filename
296 gpl
+= "Columns: %d\n" % pp
.columns
298 if pp
.colors
.items():
299 for i
, color
in enumerate(pp
.colors
):
300 gpl
+= "%3d%4d%4d %s" % (color
.color
.r
* 255, color
.color
.g
* 255,
301 color
.color
.b
* 255, color
.name
+ '\n')
302 file_preset
.write(bytes(gpl
, 'UTF-8'))
306 pp
.palette_name
= filename
307 preset_menu_class
.bl_label
= bpy
.path
.display_name(filename
)
309 self
.report({'INFO'}, "Created Palette: {}".format(filepath
))
312 preset_active
= preset_menu_class
.bl_label
313 filename
= self
.as_filename(preset_active
)
315 filepath
= os
.path
.join(target_path
, filename
) + ".gpl"
317 if not filepath
or not os
.path
.exists(filepath
):
318 self
.report({'WARNING'}, "Preset could not be found. Operation Cancelled")
319 self
.reset_preset_name(preset_menu_class
, pp
)
322 if hasattr(self
, "remove"):
323 self
.remove(context
, filepath
)
327 self
.report({'INFO'}, "Deleted palette: {}".format(filepath
))
330 traceback
.print_exc()
332 self
.reset_preset_name(preset_menu_class
, pp
)
334 if hasattr(self
, "post_cb"):
335 self
.post_cb(context
)
340 def reset_preset_name(presets
, props
):
342 presets
.bl_label
= "Presets"
343 props
.palette_name
= ""
345 def check(self
, context
):
346 self
.name
= self
.as_filename(self
.name
)
348 def invoke(self
, context
, event
):
349 if not self
.remove_active
:
350 wm
= context
.window_manager
351 return wm
.invoke_props_dialog(self
)
353 return self
.execute(context
)
356 class PALETTE_OT_preset_add(WriteGimpPalette
, Operator
):
357 bl_idname
= "palette.preset_add"
358 bl_label
= "Add Palette Preset"
359 preset_menu
= "PALETTE_MT_menu"
360 bl_description
= "Add a Palette Preset"
364 preset_subdir
= "palette"
367 class PALETTE_OT_add_color(Operator
):
368 bl_idname
= "palette_props.add_color"
370 bl_description
= "Add a Color to the Palette"
372 def execute(self
, context
):
373 pp
= bpy
.context
.scene
.palette_props
375 if pp
.colors
.items():
376 new_index
= pp
.current_color_index
+ 1
379 last
= pp
.colors
.__len
__() - 1
381 pp
.colors
.move(last
, new_index
)
382 pp
.current_color_index
= new_index
389 class PALETTE_OT_remove_color(Operator
):
390 bl_idname
= "palette_props.remove_color"
392 bl_description
= "Remove Selected Color"
395 def poll(cls
, context
):
396 pp
= bpy
.context
.scene
.palette_props
397 return bool(pp
.colors
.items())
399 def execute(self
, context
):
400 pp
= context
.scene
.palette_props
401 i
= pp
.current_color_index
404 if pp
.current_color_index
>= pp
.colors
.__len
__():
405 pp
.index
= pp
.current_color_index
= pp
.colors
.__len
__() - 1
410 class PALETTE_OT_sample_tool_color(Operator
):
411 bl_idname
= "palette_props.sample_tool_color"
413 bl_description
= "Sample Tool Color"
415 def execute(self
, context
):
416 pp
= context
.scene
.palette_props
417 brush
= current_brush()
418 pp
.colors
[pp
.current_color_index
].color
= brush
.color
423 class IMAGE_OT_select_color(Operator
):
424 bl_idname
= "paint.select_color"
426 bl_description
= "Select this color"
427 bl_options
= {'UNDO'}
429 color_index
: IntProperty()
431 def invoke(self
, context
, event
):
432 palette_props
= context
.scene
.palette_props
433 palette_props
.current_color_index
= self
.color_index
440 def color_palette_draw(self
, context
):
441 palette_props
= context
.scene
.palette_props
445 row
= layout
.row(align
=True)
446 row
.menu("PALETTE_MT_menu", text
=PALETTE_MT_menu
.bl_label
)
447 row
.operator("palette.preset_add", text
="", icon
='ADD').remove_active
= False
448 row
.operator("palette.preset_add", text
="", icon
='REMOVE').remove_active
= True
450 col
= layout
.column(align
=True)
451 row
= col
.row(align
=True)
452 row
.operator("palette_props.add_color", icon
='ADD')
453 row
.prop(palette_props
, "index")
454 row
.operator("palette_props.remove_color", icon
="PANEL_CLOSE")
456 row
= col
.row(align
=True)
457 row
.prop(palette_props
, "columns")
458 if palette_props
.colors
.items():
460 row
= layout
.row(align
=True)
461 row
.prop(palette_props
, "color_name")
462 row
.operator("palette_props.sample_tool_color", icon
="COLOR")
464 laycol
= layout
.column(align
=False)
466 if palette_props
.columns
:
467 columns
= palette_props
.columns
471 for i
, color
in enumerate(palette_props
.colors
):
473 row1
= laycol
.row(align
=True)
475 row2
= laycol
.row(align
=True)
478 active
= True if i
== palette_props
.current_color_index
else False
479 icons
= "LAYER_ACTIVE" if active
else "LAYER_USED"
480 row1
.prop(palette_props
.colors
[i
], "color", event
=True, toggle
=True)
481 row2
.operator("paint.select_color", text
=" ",
482 emboss
=active
, icon
=icons
).color_index
= i
486 row
.prop(palette_props
, "presets_folder", text
="")
489 class BrushButtonsPanel():
490 bl_space_type
= 'IMAGE_EDITOR'
491 bl_region_type
= 'UI'
494 def poll(cls
, context
):
495 sima
= context
.space_data
496 toolsettings
= context
.tool_settings
.image_paint
497 return sima
.show_paint
and toolsettings
.brush
501 bl_space_type
= 'VIEW_3D'
502 bl_region_type
= 'UI'
503 bl_category
= 'Paint'
506 def paint_settings(context
):
507 ts
= context
.tool_settings
509 if context
.vertex_paint_object
:
510 return ts
.vertex_paint
511 elif context
.weight_paint_object
:
512 return ts
.weight_paint
513 elif context
.texture_paint_object
:
514 return ts
.image_paint
518 class IMAGE_PT_color_palette(BrushButtonsPanel
, Panel
):
519 bl_label
= "Color Palette"
520 bl_options
= {'DEFAULT_CLOSED'}
522 def draw(self
, context
):
523 color_palette_draw(self
, context
)
526 class VIEW3D_PT_color_palette(PaintPanel
, Panel
):
527 bl_label
= "Color Palette"
528 bl_options
= {'DEFAULT_CLOSED'}
531 def poll(cls
, context
):
532 return (context
.image_paint_object
or context
.vertex_paint_object
)
534 def draw(self
, context
):
535 color_palette_draw(self
, context
)
538 class VIEW3D_OT_select_weight(Operator
):
539 bl_idname
= "paint.select_weight"
541 bl_description
= "Select this weight value slot"
542 bl_options
= {'UNDO'}
544 weight_index
: IntProperty()
546 def current_weight(self
):
547 pp
= bpy
.context
.scene
.palette_props
548 if self
.weight_index
== 0:
550 elif self
.weight_index
== 1:
552 elif self
.weight_index
== 2:
554 elif self
.weight_index
== 3:
556 elif self
.weight_index
== 4:
558 elif self
.weight_index
== 5:
560 elif self
.weight_index
== 6:
562 elif self
.weight_index
== 7:
564 elif self
.weight_index
== 8:
566 elif self
.weight_index
== 9:
568 elif self
.weight_index
== 10:
569 weight
= pp
.weight_10
572 def invoke(self
, context
, event
):
573 palette_props
= context
.scene
.palette_props
574 palette_props
.current_weight_index
= self
.weight_index
576 if self
.weight_index
== 0:
577 weight
= palette_props
.weight_0
578 elif self
.weight_index
== 1:
579 weight
= palette_props
.weight_1
580 elif self
.weight_index
== 2:
581 weight
= palette_props
.weight_2
582 elif self
.weight_index
== 3:
583 weight
= palette_props
.weight_3
584 elif self
.weight_index
== 4:
585 weight
= palette_props
.weight_4
586 elif self
.weight_index
== 5:
587 weight
= palette_props
.weight_5
588 elif self
.weight_index
== 6:
589 weight
= palette_props
.weight_6
590 elif self
.weight_index
== 7:
591 weight
= palette_props
.weight_7
592 elif self
.weight_index
== 8:
593 weight
= palette_props
.weight_8
594 elif self
.weight_index
== 9:
595 weight
= palette_props
.weight_9
596 elif self
.weight_index
== 10:
597 weight
= palette_props
.weight_10
598 palette_props
.weight
= weight
603 class VIEW3D_OT_reset_weight_palette(Operator
):
604 bl_idname
= "paint.reset_weight_palette"
606 bl_description
= "Reset the active Weight slot to it's default value"
608 def execute(self
, context
):
610 palette_props
= context
.scene
.palette_props
612 0: 0.0, 1: 0.1, 2: 0.25,
613 3: 0.333, 4: 0.4, 5: 0.5,
614 6: 0.6, 7: 0.6666, 8: 0.75,
617 current_idx
= palette_props
.current_weight_index
618 palette_props
.weight
= dict_defs
[current_idx
]
620 var_name
= "weight_" + str(current_idx
)
621 var_to_change
= getattr(palette_props
, var_name
, None)
623 var_to_change
= dict_defs
[current_idx
]
627 except Exception as e
:
628 self
.report({'WARNING'},
629 "Reset Weight palette could not be completed (See Console for more info)")
630 print("\n[Paint Palette]\nOperator: paint.reset_weight_palette\nError: %s\n" % e
)
635 class VIEW3D_PT_weight_palette(PaintPanel
, Panel
):
636 bl_label
= "Weight Palette"
637 bl_options
= {'DEFAULT_CLOSED'}
640 def poll(cls
, context
):
641 return context
.weight_paint_object
643 def draw(self
, context
):
644 palette_props
= context
.scene
.palette_props
648 row
.prop(palette_props
, "weight", slider
=True)
651 selected_weight
= palette_props
.current_weight_index
652 for props
in range(0, 11):
653 embossed
= False if props
== selected_weight
else True
654 prop_name
= "weight_" + str(props
)
655 prop_value
= getattr(palette_props
, prop_name
, "")
657 row
= box
.row(align
=True)
658 elif (props
+ 2) % 3 == 0:
659 col
= box
.column(align
=True)
660 row
= col
.row(align
=True)
663 row
= box
.row(align
=True)
664 row
= row
.row(align
=True)
666 row
.operator("paint.select_weight", text
="%.2f" % prop_value
,
667 emboss
=embossed
).weight_index
= props
670 row
.operator("paint.reset_weight_palette", text
="Reset")
673 class PALETTE_Colors(PropertyGroup
):
674 """Class for colors CollectionProperty"""
675 color
: FloatVectorProperty(
678 default
=(0.8, 0.8, 0.8),
681 subtype
='COLOR_GAMMA',
686 class PALETTE_Props(PropertyGroup
):
688 def update_color_name(self
, context
):
689 pp
= bpy
.context
.scene
.palette_props
690 pp
.colors
[pp
.current_color_index
].name
= pp
.color_name
693 def move_color(self
, context
):
694 pp
= bpy
.context
.scene
.palette_props
695 if pp
.colors
.items() and pp
.current_color_index
!= pp
.index
:
696 if pp
.index
>= pp
.colors
.__len
__():
697 pp
.index
= pp
.colors
.__len
__() - 1
699 pp
.colors
.move(pp
.current_color_index
, pp
.index
)
700 pp
.current_color_index
= pp
.index
703 def update_weight(self
, context
):
704 pp
= context
.scene
.palette_props
706 if pp
.current_weight_index
== 0:
708 elif pp
.current_weight_index
== 1:
710 elif pp
.current_weight_index
== 2:
712 elif pp
.current_weight_index
== 3:
714 elif pp
.current_weight_index
== 4:
716 elif pp
.current_weight_index
== 5:
718 elif pp
.current_weight_index
== 6:
720 elif pp
.current_weight_index
== 7:
722 elif pp
.current_weight_index
== 8:
724 elif pp
.current_weight_index
== 9:
726 elif pp
.current_weight_index
== 10:
727 pp
.weight_10
= weight
728 bpy
.context
.tool_settings
.unified_paint_settings
.weight
= weight
731 palette_name
: StringProperty(
736 color_name
: StringProperty(
738 description
="Color Name",
740 update
=update_color_name
742 columns
: IntProperty(
744 description
="Number of Columns",
750 description
="Move Selected Color",
754 notes
: StringProperty(
755 name
="Palette Notes",
758 current_color_index
: IntProperty(
759 name
="Current Color Index",
764 current_weight_index
: IntProperty(
765 name
="Current Color Index",
770 presets_folder
: StringProperty(name
="",
771 description
="Palettes Folder",
775 colors
: CollectionProperty(
778 weight
: FloatProperty(
780 description
="Modify the active Weight preset slot value",
786 weight_0
: FloatProperty(
791 weight_1
: FloatProperty(
796 weight_2
: FloatProperty(
801 weight_3
: FloatProperty(
806 weight_4
: FloatProperty(
811 weight_5
: FloatProperty(
816 weight_6
: FloatProperty(
821 weight_7
: FloatProperty(
826 weight_8
: FloatProperty(
831 weight_9
: FloatProperty(
836 weight_10
: FloatProperty(
845 PALETTE_OT_load_gimp_palette
,
846 PALETTE_OT_preset_add
,
847 PALETTE_OT_add_color
,
848 PALETTE_OT_remove_color
,
849 PALETTE_OT_sample_tool_color
,
850 IMAGE_OT_select_color
,
851 IMAGE_PT_color_palette
,
852 VIEW3D_PT_color_palette
,
853 VIEW3D_OT_select_weight
,
854 VIEW3D_OT_reset_weight_palette
,
855 VIEW3D_PT_weight_palette
,
863 bpy
.utils
.register_class(cls
)
865 bpy
.types
.Scene
.palette_props
= PointerProperty(
867 name
="Palette Props",
873 for cls
in reversed(classes
):
874 bpy
.utils
.unregister_class(cls
)
876 del bpy
.types
.Scene
.palette_props
879 if __name__
== "__main__":