1 # SPDX-License-Identifier: GPL-2.0-or-later
4 "name": "Object Color Rules",
5 "author": "Campbell Barton",
8 "location": "Properties > Object Buttons",
9 "description": "Rules for assigning object color (for object & wireframe colors).",
10 "doc_url": "{BLENDER_MANUAL_URL}/addons/object/color_rules.html",
15 def test_name(rule
, needle
, haystack
, cache
):
16 if rule
.use_match_regex
:
19 re_needle
= re
.compile(needle
)
20 cache
[:] = [re_needle
]
23 return (re_needle
.match(haystack
) is not None)
25 return (needle
in haystack
)
31 def __new__(cls
, *args
, **kwargs
):
32 raise RuntimeError("%s should not be instantiated" % cls
)
35 def NAME(obj
, rule
, cache
):
36 match_name
= rule
.match_name
37 return test_name(rule
, match_name
, obj
.name
, cache
)
39 def DATA(obj
, rule
, cache
):
40 match_name
= rule
.match_name
42 if obj_data
is not None:
43 return test_name(rule
, match_name
, obj_data
.name
, cache
)
48 def COLLECTION(obj
, rule
, cache
):
50 match_name
= rule
.match_name
51 objects
= {o
for g
in bpy
.data
.collections
if test_name(rule
, match_name
, g
.name
, cache
) for o
in g
.objects
}
52 cache
["objects"] = objects
54 objects
= cache
["objects"]
59 def MATERIAL(obj
, rule
, cache
):
60 match_name
= rule
.match_name
61 materials
= getattr(obj
.data
, "materials", None)
63 return ((materials
is not None) and
64 (any((test_name(rule
, match_name
, m
.name
) for m
in materials
if m
is not None))))
67 def TYPE(obj
, rule
, cache
):
68 return (obj
.type == rule
.match_object_type
)
71 def EXPR(obj
, rule
, cache
):
73 match_expr
= rule
.match_expr
74 expr
= compile(match_expr
, rule
.name
, 'eval')
77 namespace
.update(__import__("math").__dict
__)
80 cache
["namespace"] = namespace
83 namespace
= cache
["namespace"]
86 return bool(eval(expr
, {}, {"self": obj
}))
96 def __new__(cls
, *args
, **kwargs
):
97 raise RuntimeError("%s should not be instantiated" % cls
)
100 def _generic_match_name(layout
, rule
):
101 layout
.label(text
="Match Name:")
102 row
= layout
.row(align
=True)
103 row
.prop(rule
, "match_name", text
="")
104 row
.prop(rule
, "use_match_regex", text
="", icon
='SORTALPHA')
107 def NAME(layout
, rule
):
108 rule_draw
._generic
_match
_name
(layout
, rule
)
111 def DATA(layout
, rule
):
112 rule_draw
._generic
_match
_name
(layout
, rule
)
115 def COLLECTION(layout
, rule
):
116 rule_draw
._generic
_match
_name
(layout
, rule
)
119 def MATERIAL(layout
, rule
):
120 rule_draw
._generic
_match
_name
(layout
, rule
)
123 def TYPE(layout
, rule
):
125 row
.prop(rule
, "match_object_type")
128 def EXPR(layout
, rule
):
129 col
= layout
.column()
130 col
.label(text
="Scripted Expression:")
131 col
.prop(rule
, "match_expr", text
="")
134 def object_colors_calc(rules
, objects
):
135 from mathutils
import Color
137 rules_cb
= [getattr(rule_test
, rule
.type) for rule
in rules
]
138 rules_blend
= [(1.0 - rule
.factor
, rule
.factor
) for rule
in rules
]
139 rules_color
= [Color(rule
.color
) for rule
in rules
]
140 rules_cache
= [{} for i
in range(len(rules
))]
141 rules_inv
= [rule
.use_invert
for rule
in rules
]
146 obj_color
= Color(obj
.color
[0:3])
148 for (rule
, test_cb
, color
, blend
, cache
, use_invert
) \
149 in zip(rules
, rules_cb
, rules_color
, rules_blend
, rules_cache
, rules_inv
):
151 if test_cb(obj
, rule
, cache
) is not use_invert
:
155 # prevent mixing colors losing saturation
156 obj_color_s
= obj_color
.s
157 obj_color
= (obj_color
* blend
[0]) + (color
* blend
[1])
158 obj_color
.s
= (obj_color_s
* blend
[0]) + (color
.s
* blend
[1])
163 obj
.color
[0:3] = obj_color
168 def object_colors_select(rule
, objects
):
171 rule_type
= rule
.type
172 test_cb
= getattr(rule_test
, rule_type
)
175 obj
.select
= test_cb(obj
, rule
, cache
)
178 def object_colors_rule_validate(rule
, report
):
179 rule_type
= rule
.type
181 if rule_type
in {'NAME', 'DATA', 'COLLECTION', 'MATERIAL'}:
182 if rule
.use_match_regex
:
185 re
.compile(rule
.match_name
)
186 except Exception as e
:
187 report({'ERROR'}, "Rule %r: %s" % (rule
.name
, str(e
)))
190 elif rule_type
== 'EXPR':
192 compile(rule
.match_expr
, rule
.name
, 'eval')
193 except Exception as e
:
194 report({'ERROR'}, "Rule %r: %s" % (rule
.name
, str(e
)))
202 from bpy
.types
import (
207 from bpy
.props
import (
219 class OBJECT_PT_color_rules(Panel
):
220 bl_label
= "Color Rules"
221 bl_space_type
= 'PROPERTIES'
222 bl_region_type
= 'WINDOW'
223 bl_context
= "object"
224 bl_options
= {'DEFAULT_CLOSED'}
226 def draw(self
, context
):
229 scene
= context
.scene
234 "OBJECT_UL_color_rule", "color_rules",
235 scene
, "color_rules",
236 scene
, "color_rules_active_index",
240 colsub
= col
.column(align
=True)
241 colsub
.operator("object.color_rules_add", icon
='ADD', text
="")
242 colsub
.operator("object.color_rules_remove", icon
='REMOVE', text
="")
244 colsub
= col
.column(align
=True)
245 colsub
.operator("object.color_rules_move", text
="", icon
='TRIA_UP').direction
= -1
246 colsub
.operator("object.color_rules_move", text
="", icon
='TRIA_DOWN').direction
= 1
248 colsub
= col
.column(align
=True)
249 colsub
.operator("object.color_rules_select", text
="", icon
='RESTRICT_SELECT_OFF')
251 if scene
.color_rules
:
252 index
= scene
.color_rules_active_index
253 rule
= scene
.color_rules
[index
]
256 row
= box
.row(align
=True)
257 row
.prop(rule
, "name", text
="")
258 row
.prop(rule
, "type", text
="")
259 row
.prop(rule
, "use_invert", text
="", icon
='ARROW_LEFTRIGHT')
261 draw_cb
= getattr(rule_draw
, rule
.type)
264 row
= layout
.split(factor
=0.75, align
=True)
265 props
= row
.operator("object.color_rules_assign", text
="Assign Selected")
266 props
.use_selection
= True
267 props
= row
.operator("object.color_rules_assign", text
="All")
268 props
.use_selection
= False
271 class OBJECT_UL_color_rule(UIList
):
272 def draw_item(self
, context
, layout
, data
, rule
, icon
, active_data
, active_propname
, index
):
273 # assert(isinstance(rule, bpy.types.ShapeKey))
274 # scene = active_data
275 split
= layout
.split(factor
=0.5)
276 row
= split
.split(align
=False)
277 row
.label(text
="%s (%s)" % (rule
.name
, rule
.type.lower()))
278 split
= split
.split(factor
=0.7)
279 split
.prop(rule
, "factor", text
="", emboss
=False)
280 split
.prop(rule
, "color", text
="")
283 class OBJECT_OT_color_rules_assign(Operator
):
284 """Assign colors to objects based on user rules"""
285 bl_idname
= "object.color_rules_assign"
286 bl_label
= "Assign Colors"
287 bl_options
= {'UNDO'}
289 use_selection
: BoolProperty(
291 description
="Apply to selected (otherwise all objects in the scene)",
295 def execute(self
, context
):
296 scene
= context
.scene
298 if self
.use_selection
:
299 objects
= context
.selected_editable_objects
301 objects
= scene
.objects
303 rules
= scene
.color_rules
[:]
305 if not object_colors_rule_validate(rule
, self
.report
):
308 changed_count
= object_colors_calc(rules
, objects
)
309 self
.report({'INFO'}, "Set colors for {} of {} objects".format(changed_count
, len(objects
)))
313 class OBJECT_OT_color_rules_select(Operator
):
314 """Select objects matching the current rule"""
315 bl_idname
= "object.color_rules_select"
316 bl_label
= "Select Rule"
317 bl_options
= {'UNDO'}
319 def execute(self
, context
):
320 scene
= context
.scene
321 rule
= scene
.color_rules
[scene
.color_rules_active_index
]
323 if not object_colors_rule_validate(rule
, self
.report
):
326 objects
= context
.visible_objects
327 object_colors_select(rule
, objects
)
331 class OBJECT_OT_color_rules_add(Operator
):
332 bl_idname
= "object.color_rules_add"
333 bl_label
= "Add Color Layer"
334 bl_options
= {'UNDO'}
336 def execute(self
, context
):
337 scene
= context
.scene
338 rules
= scene
.color_rules
340 rule
.name
= "Rule.%.3d" % len(rules
)
341 scene
.color_rules_active_index
= len(rules
) - 1
345 class OBJECT_OT_color_rules_remove(Operator
):
346 bl_idname
= "object.color_rules_remove"
347 bl_label
= "Remove Color Layer"
348 bl_options
= {'UNDO'}
350 def execute(self
, context
):
351 scene
= context
.scene
352 rules
= scene
.color_rules
353 rules
.remove(scene
.color_rules_active_index
)
354 if scene
.color_rules_active_index
> len(rules
) - 1:
355 scene
.color_rules_active_index
= len(rules
) - 1
359 class OBJECT_OT_color_rules_move(Operator
):
360 bl_idname
= "object.color_rules_move"
361 bl_label
= "Remove Color Layer"
362 bl_options
= {'UNDO'}
363 direction
: IntProperty()
365 def execute(self
, context
):
366 scene
= context
.scene
367 rules
= scene
.color_rules
368 index
= scene
.color_rules_active_index
369 index_new
= index
+ self
.direction
370 if index_new
< len(rules
) and index_new
>= 0:
371 rules
.move(index
, index_new
)
372 scene
.color_rules_active_index
= index_new
378 class ColorRule(bpy
.types
.PropertyGroup
):
379 name
: StringProperty(
382 color
: FloatVectorProperty(
384 description
="Color to assign",
385 subtype
='COLOR', size
=3, min=0, max=1, precision
=3, step
=0.1,
386 default
=(0.5, 0.5, 0.5),
388 factor
: FloatProperty(
390 description
="Color to assign",
391 min=0, max=1, precision
=1, step
=0.1,
397 ('NAME', "Name", "Object name contains this text (or matches regex)"),
398 ('DATA', "Data Name", "Object data name contains this text (or matches regex)"),
399 ('COLLECTION', "Collection Name", "Object in collection that contains this text (or matches regex)"),
400 ('MATERIAL', "Material Name", "Object uses a material name that contains this text (or matches regex)"),
401 ('TYPE', "Type", "Object type"),
402 ('EXPR', "Expression", (
403 "Scripted expression (using 'self' for the object) eg:\n"
404 " self.type == 'MESH' and len(self.data.vertices) > 20"
410 use_invert
: BoolProperty(
412 description
="Match when the rule isn't met",
418 # shared by all name matching
419 match_name
: StringProperty(
422 use_match_regex
: BoolProperty(
424 description
="Use regular expressions for pattern matching",
427 match_object_type
: EnumProperty(
429 items
=([(i
.identifier
, i
.name
, "")
430 for i
in bpy
.types
.Object
.bl_rna
.properties
['type'].enum_items
]
434 match_expr
: StringProperty(
436 description
="Python expression, where 'self' is the object variable"
441 OBJECT_PT_color_rules
,
442 OBJECT_OT_color_rules_add
,
443 OBJECT_OT_color_rules_remove
,
444 OBJECT_OT_color_rules_move
,
445 OBJECT_OT_color_rules_assign
,
446 OBJECT_OT_color_rules_select
,
447 OBJECT_UL_color_rule
,
454 bpy
.utils
.register_class(cls
)
456 bpy
.types
.Scene
.color_rules
= CollectionProperty(type=ColorRule
)
457 bpy
.types
.Scene
.color_rules_active_index
= IntProperty()
462 bpy
.utils
.unregister_class(cls
)
464 del bpy
.types
.Scene
.color_rules