1import bpy
2
3from bpy.types import Operator
4from bpy.props import (
5    StringProperty,
6    BoolProperty,
7    EnumProperty,
8    IntProperty,
9    FloatProperty
10    )
11
12
13from .enum_values import *
14from .functions import *
15
16from math import radians
17
18# -----------------------------------------------------------------------------
19# operator classes
20
21class VIEW3D_OT_materialutilities_assign_material_edit(bpy.types.Operator):
22    """Assign a material to the current selection"""
23
24    bl_idname = "view3d.materialutilities_assign_material_edit"
25    bl_label = "Assign Material (Material Utilities)"
26    bl_options = {'REGISTER', 'UNDO'}
27
28    material_name: StringProperty(
29            name = 'Material Name',
30            description = 'Name of Material to assign to current selection',
31            default = "",
32            maxlen = 63
33            )
34    new_material: BoolProperty(
35            name = '',
36            description = 'Add a new material, enter the name in the box',
37            default = False
38            )
39    show_dialog: BoolProperty(
40            name = 'Show Dialog',
41            default = False
42            )
43
44    @classmethod
45    def poll(cls, context):
46        return context.active_object is not None
47
48    def invoke(self, context, event):
49        if self.show_dialog:
50            return context.window_manager.invoke_props_dialog(self)
51        else:
52            return self.execute(context)
53
54    def draw(self, context):
55        layout = self.layout
56
57        col = layout.column()
58        row = col.split(factor = 0.9, align = True)
59
60        if self.new_material:
61            row.prop(self, "material_name")
62        else:
63            row.prop_search(self, "material_name", bpy.data, "materials")
64
65        row.prop(self, "new_material", expand = True, icon = 'ADD')
66
67    def execute(self, context):
68        material_name = self.material_name
69
70        if self.new_material:
71            material_name = mu_new_material_name(material_name)
72        elif material_name == "":
73            self.report({'WARNING'}, "No Material Name given!")
74            return {'CANCELLED'}
75
76        return mu_assign_material(self, material_name, 'APPEND_MATERIAL')
77
78
79class VIEW3D_OT_materialutilities_assign_material_object(bpy.types.Operator):
80    """Assign a material to the current selection
81    (See the operator panel [F9] for more options)"""
82
83    bl_idname = "view3d.materialutilities_assign_material_object"
84    bl_label = "Assign Material (Material Utilities)"
85    bl_options = {'REGISTER', 'UNDO'}
86
87    material_name: StringProperty(
88            name = 'Material Name',
89            description = 'Name of Material to assign to current selection',
90            default = "",
91            maxlen = 63
92            )
93    override_type: EnumProperty(
94            name = 'Assignment method',
95            description = '',
96            items = mu_override_type_enums
97            )
98    new_material: BoolProperty(
99            name = '',
100            description = 'Add a new material, enter the name in the box',
101            default = False
102            )
103    show_dialog: BoolProperty(
104            name = 'Show Dialog',
105            default = False
106            )
107
108    @classmethod
109    def poll(cls, context):
110        return len(context.selected_editable_objects) > 0
111
112    def invoke(self, context, event):
113        if self.show_dialog:
114            return context.window_manager.invoke_props_dialog(self)
115        else:
116            return self.execute(context)
117
118    def draw(self, context):
119        layout = self.layout
120
121        col = layout.column()
122        row = col.split(factor=0.9, align = True)
123
124        if self.new_material:
125            row.prop(self, "material_name")
126        else:
127            row.prop_search(self, "material_name", bpy.data, "materials")
128
129        row.prop(self, "new_material", expand = True, icon = 'ADD')
130
131        layout.prop(self, "override_type")
132
133
134    def execute(self, context):
135        material_name = self.material_name
136        override_type = self.override_type
137
138        if self.new_material:
139            material_name = mu_new_material_name(material_name)
140        elif material_name == "":
141            self.report({'WARNING'}, "No Material Name given!")
142            return {'CANCELLED'}
143
144        result = mu_assign_material(self, material_name, override_type)
145        return result
146
147class VIEW3D_OT_materialutilities_select_by_material_name(bpy.types.Operator):
148    """Select geometry that has the chosen material assigned to it
149    (See the operator panel [F9] for more options)"""
150
151    bl_idname = "view3d.materialutilities_select_by_material_name"
152    bl_label = "Select By Material Name (Material Utilities)"
153    bl_options = {'REGISTER', 'UNDO'}
154
155    extend_selection: BoolProperty(
156            name = 'Extend Selection',
157            description = 'Keeps the current selection and adds faces with the material to the selection'
158            )
159    material_name: StringProperty(
160            name = 'Material Name',
161            description = 'Name of Material to find and Select',
162            maxlen = 63
163            )
164    show_dialog: BoolProperty(
165            name = 'Show Dialog',
166            default = False
167    )
168
169    @classmethod
170    def poll(cls, context):
171        return len(context.visible_objects) > 0
172
173    def invoke(self, context, event):
174        if self.show_dialog:
175            return context.window_manager.invoke_props_dialog(self)
176        else:
177            return self.execute(context)
178
179    def draw(self, context):
180        layout = self.layout
181        layout.prop_search(self, "material_name", bpy.data, "materials")
182
183        layout.prop(self, "extend_selection", icon = "SELECT_EXTEND")
184
185    def execute(self, context):
186        material_name = self.material_name
187        ext = self.extend_selection
188        return mu_select_by_material_name(self, material_name, ext)
189
190
191class VIEW3D_OT_materialutilities_copy_material_to_others(bpy.types.Operator):
192    """Copy the material(s) of the active object to the other selected objects"""
193
194    bl_idname = "view3d.materialutilities_copy_material_to_others"
195    bl_label = "Copy material(s) to others (Material Utilities)"
196    bl_options = {'REGISTER', 'UNDO'}
197
198    @classmethod
199    def poll(cls, context):
200        return (context.active_object is not None) and (context.active_object.mode != 'EDIT')
201
202    def execute(self, context):
203        return mu_copy_material_to_others(self)
204
205
206class VIEW3D_OT_materialutilities_clean_material_slots(bpy.types.Operator):
207    """Removes any material slots from the selected objects that are not used"""
208
209    bl_idname = "view3d.materialutilities_clean_material_slots"
210    bl_label = "Clean Material Slots (Material Utilities)"
211    bl_options = {'REGISTER', 'UNDO'}
212
213    # affect: EnumProperty(
214    #         name = "Affect",
215    #         description = "Which objects material slots should be cleaned",
216    #         items = mu_clean_slots_enums,
217    #         default = 'ACTIVE'
218    #         )
219
220    only_active: BoolProperty(
221            name = 'Only active object',
222            description = 'Only remove the material slots for the active object ' +
223                            '(otherwise do it for every selected object)',
224            default = True
225            )
226
227    @classmethod
228    def poll(cls, context):
229        return len(context.selected_editable_objects) > 0
230
231    def draw(self, context):
232        layout = self.layout
233        layout.prop(self, "only_active", icon = "PIVOT_ACTIVE")
234
235    def execute(self, context):
236        affect = "ACTIVE" if self.only_active else "SELECTED"
237
238        return mu_cleanmatslots(self, affect)
239
240
241class VIEW3D_OT_materialutilities_remove_material_slot(bpy.types.Operator):
242    """Remove the active material slot from selected object(s)
243    (See the operator panel [F9] for more options)"""
244
245    bl_idname = "view3d.materialutilities_remove_material_slot"
246    bl_label = "Remove Active Material Slot (Material Utilities)"
247    bl_options = {'REGISTER', 'UNDO'}
248
249    only_active: BoolProperty(
250            name = 'Only active object',
251            description = 'Only remove the active material slot for the active object ' +
252                            '(otherwise do it for every selected object)',
253            default = True
254            )
255
256    @classmethod
257    def poll(cls, context):
258        return (context.active_object is not None) and (context.active_object.mode != 'EDIT')
259
260    def draw(self, context):
261        layout = self.layout
262        layout.prop(self, "only_active", icon = "PIVOT_ACTIVE")
263
264    def execute(self, context):
265        return mu_remove_material(self, self.only_active)
266
267class VIEW3D_OT_materialutilities_remove_all_material_slots(bpy.types.Operator):
268    """Remove all material slots from selected object(s)
269    (See the operator panel [F9] for more options)"""
270
271    bl_idname = "view3d.materialutilities_remove_all_material_slots"
272    bl_label = "Remove All Material Slots (Material Utilities)"
273    bl_options = {'REGISTER', 'UNDO'}
274
275    only_active: BoolProperty(
276            name = 'Only active object',
277            description = 'Only remove the material slots for the active object ' +
278                            '(otherwise do it for every selected object)',
279            default = True
280            )
281
282    @classmethod
283    def poll(cls, context):
284        return (context.active_object is not None) and (context.active_object.mode != 'EDIT')
285
286    def draw(self, context):
287        layout = self.layout
288        layout.prop(self, "only_active", icon = "PIVOT_ACTIVE")
289
290    def execute(self, context):
291        return mu_remove_all_materials(self, self.only_active)
292
293
294class VIEW3D_OT_materialutilities_replace_material(bpy.types.Operator):
295    """Replace a material by name"""
296    bl_idname = "view3d.materialutilities_replace_material"
297    bl_label = "Replace Material (Material Utilities)"
298    bl_options = {'REGISTER', 'UNDO'}
299
300    matorg: StringProperty(
301            name = "Original",
302            description = "Material to find and replace",
303            maxlen = 63,
304            )
305    matrep: StringProperty(name="Replacement",
306            description = "Material that will be used instead of the Original material",
307            maxlen = 63,
308            )
309    all_objects: BoolProperty(
310            name = "All Objects",
311            description = "Replace for all objects in this blend file (otherwise only selected objects)",
312            default = True,
313            )
314    update_selection: BoolProperty(
315            name = "Update Selection",
316            description = "Select affected objects and deselect unaffected",
317            default = True,
318            )
319
320    def draw(self, context):
321        layout = self.layout
322
323        layout.prop_search(self, "matorg", bpy.data, "materials")
324        layout.prop_search(self, "matrep", bpy.data, "materials")
325        layout.separator()
326
327        layout.prop(self, "all_objects", icon = "BLANK1")
328        layout.prop(self, "update_selection", icon = "SELECT_INTERSECT")
329
330    def invoke(self, context, event):
331        return context.window_manager.invoke_props_dialog(self)
332
333    def execute(self, context):
334        return mu_replace_material(self.matorg, self.matrep, self.all_objects, self.update_selection)
335
336
337class VIEW3D_OT_materialutilities_fake_user_set(bpy.types.Operator):
338    """Enable/disable fake user for materials"""
339
340    bl_idname = "view3d.materialutilities_fake_user_set"
341    bl_label = "Set Fake User (Material Utilities)"
342    bl_options = {'REGISTER', 'UNDO'}
343
344    fake_user: EnumProperty(
345            name = "Fake User",
346            description = "Turn fake user on or off",
347            items = mu_fake_user_set_enums,
348            default = 'TOGGLE'
349            )
350
351    affect: EnumProperty(
352            name = "Affect",
353            description = "Which materials of objects to affect",
354            items = mu_fake_user_affect_enums,
355            default = 'UNUSED'
356            )
357
358    @classmethod
359    def poll(cls, context):
360        return (context.active_object is not None)
361
362    def draw(self, context):
363        layout = self.layout
364        layout.prop(self, "fake_user", expand = True)
365        layout.separator()
366
367        layout.prop(self, "affect")
368
369    def invoke(self, context, event):
370        return context.window_manager.invoke_props_dialog(self)
371
372    def execute(self, context):
373        return mu_set_fake_user(self, self.fake_user, self.affect)
374
375
376class VIEW3D_OT_materialutilities_change_material_link(bpy.types.Operator):
377    """Link the materials to Data or Object, while keepng materials assigned"""
378
379    bl_idname = "view3d.materialutilities_change_material_link"
380    bl_label = "Change Material Linking (Material Utilities)"
381    bl_options = {'REGISTER', 'UNDO'}
382
383    override: BoolProperty(
384            name = "Override Data material",
385            description = "Override the materials assigned to the object data/mesh when switching to 'Linked to Data'\n" +
386                            "(WARNING: This will override the materials of other linked objects, " +
387                             "which have the materials linked to Data)",
388            default = False,
389            )
390    link_to: EnumProperty(
391            name = "Link",
392            description = "What should the material be linked to",
393            items = mu_link_to_enums,
394            default = 'OBJECT'
395            )
396
397    affect: EnumProperty(
398            name = "Affect",
399            description = "Which materials of objects to affect",
400            items = mu_link_affect_enums,
401            default = 'SELECTED'
402            )
403
404    @classmethod
405    def poll(cls, context):
406        return (context.active_object is not None)
407
408    def draw(self, context):
409        layout = self.layout
410
411        layout.prop(self, "link_to", expand = True)
412        layout.separator()
413
414        layout.prop(self, "affect")
415        layout.separator()
416
417        layout.prop(self, "override", icon = "DECORATE_OVERRIDE")
418
419    def invoke(self, context, event):
420        return context.window_manager.invoke_props_dialog(self)
421
422    def execute(self, context):
423        return mu_change_material_link(self, self.link_to, self.affect, self.override)
424
425class MATERIAL_OT_materialutilities_merge_base_names(bpy.types.Operator):
426    """Merges materials that has the same base names but ends with .xxx (.001, .002 etc)"""
427
428    bl_idname = "material.materialutilities_merge_base_names"
429    bl_label = "Merge Base Names"
430    bl_description = "Merge materials that has the same base names but ends with .xxx (.001, .002 etc)"
431
432    material_base_name: StringProperty(
433                            name = "Material Base Name",
434                            default = "",
435                            description = 'Base name for materials to merge ' +
436                                          '(e.g. "Material" is the base name of "Material.001", "Material.002" etc.)'
437                            )
438    is_auto: BoolProperty(
439                            name = "Auto Merge",
440                            description = "Find all available duplicate materials and Merge them"
441                            )
442
443    is_not_undo = False
444    material_error = []          # collect mat for warning messages
445
446
447    def replace_name(self):
448        """If the user chooses a material like 'Material.042', clean it up to get a base name ('Material')"""
449
450        # use the chosen material as a base one, check if there is a name
451        self.check_no_name = (False if self.material_base_name in {""} else True)
452
453        # No need to do this if it's already "clean"
454        #  (Also lessens the potential of error given about the material with the Base name)
455        if '.' not in self.material_base_name:
456            return
457
458        if self.check_no_name is True:
459            for mat in bpy.data.materials:
460                name = mat.name
461
462                if name == self.material_base_name:
463                    try:
464                        base, suffix = name.rsplit('.', 1)
465
466                        # trigger the exception
467                        num = int(suffix, 10)
468                        self.material_base_name = base
469                        mat.name = self.material_base_name
470                        return
471                    except ValueError:
472                        if name not in self.material_error:
473                            self.material_error.append(name)
474                        return
475
476        return
477
478    def split_name(self, material):
479        """Split the material name into a base and a suffix"""
480
481        name = material.name
482
483        # No need to do this if it's already "clean"/there is no suffix
484        if '.' not in name:
485            return name, None
486
487        base, suffix = name.rsplit('.', 1)
488
489        try:
490            # trigger the exception
491            num = int(suffix, 10)
492        except ValueError:
493            # Not a numeric suffix
494            # Don't report on materials not actually included in the merge!
495            if ((self.is_auto or base == self.material_base_name)
496                 and (name not in self.material_error)):
497                self.material_error.append(name)
498            return name, None
499
500        if self.is_auto is False:
501            if base == self.material_base_name:
502                return base, suffix
503            else:
504                return name, None
505
506        return base, suffix
507
508    def fixup_slot(self, slot):
509        """Fix material slots that was assigned to materials now removed"""
510
511        if not slot.material:
512            return
513
514        base, suffix = self.split_name(slot.material)
515        if suffix is None:
516            return
517
518        try:
519            base_mat = bpy.data.materials[base]
520        except KeyError:
521            print("\n[Materials Utilities Specials]\nLink to base names\nError:"
522                  "Base material %r not found\n" % base)
523            return
524
525        slot.material = base_mat
526
527    def main_loop(self, context):
528        """Loops through all objects and material slots to make sure they are assigned to the right material"""
529
530        for obj in context.scene.objects:
531            for slot in obj.material_slots:
532                self.fixup_slot(slot)
533
534    @classmethod
535    def poll(self, context):
536        return (context.mode == 'OBJECT') and (len(context.visible_objects) > 0)
537
538    def draw(self, context):
539        layout = self.layout
540
541        box_1 = layout.box()
542        box_1.prop_search(self, "material_base_name", bpy.data, "materials")
543        box_1.enabled = not self.is_auto
544        layout.separator()
545
546        layout.prop(self, "is_auto", text = "Auto Rename/Replace", icon = "SYNTAX_ON")
547
548    def invoke(self, context, event):
549        self.is_not_undo = True
550        return context.window_manager.invoke_props_dialog(self)
551
552    def execute(self, context):
553        # Reset Material errors, otherwise we risk reporting errors erroneously..
554        self.material_error = []
555
556        if not self.is_auto:
557            self.replace_name()
558
559            if self.check_no_name:
560                self.main_loop(context)
561            else:
562                self.report({'WARNING'}, "No Material Base Name given!")
563
564                self.is_not_undo = False
565                return {'CANCELLED'}
566
567        self.main_loop(context)
568
569        if self.material_error:
570            materials = ", ".join(self.material_error)
571
572            if len(self.material_error) == 1:
573                waswere = " was"
574                suff_s = ""
575            else:
576                waswere = " were"
577                suff_s = "s"
578
579            self.report({'WARNING'}, materials + waswere + " not removed or set as Base" + suff_s)
580
581        self.is_not_undo = False
582        return {'FINISHED'}
583
584class MATERIAL_OT_materialutilities_material_slot_move(bpy.types.Operator):
585    """Move the active material slot"""
586
587    bl_idname = "material.materialutilities_slot_move"
588    bl_label = "Move Slot"
589    bl_description = "Move the material slot"
590    bl_options = {'REGISTER', 'UNDO'}
591
592    movement: EnumProperty(
593                name = "Move",
594                description = "How to move the material slot",
595                items = mu_material_slot_move_enums
596                )
597
598    @classmethod
599    def poll(self, context):
600        # would prefer to access self.movement here, but can't..
601        obj = context.active_object
602        if not obj:
603            return False
604        if (obj.active_material_index < 0) or (len(obj.material_slots) <= 1):
605            return False
606        return True
607
608    def execute(self, context):
609        active_object = context.active_object
610        active_material = context.object.active_material
611
612        if self.movement == 'TOP':
613            dir = 'UP'
614
615            steps = active_object.active_material_index
616        else:
617            dir = 'DOWN'
618
619            last_slot_index = len(active_object.material_slots) - 1
620            steps = last_slot_index - active_object.active_material_index
621
622        if steps == 0:
623            self.report({'WARNING'}, active_material.name + " already at " + self.movement.lower() + '!')
624        else:
625            for i in range(steps):
626                bpy.ops.object.material_slot_move(direction = dir)
627
628            self.report({'INFO'}, active_material.name + ' moved to ' + self.movement.lower())
629
630        return {'FINISHED'}
631
632
633
634class MATERIAL_OT_materialutilities_join_objects(bpy.types.Operator):
635    """Join objects that have the same (selected) material(s)"""
636
637    bl_idname = "material.materialutilities_join_objects"
638    bl_label = "Join by material (Material Utilities)"
639    bl_description = "Join objects that share the same material"
640    bl_options = {'REGISTER', 'UNDO'}
641
642    material_name: StringProperty(
643                            name = "Material",
644                            default = "",
645                            description = 'Material to use to join objects'
646                            )
647    is_auto: BoolProperty(
648                            name = "Auto Join",
649                            description = "Join objects for all materials"
650                            )
651
652    is_not_undo = True
653    material_error = []          # collect mat for warning messages
654
655
656    @classmethod
657    def poll(self, context):
658        # This operator only works in Object mode
659        return (context.mode == 'OBJECT') and (len(context.visible_objects) > 0)
660
661    def draw(self, context):
662        layout = self.layout
663
664        box_1 = layout.box()
665        box_1.prop_search(self, "material_name", bpy.data, "materials")
666        box_1.enabled = not self.is_auto
667        layout.separator()
668
669        layout.prop(self, "is_auto", text = "Auto Join", icon = "SYNTAX_ON")
670
671    def invoke(self, context, event):
672        self.is_not_undo = True
673        return context.window_manager.invoke_props_dialog(self)
674
675    def execute(self, context):
676        # Reset Material errors, otherwise we risk reporting errors erroneously..
677        self.material_error = []
678        materials = []
679
680        if not self.is_auto:
681            if self.material_name == "":
682                self.report({'WARNING'}, "No Material Name given!")
683
684                self.is_not_undo = False
685                return {'CANCELLED'}
686            materials = [self.material_name]
687        else:
688            materials = bpy.data.materials.keys()
689
690        result = mu_join_objects(self, materials)
691        self.is_not_undo = False
692
693        return result
694
695
696class MATERIAL_OT_materialutilities_auto_smooth_angle(bpy.types.Operator):
697    """Set Auto smooth values for selected objects"""
698    # Inspired by colkai
699
700    bl_idname = "view3d.materialutilities_auto_smooth_angle"
701    bl_label = "Set Auto Smooth Angle (Material Utilities)"
702    bl_options = {'REGISTER', 'UNDO'}
703
704    affect: EnumProperty(
705            name = "Affect",
706            description = "Which objects of to affect",
707            items = mu_affect_enums,
708            default = 'SELECTED'
709            )
710    angle: FloatProperty(
711            name = "Angle",
712            description = "Maximum angle between face normals that will be considered as smooth",
713            subtype = 'ANGLE',
714            min = 0,
715            max = radians(180),
716            default = radians(35)
717            )
718    set_smooth_shading: BoolProperty(
719            name = "Set Smooth",
720            description = "Set Smooth shading for the affected objects\n"
721                   "This overrides the currenth smooth/flat shading that might be set to different parts of the object",
722            default = True
723            )
724
725    @classmethod
726    def poll(cls, context):
727        return (len(bpy.data.objects) > 0) and (context.mode == 'OBJECT')
728
729    def invoke(self, context, event):
730        self.is_not_undo = True
731        return context.window_manager.invoke_props_dialog(self)
732
733    def draw(self, context):
734        layout = self.layout
735
736        layout.prop(self, "angle")
737        layout.prop(self, "affect")
738
739        layout.prop(self, "set_smooth_shading", icon = "BLANK1")
740
741    def execute(self, context):
742        return mu_set_auto_smooth(self, self.angle, self.affect, self.set_smooth_shading)
743