1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 "name": "Object Color Rules",
7 "author": "Campbell Barton",
10 "location": "Properties > Object Buttons",
11 "description": "Rules for assigning object color (for object & wireframe colors).",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/object/color_rules.html",
17 def test_name(rule
, needle
, haystack
, cache
):
18 if rule
.use_match_regex
:
21 re_needle
= re
.compile(needle
)
22 cache
[:] = [re_needle
]
25 return (re_needle
.match(haystack
) is not None)
27 return (needle
in haystack
)
33 def __new__(cls
, *args
, **kwargs
):
34 raise RuntimeError("%s should not be instantiated" % cls
)
37 def NAME(obj
, rule
, cache
):
38 match_name
= rule
.match_name
39 return test_name(rule
, match_name
, obj
.name
, cache
)
41 def DATA(obj
, rule
, cache
):
42 match_name
= rule
.match_name
44 if obj_data
is not None:
45 return test_name(rule
, match_name
, obj_data
.name
, cache
)
50 def COLLECTION(obj
, rule
, cache
):
52 match_name
= rule
.match_name
53 objects
= {o
for g
in bpy
.data
.collections
if test_name(rule
, match_name
, g
.name
, cache
) for o
in g
.objects
}
54 cache
["objects"] = objects
56 objects
= cache
["objects"]
61 def MATERIAL(obj
, rule
, cache
):
62 match_name
= rule
.match_name
63 materials
= getattr(obj
.data
, "materials", None)
65 return ((materials
is not None) and
66 (any((test_name(rule
, match_name
, m
.name
) for m
in materials
if m
is not None))))
69 def TYPE(obj
, rule
, cache
):
70 return (obj
.type == rule
.match_object_type
)
73 def EXPR(obj
, rule
, cache
):
75 match_expr
= rule
.match_expr
76 expr
= compile(match_expr
, rule
.name
, 'eval')
79 namespace
.update(__import__("math").__dict
__)
82 cache
["namespace"] = namespace
85 namespace
= cache
["namespace"]
88 return bool(eval(expr
, {}, {"self": obj
}))
98 def __new__(cls
, *args
, **kwargs
):
99 raise RuntimeError("%s should not be instantiated" % cls
)
102 def _generic_match_name(layout
, rule
):
103 layout
.label(text
="Match Name:")
104 row
= layout
.row(align
=True)
105 row
.prop(rule
, "match_name", text
="")
106 row
.prop(rule
, "use_match_regex", text
="", icon
='SORTALPHA')
109 def NAME(layout
, rule
):
110 rule_draw
._generic
_match
_name
(layout
, rule
)
113 def DATA(layout
, rule
):
114 rule_draw
._generic
_match
_name
(layout
, rule
)
117 def COLLECTION(layout
, rule
):
118 rule_draw
._generic
_match
_name
(layout
, rule
)
121 def MATERIAL(layout
, rule
):
122 rule_draw
._generic
_match
_name
(layout
, rule
)
125 def TYPE(layout
, rule
):
127 row
.prop(rule
, "match_object_type")
130 def EXPR(layout
, rule
):
131 col
= layout
.column()
132 col
.label(text
="Scripted Expression:")
133 col
.prop(rule
, "match_expr", text
="")
136 def object_colors_calc(rules
, objects
):
137 from mathutils
import Color
139 rules_cb
= [getattr(rule_test
, rule
.type) for rule
in rules
]
140 rules_blend
= [(1.0 - rule
.factor
, rule
.factor
) for rule
in rules
]
141 rules_color
= [Color(rule
.color
) for rule
in rules
]
142 rules_cache
= [{} for i
in range(len(rules
))]
143 rules_inv
= [rule
.use_invert
for rule
in rules
]
148 obj_color
= Color(obj
.color
[0:3])
150 for (rule
, test_cb
, color
, blend
, cache
, use_invert
) \
151 in zip(rules
, rules_cb
, rules_color
, rules_blend
, rules_cache
, rules_inv
):
153 if test_cb(obj
, rule
, cache
) is not use_invert
:
157 # prevent mixing colors losing saturation
158 obj_color_s
= obj_color
.s
159 obj_color
= (obj_color
* blend
[0]) + (color
* blend
[1])
160 obj_color
.s
= (obj_color_s
* blend
[0]) + (color
.s
* blend
[1])
165 obj
.color
[0:3] = obj_color
170 def object_colors_select(rule
, objects
):
173 rule_type
= rule
.type
174 test_cb
= getattr(rule_test
, rule_type
)
177 obj
.select_set(test_cb(obj
, rule
, cache
))
180 def object_colors_rule_validate(rule
, report
):
181 rule_type
= rule
.type
183 if rule_type
in {'NAME', 'DATA', 'COLLECTION', 'MATERIAL'}:
184 if rule
.use_match_regex
:
187 re
.compile(rule
.match_name
)
188 except Exception as e
:
189 report({'ERROR'}, "Rule %r: %s" % (rule
.name
, str(e
)))
192 elif rule_type
== 'EXPR':
194 compile(rule
.match_expr
, rule
.name
, 'eval')
195 except Exception as e
:
196 report({'ERROR'}, "Rule %r: %s" % (rule
.name
, str(e
)))
204 from bpy
.types
import (
209 from bpy
.props
import (
221 class OBJECT_PT_color_rules(Panel
):
222 bl_label
= "Color Rules"
223 bl_space_type
= 'PROPERTIES'
224 bl_region_type
= 'WINDOW'
225 bl_context
= "object"
226 bl_options
= {'DEFAULT_CLOSED'}
228 def draw(self
, context
):
231 scene
= context
.scene
236 "OBJECT_UL_color_rule", "color_rules",
237 scene
, "color_rules",
238 scene
, "color_rules_active_index",
242 colsub
= col
.column(align
=True)
243 colsub
.operator("object.color_rules_add", icon
='ADD', text
="")
244 colsub
.operator("object.color_rules_remove", icon
='REMOVE', text
="")
246 colsub
= col
.column(align
=True)
247 colsub
.operator("object.color_rules_move", text
="", icon
='TRIA_UP').direction
= -1
248 colsub
.operator("object.color_rules_move", text
="", icon
='TRIA_DOWN').direction
= 1
250 colsub
= col
.column(align
=True)
251 colsub
.operator("object.color_rules_select", text
="", icon
='RESTRICT_SELECT_OFF')
253 if scene
.color_rules
:
254 index
= scene
.color_rules_active_index
255 rule
= scene
.color_rules
[index
]
258 row
= box
.row(align
=True)
259 row
.prop(rule
, "name", text
="")
260 row
.prop(rule
, "type", text
="")
261 row
.prop(rule
, "use_invert", text
="", icon
='ARROW_LEFTRIGHT')
263 draw_cb
= getattr(rule_draw
, rule
.type)
266 row
= layout
.split(factor
=0.75, align
=True)
267 props
= row
.operator("object.color_rules_assign", text
="Assign Selected")
268 props
.use_selection
= True
269 props
= row
.operator("object.color_rules_assign", text
="All")
270 props
.use_selection
= False
273 class OBJECT_UL_color_rule(UIList
):
274 def draw_item(self
, context
, layout
, data
, rule
, icon
, active_data
, active_propname
, index
):
275 # assert(isinstance(rule, bpy.types.ShapeKey))
276 # scene = active_data
277 split
= layout
.split(factor
=0.5)
278 row
= split
.split(align
=False)
279 row
.label(text
="%s (%s)" % (rule
.name
, rule
.type.lower()))
280 split
= split
.split(factor
=0.7)
281 split
.prop(rule
, "factor", text
="", emboss
=False)
282 split
.prop(rule
, "color", text
="")
285 class OBJECT_OT_color_rules_assign(Operator
):
286 """Assign colors to objects based on user rules"""
287 bl_idname
= "object.color_rules_assign"
288 bl_label
= "Assign Colors"
289 bl_options
= {'UNDO'}
291 use_selection
: BoolProperty(
293 description
="Apply to selected (otherwise all objects in the scene)",
297 def execute(self
, context
):
298 scene
= context
.scene
300 if self
.use_selection
:
301 objects
= context
.selected_editable_objects
303 objects
= scene
.objects
305 rules
= scene
.color_rules
[:]
307 if not object_colors_rule_validate(rule
, self
.report
):
310 changed_count
= object_colors_calc(rules
, objects
)
311 self
.report({'INFO'}, "Set colors for {} of {} objects".format(changed_count
, len(objects
)))
315 class OBJECT_OT_color_rules_select(Operator
):
316 """Select objects matching the current rule"""
317 bl_idname
= "object.color_rules_select"
318 bl_label
= "Select Rule"
319 bl_options
= {'UNDO'}
321 def execute(self
, context
):
322 scene
= context
.scene
323 rule
= scene
.color_rules
[scene
.color_rules_active_index
]
325 if not object_colors_rule_validate(rule
, self
.report
):
328 objects
= context
.visible_objects
329 object_colors_select(rule
, objects
)
333 class OBJECT_OT_color_rules_add(Operator
):
334 bl_idname
= "object.color_rules_add"
335 bl_label
= "Add Color Layer"
336 bl_options
= {'UNDO'}
338 def execute(self
, context
):
339 scene
= context
.scene
340 rules
= scene
.color_rules
342 rule
.name
= "Rule.%.3d" % len(rules
)
343 scene
.color_rules_active_index
= len(rules
) - 1
347 class OBJECT_OT_color_rules_remove(Operator
):
348 bl_idname
= "object.color_rules_remove"
349 bl_label
= "Remove Color Layer"
350 bl_options
= {'UNDO'}
352 def execute(self
, context
):
353 scene
= context
.scene
354 rules
= scene
.color_rules
355 rules
.remove(scene
.color_rules_active_index
)
356 if scene
.color_rules_active_index
> len(rules
) - 1:
357 scene
.color_rules_active_index
= len(rules
) - 1
361 class OBJECT_OT_color_rules_move(Operator
):
362 bl_idname
= "object.color_rules_move"
363 bl_label
= "Remove Color Layer"
364 bl_options
= {'UNDO'}
365 direction
: IntProperty()
367 def execute(self
, context
):
368 scene
= context
.scene
369 rules
= scene
.color_rules
370 index
= scene
.color_rules_active_index
371 index_new
= index
+ self
.direction
372 if index_new
< len(rules
) and index_new
>= 0:
373 rules
.move(index
, index_new
)
374 scene
.color_rules_active_index
= index_new
380 class ColorRule(bpy
.types
.PropertyGroup
):
381 name
: StringProperty(
384 color
: FloatVectorProperty(
386 description
="Color to assign",
387 subtype
='COLOR', size
=3, min=0, max=1, precision
=3, step
=0.1,
388 default
=(0.5, 0.5, 0.5),
390 factor
: FloatProperty(
392 description
="Color to assign",
393 min=0, max=1, precision
=1, step
=0.1,
399 ('NAME', "Name", "Object name contains this text (or matches regex)"),
400 ('DATA', "Data Name", "Object data name contains this text (or matches regex)"),
401 ('COLLECTION', "Collection Name", "Object in collection that contains this text (or matches regex)"),
402 ('MATERIAL', "Material Name", "Object uses a material name that contains this text (or matches regex)"),
403 ('TYPE', "Type", "Object type"),
404 ('EXPR', "Expression", (
405 "Scripted expression (using 'self' for the object) eg:\n"
406 " self.type == 'MESH' and len(self.data.vertices) > 20"
412 use_invert
: BoolProperty(
414 description
="Match when the rule isn't met",
420 # shared by all name matching
421 match_name
: StringProperty(
424 use_match_regex
: BoolProperty(
426 description
="Use regular expressions for pattern matching",
429 match_object_type
: EnumProperty(
431 items
=([(i
.identifier
, i
.name
, "")
432 for i
in bpy
.types
.Object
.bl_rna
.properties
['type'].enum_items
]
436 match_expr
: StringProperty(
438 description
="Python expression, where 'self' is the object variable"
443 OBJECT_PT_color_rules
,
444 OBJECT_OT_color_rules_add
,
445 OBJECT_OT_color_rules_remove
,
446 OBJECT_OT_color_rules_move
,
447 OBJECT_OT_color_rules_assign
,
448 OBJECT_OT_color_rules_select
,
449 OBJECT_UL_color_rule
,
456 bpy
.utils
.register_class(cls
)
458 bpy
.types
.Scene
.color_rules
= CollectionProperty(type=ColorRule
)
459 bpy
.types
.Scene
.color_rules_active_index
= IntProperty()
464 bpy
.utils
.unregister_class(cls
)
466 del bpy
.types
.Scene
.color_rules