1 # ***** BEGIN GPL LICENSE BLOCK *****
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.
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.
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.
17 # ***** END GPL LICENCE BLOCK *****
20 "name": "Object Color Rules",
21 "author": "Campbell Barton",
23 "blender": (2, 80, 0),
24 "location": "Properties > Object Buttons",
25 "description": "Rules for assigning object color (for object & wireframe colors).",
26 "doc_url": "{BLENDER_MANUAL_URL}/addons/object/color_rules.html",
31 def test_name(rule
, needle
, haystack
, cache
):
32 if rule
.use_match_regex
:
35 re_needle
= re
.compile(needle
)
36 cache
[:] = [re_needle
]
39 return (re_needle
.match(haystack
) is not None)
41 return (needle
in haystack
)
47 def __new__(cls
, *args
, **kwargs
):
48 raise RuntimeError("%s should not be instantiated" % cls
)
51 def NAME(obj
, rule
, cache
):
52 match_name
= rule
.match_name
53 return test_name(rule
, match_name
, obj
.name
, cache
)
55 def DATA(obj
, rule
, cache
):
56 match_name
= rule
.match_name
58 if obj_data
is not None:
59 return test_name(rule
, match_name
, obj_data
.name
, cache
)
64 def COLLECTION(obj
, rule
, cache
):
66 match_name
= rule
.match_name
67 objects
= {o
for g
in bpy
.data
.collections
if test_name(rule
, match_name
, g
.name
, cache
) for o
in g
.objects
}
68 cache
["objects"] = objects
70 objects
= cache
["objects"]
75 def MATERIAL(obj
, rule
, cache
):
76 match_name
= rule
.match_name
77 materials
= getattr(obj
.data
, "materials", None)
79 return ((materials
is not None) and
80 (any((test_name(rule
, match_name
, m
.name
) for m
in materials
if m
is not None))))
83 def TYPE(obj
, rule
, cache
):
84 return (obj
.type == rule
.match_object_type
)
87 def EXPR(obj
, rule
, cache
):
89 match_expr
= rule
.match_expr
90 expr
= compile(match_expr
, rule
.name
, 'eval')
93 namespace
.update(__import__("math").__dict
__)
96 cache
["namespace"] = namespace
99 namespace
= cache
["namespace"]
102 return bool(eval(expr
, {}, {"self": obj
}))
105 traceback
.print_exc()
112 def __new__(cls
, *args
, **kwargs
):
113 raise RuntimeError("%s should not be instantiated" % cls
)
116 def _generic_match_name(layout
, rule
):
117 layout
.label(text
="Match Name:")
118 row
= layout
.row(align
=True)
119 row
.prop(rule
, "match_name", text
="")
120 row
.prop(rule
, "use_match_regex", text
="", icon
='SORTALPHA')
123 def NAME(layout
, rule
):
124 rule_draw
._generic
_match
_name
(layout
, rule
)
127 def DATA(layout
, rule
):
128 rule_draw
._generic
_match
_name
(layout
, rule
)
131 def COLLECTION(layout
, rule
):
132 rule_draw
._generic
_match
_name
(layout
, rule
)
135 def MATERIAL(layout
, rule
):
136 rule_draw
._generic
_match
_name
(layout
, rule
)
139 def TYPE(layout
, rule
):
141 row
.prop(rule
, "match_object_type")
144 def EXPR(layout
, rule
):
145 col
= layout
.column()
146 col
.label(text
="Scripted Expression:")
147 col
.prop(rule
, "match_expr", text
="")
150 def object_colors_calc(rules
, objects
):
151 from mathutils
import Color
153 rules_cb
= [getattr(rule_test
, rule
.type) for rule
in rules
]
154 rules_blend
= [(1.0 - rule
.factor
, rule
.factor
) for rule
in rules
]
155 rules_color
= [Color(rule
.color
) for rule
in rules
]
156 rules_cache
= [{} for i
in range(len(rules
))]
157 rules_inv
= [rule
.use_invert
for rule
in rules
]
162 obj_color
= Color(obj
.color
[0:3])
164 for (rule
, test_cb
, color
, blend
, cache
, use_invert
) \
165 in zip(rules
, rules_cb
, rules_color
, rules_blend
, rules_cache
, rules_inv
):
167 if test_cb(obj
, rule
, cache
) is not use_invert
:
171 # prevent mixing colors losing saturation
172 obj_color_s
= obj_color
.s
173 obj_color
= (obj_color
* blend
[0]) + (color
* blend
[1])
174 obj_color
.s
= (obj_color_s
* blend
[0]) + (color
.s
* blend
[1])
179 obj
.color
[0:3] = obj_color
184 def object_colors_select(rule
, objects
):
187 rule_type
= rule
.type
188 test_cb
= getattr(rule_test
, rule_type
)
191 obj
.select
= test_cb(obj
, rule
, cache
)
194 def object_colors_rule_validate(rule
, report
):
195 rule_type
= rule
.type
197 if rule_type
in {'NAME', 'DATA', 'COLLECTION', 'MATERIAL'}:
198 if rule
.use_match_regex
:
201 re
.compile(rule
.match_name
)
202 except Exception as e
:
203 report({'ERROR'}, "Rule %r: %s" % (rule
.name
, str(e
)))
206 elif rule_type
== 'EXPR':
208 compile(rule
.match_expr
, rule
.name
, 'eval')
209 except Exception as e
:
210 report({'ERROR'}, "Rule %r: %s" % (rule
.name
, str(e
)))
218 from bpy
.types
import (
223 from bpy
.props
import (
235 class OBJECT_PT_color_rules(Panel
):
236 bl_label
= "Color Rules"
237 bl_space_type
= 'PROPERTIES'
238 bl_region_type
= 'WINDOW'
239 bl_context
= "object"
240 bl_options
= {'DEFAULT_CLOSED'}
242 def draw(self
, context
):
245 scene
= context
.scene
250 "OBJECT_UL_color_rule", "color_rules",
251 scene
, "color_rules",
252 scene
, "color_rules_active_index",
256 colsub
= col
.column(align
=True)
257 colsub
.operator("object.color_rules_add", icon
='ADD', text
="")
258 colsub
.operator("object.color_rules_remove", icon
='REMOVE', text
="")
260 colsub
= col
.column(align
=True)
261 colsub
.operator("object.color_rules_move", text
="", icon
='TRIA_UP').direction
= -1
262 colsub
.operator("object.color_rules_move", text
="", icon
='TRIA_DOWN').direction
= 1
264 colsub
= col
.column(align
=True)
265 colsub
.operator("object.color_rules_select", text
="", icon
='RESTRICT_SELECT_OFF')
267 if scene
.color_rules
:
268 index
= scene
.color_rules_active_index
269 rule
= scene
.color_rules
[index
]
272 row
= box
.row(align
=True)
273 row
.prop(rule
, "name", text
="")
274 row
.prop(rule
, "type", text
="")
275 row
.prop(rule
, "use_invert", text
="", icon
='ARROW_LEFTRIGHT')
277 draw_cb
= getattr(rule_draw
, rule
.type)
280 row
= layout
.split(factor
=0.75, align
=True)
281 props
= row
.operator("object.color_rules_assign", text
="Assign Selected")
282 props
.use_selection
= True
283 props
= row
.operator("object.color_rules_assign", text
="All")
284 props
.use_selection
= False
287 class OBJECT_UL_color_rule(UIList
):
288 def draw_item(self
, context
, layout
, data
, rule
, icon
, active_data
, active_propname
, index
):
289 # assert(isinstance(rule, bpy.types.ShapeKey))
290 # scene = active_data
291 split
= layout
.split(factor
=0.5)
292 row
= split
.split(align
=False)
293 row
.label(text
="%s (%s)" % (rule
.name
, rule
.type.lower()))
294 split
= split
.split(factor
=0.7)
295 split
.prop(rule
, "factor", text
="", emboss
=False)
296 split
.prop(rule
, "color", text
="")
299 class OBJECT_OT_color_rules_assign(Operator
):
300 """Assign colors to objects based on user rules"""
301 bl_idname
= "object.color_rules_assign"
302 bl_label
= "Assign Colors"
303 bl_options
= {'UNDO'}
305 use_selection
: BoolProperty(
307 description
="Apply to selected (otherwise all objects in the scene)",
311 def execute(self
, context
):
312 scene
= context
.scene
314 if self
.use_selection
:
315 objects
= context
.selected_editable_objects
317 objects
= scene
.objects
319 rules
= scene
.color_rules
[:]
321 if not object_colors_rule_validate(rule
, self
.report
):
324 changed_count
= object_colors_calc(rules
, objects
)
325 self
.report({'INFO'}, "Set colors for {} of {} objects".format(changed_count
, len(objects
)))
329 class OBJECT_OT_color_rules_select(Operator
):
330 """Select objects matching the current rule"""
331 bl_idname
= "object.color_rules_select"
332 bl_label
= "Select Rule"
333 bl_options
= {'UNDO'}
335 def execute(self
, context
):
336 scene
= context
.scene
337 rule
= scene
.color_rules
[scene
.color_rules_active_index
]
339 if not object_colors_rule_validate(rule
, self
.report
):
342 objects
= context
.visible_objects
343 object_colors_select(rule
, objects
)
347 class OBJECT_OT_color_rules_add(Operator
):
348 bl_idname
= "object.color_rules_add"
349 bl_label
= "Add Color Layer"
350 bl_options
= {'UNDO'}
352 def execute(self
, context
):
353 scene
= context
.scene
354 rules
= scene
.color_rules
356 rule
.name
= "Rule.%.3d" % len(rules
)
357 scene
.color_rules_active_index
= len(rules
) - 1
361 class OBJECT_OT_color_rules_remove(Operator
):
362 bl_idname
= "object.color_rules_remove"
363 bl_label
= "Remove Color Layer"
364 bl_options
= {'UNDO'}
366 def execute(self
, context
):
367 scene
= context
.scene
368 rules
= scene
.color_rules
369 rules
.remove(scene
.color_rules_active_index
)
370 if scene
.color_rules_active_index
> len(rules
) - 1:
371 scene
.color_rules_active_index
= len(rules
) - 1
375 class OBJECT_OT_color_rules_move(Operator
):
376 bl_idname
= "object.color_rules_move"
377 bl_label
= "Remove Color Layer"
378 bl_options
= {'UNDO'}
379 direction
: IntProperty()
381 def execute(self
, context
):
382 scene
= context
.scene
383 rules
= scene
.color_rules
384 index
= scene
.color_rules_active_index
385 index_new
= index
+ self
.direction
386 if index_new
< len(rules
) and index_new
>= 0:
387 rules
.move(index
, index_new
)
388 scene
.color_rules_active_index
= index_new
394 class ColorRule(bpy
.types
.PropertyGroup
):
395 name
: StringProperty(
398 color
: FloatVectorProperty(
400 description
="Color to assign",
401 subtype
='COLOR', size
=3, min=0, max=1, precision
=3, step
=0.1,
402 default
=(0.5, 0.5, 0.5),
404 factor
: FloatProperty(
406 description
="Color to assign",
407 min=0, max=1, precision
=1, step
=0.1,
413 ('NAME', "Name", "Object name contains this text (or matches regex)"),
414 ('DATA', "Data Name", "Object data name contains this text (or matches regex)"),
415 ('COLLECTION', "Collection Name", "Object in collection that contains this text (or matches regex)"),
416 ('MATERIAL', "Material Name", "Object uses a material name that contains this text (or matches regex)"),
417 ('TYPE', "Type", "Object type"),
418 ('EXPR', "Expression", (
419 "Scripted expression (using 'self' for the object) eg:\n"
420 " self.type == 'MESH' and len(self.data.vertices) > 20"
426 use_invert
: BoolProperty(
428 description
="Match when the rule isn't met",
434 # shared by all name matching
435 match_name
: StringProperty(
438 use_match_regex
: BoolProperty(
440 description
="Use regular expressions for pattern matching",
443 match_object_type
: EnumProperty(
445 items
=([(i
.identifier
, i
.name
, "")
446 for i
in bpy
.types
.Object
.bl_rna
.properties
['type'].enum_items
]
450 match_expr
: StringProperty(
452 description
="Python expression, where 'self' is the object variable"
457 OBJECT_PT_color_rules
,
458 OBJECT_OT_color_rules_add
,
459 OBJECT_OT_color_rules_remove
,
460 OBJECT_OT_color_rules_move
,
461 OBJECT_OT_color_rules_assign
,
462 OBJECT_OT_color_rules_select
,
463 OBJECT_UL_color_rule
,
470 bpy
.utils
.register_class(cls
)
472 bpy
.types
.Scene
.color_rules
= CollectionProperty(type=ColorRule
)
473 bpy
.types
.Scene
.color_rules_active_index
= IntProperty()
478 bpy
.utils
.unregister_class(cls
)
480 del bpy
.types
.Scene
.color_rules