From 5e6515149f47b2aae9a356eb2744170d8b3acbb8 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Thu, 28 Feb 2019 16:34:05 +1100 Subject: object_color_rules: update for 2.8x Add to add-ons since color objects/wireframe is now supported. --- object_color_rules.py | 478 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 478 insertions(+) create mode 100644 object_color_rules.py (limited to 'object_color_rules.py') diff --git a/object_color_rules.py b/object_color_rules.py new file mode 100644 index 00000000..dfa835aa --- /dev/null +++ b/object_color_rules.py @@ -0,0 +1,478 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +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).", + "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 loosing 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 = 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" + + 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 -- cgit v1.2.3