# SPDX-License-Identifier: GPL-2.0-or-later bl_info = { "name": "Object Color Rules", "author": "Campbell Barton", "version": (0, 0, 2), "blender": (2, 80, 0), "location": "Properties > Object Buttons", "description": "Rules for assigning object color (for object & wireframe colors).", "doc_url": "{BLENDER_MANUAL_URL}/addons/object/color_rules.html", "category": "Object", } def test_name(rule, needle, haystack, cache): if rule.use_match_regex: if not cache: import re re_needle = re.compile(needle) cache[:] = [re_needle] else: re_needle = cache[0] return (re_needle.match(haystack) is not None) else: return (needle in haystack) class rule_test: __slots__ = () def __new__(cls, *args, **kwargs): raise RuntimeError("%s should not be instantiated" % cls) @staticmethod def NAME(obj, rule, cache): match_name = rule.match_name return test_name(rule, match_name, obj.name, cache) def DATA(obj, rule, cache): match_name = rule.match_name obj_data = obj.data if obj_data is not None: return test_name(rule, match_name, obj_data.name, cache) else: return False @staticmethod def COLLECTION(obj, rule, cache): if not cache: match_name = rule.match_name objects = {o for g in bpy.data.collections if test_name(rule, match_name, g.name, cache) for o in g.objects} cache["objects"] = objects else: objects = cache["objects"] return obj in objects @staticmethod def MATERIAL(obj, rule, cache): match_name = rule.match_name materials = getattr(obj.data, "materials", None) return ((materials is not None) and (any((test_name(rule, match_name, m.name) for m in materials if m is not None)))) @staticmethod def TYPE(obj, rule, cache): return (obj.type == rule.match_object_type) @staticmethod def EXPR(obj, rule, cache): if not cache: match_expr = rule.match_expr expr = compile(match_expr, rule.name, 'eval') namespace = {} namespace.update(__import__("math").__dict__) cache["expr"] = expr cache["namespace"] = namespace else: expr = cache["expr"] namespace = cache["namespace"] try: return bool(eval(expr, {}, {"self": obj})) except: import traceback traceback.print_exc() return False class rule_draw: __slots__ = () def __new__(cls, *args, **kwargs): raise RuntimeError("%s should not be instantiated" % cls) @staticmethod def _generic_match_name(layout, rule): layout.label(text="Match Name:") row = layout.row(align=True) row.prop(rule, "match_name", text="") row.prop(rule, "use_match_regex", text="", icon='SORTALPHA') @staticmethod def NAME(layout, rule): rule_draw._generic_match_name(layout, rule) @staticmethod def DATA(layout, rule): rule_draw._generic_match_name(layout, rule) @staticmethod def COLLECTION(layout, rule): rule_draw._generic_match_name(layout, rule) @staticmethod def MATERIAL(layout, rule): rule_draw._generic_match_name(layout, rule) @staticmethod def TYPE(layout, rule): row = layout.row() row.prop(rule, "match_object_type") @staticmethod def EXPR(layout, rule): col = layout.column() col.label(text="Scripted Expression:") col.prop(rule, "match_expr", text="") def object_colors_calc(rules, objects): from mathutils import Color rules_cb = [getattr(rule_test, rule.type) for rule in rules] rules_blend = [(1.0 - rule.factor, rule.factor) for rule in rules] rules_color = [Color(rule.color) for rule in rules] rules_cache = [{} for i in range(len(rules))] rules_inv = [rule.use_invert for rule in rules] changed_count = 0 for obj in objects: is_set = False obj_color = Color(obj.color[0:3]) for (rule, test_cb, color, blend, cache, use_invert) \ in zip(rules, rules_cb, rules_color, rules_blend, rules_cache, rules_inv): if test_cb(obj, rule, cache) is not use_invert: if is_set is False: obj_color = color else: # prevent mixing colors losing saturation obj_color_s = obj_color.s obj_color = (obj_color * blend[0]) + (color * blend[1]) obj_color.s = (obj_color_s * blend[0]) + (color.s * blend[1]) is_set = True if is_set: obj.color[0:3] = obj_color changed_count += 1 return changed_count def object_colors_select(rule, objects): cache = {} rule_type = rule.type test_cb = getattr(rule_test, rule_type) for obj in objects: obj.select_set(test_cb(obj, rule, cache)) def object_colors_rule_validate(rule, report): rule_type = rule.type if rule_type in {'NAME', 'DATA', 'COLLECTION', 'MATERIAL'}: if rule.use_match_regex: import re try: re.compile(rule.match_name) except Exception as e: report({'ERROR'}, "Rule %r: %s" % (rule.name, str(e))) return False elif rule_type == 'EXPR': try: compile(rule.match_expr, rule.name, 'eval') except Exception as e: report({'ERROR'}, "Rule %r: %s" % (rule.name, str(e))) return False return True import bpy from bpy.types import ( Operator, Panel, UIList, ) from bpy.props import ( StringProperty, BoolProperty, IntProperty, FloatProperty, EnumProperty, CollectionProperty, BoolVectorProperty, FloatVectorProperty, ) class OBJECT_PT_color_rules(Panel): bl_label = "Color Rules" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "object" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout scene = context.scene # Rig type list row = layout.row() row.template_list( "OBJECT_UL_color_rule", "color_rules", scene, "color_rules", scene, "color_rules_active_index", ) col = row.column() colsub = col.column(align=True) colsub.operator("object.color_rules_add", icon='ADD', text="") colsub.operator("object.color_rules_remove", icon='REMOVE', text="") colsub = col.column(align=True) colsub.operator("object.color_rules_move", text="", icon='TRIA_UP').direction = -1 colsub.operator("object.color_rules_move", text="", icon='TRIA_DOWN').direction = 1 colsub = col.column(align=True) colsub.operator("object.color_rules_select", text="", icon='RESTRICT_SELECT_OFF') if scene.color_rules: index = scene.color_rules_active_index rule = scene.color_rules[index] box = layout.box() row = box.row(align=True) row.prop(rule, "name", text="") row.prop(rule, "type", text="") row.prop(rule, "use_invert", text="", icon='ARROW_LEFTRIGHT') draw_cb = getattr(rule_draw, rule.type) draw_cb(box, rule) row = layout.split(factor=0.75, align=True) props = row.operator("object.color_rules_assign", text="Assign Selected") props.use_selection = True props = row.operator("object.color_rules_assign", text="All") props.use_selection = False class OBJECT_UL_color_rule(UIList): def draw_item(self, context, layout, data, rule, icon, active_data, active_propname, index): # assert(isinstance(rule, bpy.types.ShapeKey)) # scene = active_data split = layout.split(factor=0.5) row = split.split(align=False) row.label(text="%s (%s)" % (rule.name, rule.type.lower())) split = split.split(factor=0.7) split.prop(rule, "factor", text="", emboss=False) split.prop(rule, "color", text="") class OBJECT_OT_color_rules_assign(Operator): """Assign colors to objects based on user rules""" bl_idname = "object.color_rules_assign" bl_label = "Assign Colors" bl_options = {'UNDO'} use_selection: BoolProperty( name="Selected", description="Apply to selected (otherwise all objects in the scene)", default=True, ) def execute(self, context): scene = context.scene if self.use_selection: objects = context.selected_editable_objects else: objects = scene.objects rules = scene.color_rules[:] for rule in rules: if not object_colors_rule_validate(rule, self.report): return {'CANCELLED'} changed_count = object_colors_calc(rules, objects) self.report({'INFO'}, "Set colors for {} of {} objects".format(changed_count, len(objects))) return {'FINISHED'} class OBJECT_OT_color_rules_select(Operator): """Select objects matching the current rule""" bl_idname = "object.color_rules_select" bl_label = "Select Rule" bl_options = {'UNDO'} def execute(self, context): scene = context.scene rule = scene.color_rules[scene.color_rules_active_index] if not object_colors_rule_validate(rule, self.report): return {'CANCELLED'} objects = context.visible_objects object_colors_select(rule, objects) return {'FINISHED'} class OBJECT_OT_color_rules_add(Operator): bl_idname = "object.color_rules_add" bl_label = "Add Color Layer" bl_options = {'UNDO'} def execute(self, context): scene = context.scene rules = scene.color_rules rule = rules.add() rule.name = "Rule.%.3d" % len(rules) scene.color_rules_active_index = len(rules) - 1 return {'FINISHED'} class OBJECT_OT_color_rules_remove(Operator): bl_idname = "object.color_rules_remove" bl_label = "Remove Color Layer" bl_options = {'UNDO'} def execute(self, context): scene = context.scene rules = scene.color_rules rules.remove(scene.color_rules_active_index) if scene.color_rules_active_index > len(rules) - 1: scene.color_rules_active_index = len(rules) - 1 return {'FINISHED'} class OBJECT_OT_color_rules_move(Operator): bl_idname = "object.color_rules_move" bl_label = "Remove Color Layer" bl_options = {'UNDO'} direction: IntProperty() def execute(self, context): scene = context.scene rules = scene.color_rules index = scene.color_rules_active_index index_new = index + self.direction if index_new < len(rules) and index_new >= 0: rules.move(index, index_new) scene.color_rules_active_index = index_new return {'FINISHED'} else: return {'CANCELLED'} class ColorRule(bpy.types.PropertyGroup): name: StringProperty( name="Rule Name", ) color: FloatVectorProperty( name="Color", description="Color to assign", subtype='COLOR', size=3, min=0, max=1, precision=3, step=0.1, default=(0.5, 0.5, 0.5), ) factor: FloatProperty( name="Opacity", description="Color to assign", min=0, max=1, precision=1, step=0.1, default=1.0, ) type: EnumProperty( name="Rule Type", items=( ('NAME', "Name", "Object name contains this text (or matches regex)"), ('DATA', "Data Name", "Object data name contains this text (or matches regex)"), ('COLLECTION', "Collection Name", "Object in collection that contains this text (or matches regex)"), ('MATERIAL', "Material Name", "Object uses a material name that contains this text (or matches regex)"), ('TYPE', "Type", "Object type"), ('EXPR', "Expression", ( "Scripted expression (using 'self' for the object) eg:\n" " self.type == 'MESH' and len(self.data.vertices) > 20" ) ), ), ) use_invert: BoolProperty( name="Invert", description="Match when the rule isn't met", ) # ------------------ # Matching Variables # shared by all name matching match_name: StringProperty( name="Match Name", ) use_match_regex: BoolProperty( name="Regex", description="Use regular expressions for pattern matching", ) # type == 'TYPE' match_object_type: EnumProperty( name="Object Type", items=([(i.identifier, i.name, "") for i in bpy.types.Object.bl_rna.properties['type'].enum_items] ) ) # type == 'EXPR' match_expr: StringProperty( name="Expression", description="Python expression, where 'self' is the object variable" ) classes = ( OBJECT_PT_color_rules, OBJECT_OT_color_rules_add, OBJECT_OT_color_rules_remove, OBJECT_OT_color_rules_move, OBJECT_OT_color_rules_assign, OBJECT_OT_color_rules_select, OBJECT_UL_color_rule, ColorRule, ) def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.Scene.color_rules = CollectionProperty(type=ColorRule) bpy.types.Scene.color_rules_active_index = IntProperty() def unregister(): for cls in classes: bpy.utils.unregister_class(cls) del bpy.types.Scene.color_rules