d1c546827c51ea5ed9fc62420848fe620b8fa0d9
[blender-addons-contrib.git] / mesh_tools / __init__.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18 # Contributed to by:
19 # meta-androcto,  Hidesato Ikeya, zmj100, Gert De Roost, TrumanBlending, PKHG, #
20 # Oscurart, Greg, Stanislav Blinov, komi3D, BlenderLab, Paul Marshall (brikbot), #
21 # metalliandy, macouno, CoDEmanX, dustractor, Liero, lijenstina, Germano Cavalcante #
22 # Pistiwique, Jimmy Hazevoet #
23
24 bl_info = {
25     "name": "Mesh Tools",
26     "author": "Meta-Androcto",
27     "version": (0, 3, 6),
28     "blender": (2, 80, 0),
29     "location": "View3D > Toolbar and View3D > Specials Menu",
30     "warning": "",
31     "description": "Mesh modelling toolkit. Several tools to aid modelling",
32     "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/"
33                 "Py/Scripts/Modeling/Extra_Tools",
34     "category": "Mesh",
35 }
36
37 # Import From Files
38 if "bpy" in locals():
39     import importlib
40     importlib.reload(mesh_offset_edges)
41     importlib.reload(split_solidify)
42     importlib.reload(mesh_filletplus)
43     importlib.reload(mesh_vertex_chamfer)
44     importlib.reload(random_vertices)
45     importlib.reload(mesh_extrude_and_reshape)
46     importlib.reload(mesh_edge_roundifier)
47     importlib.reload(mesh_edgetools)
48
49
50 else:
51     from . import mesh_offset_edges
52     from . import split_solidify
53     from . import mesh_filletplus
54     from . import mesh_vertex_chamfer
55     from . import random_vertices
56     from . import mesh_extrude_and_reshape
57     from . import mesh_edge_roundifier
58     from . import mesh_edgetools
59
60
61 import bmesh
62 import bpy
63 import collections
64 import mathutils
65 import random
66 from math import (
67         sin, cos, tan,
68         degrees, radians, pi,
69         )
70 from random import gauss
71 from mathutils import Matrix, Euler, Vector
72 from bpy_extras import view3d_utils
73 from bpy.types import (
74         Operator,
75         Menu,
76         Panel,
77         PropertyGroup,
78         AddonPreferences,
79         )
80 from bpy.props import (
81         BoolProperty,
82         BoolVectorProperty,
83         EnumProperty,
84         FloatProperty,
85         FloatVectorProperty,
86         IntVectorProperty,
87         PointerProperty,
88         StringProperty,
89         IntProperty
90         )
91
92 # ########################################
93 # ##### General functions ################
94 # ########################################
95
96
97 # Multi extrude
98 def gloc(self, r):
99     return Vector((self.offx, self.offy, self.offz))
100
101
102 def vloc(self, r):
103     random.seed(self.ran + r)
104     return self.off * (1 + gauss(0, self.var1 / 3))
105
106
107 def nrot(self, n):
108     return Euler((radians(self.nrotx) * n[0],
109                   radians(self.nroty) * n[1],
110                   radians(self.nrotz) * n[2]), 'XYZ')
111
112
113 def vrot(self, r):
114     random.seed(self.ran + r)
115     return Euler((radians(self.rotx) + gauss(0, self.var2 / 3),
116                   radians(self.roty) + gauss(0, self.var2 / 3),
117                   radians(self.rotz) + gauss(0, self.var2 / 3)), 'XYZ')
118
119
120 def vsca(self, r):
121     random.seed(self.ran + r)
122     return self.sca * (1 + gauss(0, self.var3 / 3))
123
124
125 class ME_OT_MExtrude(Operator):
126     bl_idname = "object.mextrude"
127     bl_label = "Multi Extrude"
128     bl_description = ("Extrude selected Faces with Rotation,\n"
129                       "Scaling, Variation, Randomization")
130     bl_options = {"REGISTER", "UNDO", "PRESET"}
131
132     off : FloatProperty(
133             name="Offset",
134             soft_min=0.001, soft_max=10,
135             min=-100, max=100,
136             default=1.0,
137             description="Translation"
138             )
139     offx : FloatProperty(
140             name="Loc X",
141             soft_min=-10.0, soft_max=10.0,
142             min=-100.0, max=100.0,
143             default=0.0,
144             description="Global Translation X"
145             )
146     offy : FloatProperty(
147             name="Loc Y",
148             soft_min=-10.0, soft_max=10.0,
149             min=-100.0, max=100.0,
150             default=0.0,
151             description="Global Translation Y"
152             )
153     offz : FloatProperty(
154             name="Loc Z",
155             soft_min=-10.0, soft_max=10.0,
156             min=-100.0, max=100.0,
157             default=0.0,
158             description="Global Translation Z"
159             )
160     rotx : FloatProperty(
161             name="Rot X",
162             min=-85, max=85,
163             soft_min=-30, soft_max=30,
164             default=0,
165             description="X Rotation"
166             )
167     roty : FloatProperty(
168             name="Rot Y",
169             min=-85, max=85,
170             soft_min=-30,
171             soft_max=30,
172             default=0,
173             description="Y Rotation"
174             )
175     rotz : FloatProperty(
176             name="Rot Z",
177             min=-85, max=85,
178             soft_min=-30, soft_max=30,
179             default=-0,
180             description="Z Rotation"
181             )
182     nrotx : FloatProperty(
183             name="N Rot X",
184             min=-85, max=85,
185             soft_min=-30, soft_max=30,
186             default=0,
187             description="Normal X Rotation"
188             )
189     nroty : FloatProperty(
190             name="N Rot Y",
191             min=-85, max=85,
192             soft_min=-30, soft_max=30,
193             default=0,
194             description="Normal Y Rotation"
195             )
196     nrotz : FloatProperty(
197             name="N Rot Z",
198             min=-85, max=85,
199             soft_min=-30, soft_max=30,
200             default=-0,
201             description="Normal Z Rotation"
202             )
203     sca : FloatProperty(
204             name="Scale",
205             min=0.01, max=10,
206             soft_min=0.5, soft_max=1.5,
207             default=1.0,
208             description="Scaling of the selected faces after extrusion"
209             )
210     var1 : FloatProperty(
211             name="Offset Var", min=-10, max=10,
212             soft_min=-1, soft_max=1,
213             default=0,
214             description="Offset variation"
215             )
216     var2 : FloatProperty(
217             name="Rotation Var",
218             min=-10, max=10,
219             soft_min=-1, soft_max=1,
220             default=0,
221             description="Rotation variation"
222             )
223     var3 : FloatProperty(
224             name="Scale Noise",
225             min=-10, max=10,
226             soft_min=-1, soft_max=1,
227             default=0,
228             description="Scaling noise"
229             )
230     var4 : IntProperty(
231             name="Probability",
232             min=0, max=100,
233             default=100,
234             description="Probability, chance of extruding a face"
235             )
236     num : IntProperty(
237             name="Repeat",
238             min=1, max=500,
239             soft_max=100,
240             default=1,
241             description="Repetitions"
242             )
243     ran : IntProperty(
244             name="Seed",
245             min=-9999, max=9999,
246             default=0,
247             description="Seed to feed random values"
248             )
249     opt1 : BoolProperty(
250             name="Polygon coordinates",
251             default=True,
252             description="Polygon coordinates, Object coordinates"
253             )
254     opt2 : BoolProperty(
255             name="Proportional offset",
256             default=False,
257             description="Scale * Offset"
258             )
259     opt3 : BoolProperty(
260             name="Per step rotation noise",
261             default=False,
262             description="Per step rotation noise, Initial rotation noise"
263             )
264     opt4 : BoolProperty(
265             name="Per step scale noise",
266             default=False,
267             description="Per step scale noise, Initial scale noise"
268             )
269
270     @classmethod
271     def poll(cls, context):
272         obj = context.object
273         return (obj and obj.type == 'MESH')
274
275     def draw(self, context):
276         layout = self.layout
277         col = layout.column(align=True)
278         col.label(text="Transformations:")
279         col.prop(self, "off", slider=True)
280         col.prop(self, "offx", slider=True)
281         col.prop(self, "offy", slider=True)
282         col.prop(self, "offz", slider=True)
283
284         col = layout.column(align=True)
285         col.prop(self, "rotx", slider=True)
286         col.prop(self, "roty", slider=True)
287         col.prop(self, "rotz", slider=True)
288         col.prop(self, "nrotx", slider=True)
289         col.prop(self, "nroty", slider=True)
290         col.prop(self, "nrotz", slider=True)
291         col = layout.column(align=True)
292         col.prop(self, "sca", slider=True)
293
294         col = layout.column(align=True)
295         col.label(text="Variation settings:")
296         col.prop(self, "var1", slider=True)
297         col.prop(self, "var2", slider=True)
298         col.prop(self, "var3", slider=True)
299         col.prop(self, "var4", slider=True)
300         col.prop(self, "ran")
301         col = layout.column(align=False)
302         col.prop(self, 'num')
303
304         col = layout.column(align=True)
305         col.label(text="Options:")
306         col.prop(self, "opt1")
307         col.prop(self, "opt2")
308         col.prop(self, "opt3")
309         col.prop(self, "opt4")
310
311     def execute(self, context):
312         obj = bpy.context.object
313         om = obj.mode
314         bpy.context.tool_settings.mesh_select_mode = [False, False, True]
315         origin = Vector([0.0, 0.0, 0.0])
316
317         # bmesh operations
318         bpy.ops.object.mode_set()
319         bm = bmesh.new()
320         bm.from_mesh(obj.data)
321         sel = [f for f in bm.faces if f.select]
322
323         after = []
324
325         # faces loop
326         for i, of in enumerate(sel):
327             nro = nrot(self, of.normal)
328             off = vloc(self, i)
329             loc = gloc(self, i)
330             of.normal_update()
331
332             # initial rotation noise
333             if self.opt3 is False:
334                 rot = vrot(self, i)
335             # initial scale noise
336             if self.opt4 is False:
337                 s = vsca(self, i)
338
339             # extrusion loop
340             for r in range(self.num):
341                 # random probability % for extrusions
342                 if self.var4 > int(random.random() * 100):
343                     nf = of.copy()
344                     nf.normal_update()
345                     no = nf.normal.copy()
346
347                     # face/obj coordinates
348                     if self.opt1 is True:
349                         ce = nf.calc_center_bounds()
350                     else:
351                         ce = origin
352
353                     # per step rotation noise
354                     if self.opt3 is True:
355                         rot = vrot(self, i + r)
356                     # per step scale noise
357                     if self.opt4 is True:
358                         s = vsca(self, i + r)
359
360                     # proportional, scale * offset
361                     if self.opt2 is True:
362                         off = s * off
363
364                     for v in nf.verts:
365                         v.co -= ce
366                         v.co.rotate(nro)
367                         v.co.rotate(rot)
368                         v.co += ce + loc + no * off
369                         v.co = v.co.lerp(ce, 1 - s)
370
371                     # extrude code from TrumanBlending
372                     for a, b in zip(of.loops, nf.loops):
373                         sf = bm.faces.new((a.vert, a.link_loop_next.vert,
374                                            b.link_loop_next.vert, b.vert))
375                         sf.normal_update()
376                     bm.faces.remove(of)
377                     of = nf
378
379             after.append(of)
380
381         for v in bm.verts:
382             v.select = False
383         for e in bm.edges:
384             e.select = False
385
386         for f in after:
387             if f not in sel:
388                 f.select = True
389             else:
390                 f.select = False
391
392         bm.to_mesh(obj.data)
393         obj.data.update()
394
395         # restore user settings
396         bpy.ops.object.mode_set(mode=om)
397
398         if not len(sel):
399             self.report({"WARNING"},
400                         "No suitable Face selection found. Operation cancelled")
401             return {'CANCELLED'}
402
403         return {'FINISHED'}
404
405 # Face inset fillet
406 def edit_mode_out():
407     bpy.ops.object.mode_set(mode='OBJECT')
408
409
410 def edit_mode_in():
411     bpy.ops.object.mode_set(mode='EDIT')
412
413
414 def angle_rotation(rp, q, axis, angle):
415     # returns the vector made by the rotation of the vector q
416     # rp by angle around axis and then adds rp
417
418     return (Matrix.Rotation(angle, 3, axis) @ (q - rp)) + rp
419
420
421 def face_inset_fillet(bme, face_index_list, inset_amount, distance,
422                       number_of_sides, out, radius, type_enum, kp):
423     list_del = []
424
425     for faceindex in face_index_list:
426
427         bme.faces.ensure_lookup_table()
428         # loops through the faces...
429         f = bme.faces[faceindex]
430         f.select_set(False)
431         list_del.append(f)
432         f.normal_update()
433         vertex_index_list = [v.index for v in f.verts]
434         dict_0 = {}
435         orientation_vertex_list = []
436         n = len(vertex_index_list)
437         for i in range(n):
438             # loops through the vertices
439             dict_0[i] = []
440             bme.verts.ensure_lookup_table()
441             p = (bme.verts[vertex_index_list[i]].co).copy()
442             p1 = (bme.verts[vertex_index_list[(i - 1) % n]].co).copy()
443             p2 = (bme.verts[vertex_index_list[(i + 1) % n]].co).copy()
444             # copies some vert coordinates, always the 3 around i
445             dict_0[i].append(bme.verts[vertex_index_list[i]])
446             # appends the bmesh vert of the appropriate index to the dict
447             vec1 = p - p1
448             vec2 = p - p2
449             # vectors for the other corner points to the cornerpoint
450             # corresponding to i / p
451             angle = vec1.angle(vec2)
452
453             adj = inset_amount / tan(angle * 0.5)
454             h = (adj ** 2 + inset_amount ** 2) ** 0.5
455             if round(degrees(angle)) == 180 or round(degrees(angle)) == 0.0:
456                 # if the corner is a straight line...
457                 # I think this creates some new points...
458                 if out is True:
459                     val = ((f.normal).normalized() * inset_amount)
460                 else:
461                     val = -((f.normal).normalized() * inset_amount)
462                 p6 = angle_rotation(p, p + val, vec1, radians(90))
463             else:
464                 # if the corner is an actual corner
465                 val = ((f.normal).normalized() * h)
466                 if out is True:
467                     # this -(p - (vec2.normalized() * adj))) is just the freaking axis afaik...
468                     p6 = angle_rotation(
469                                 p, p + val,
470                                 -(p - (vec2.normalized() * adj)),
471                                 -radians(90)
472                                 )
473                 else:
474                     p6 = angle_rotation(
475                                 p, p - val,
476                                 ((p - (vec1.normalized() * adj)) - (p - (vec2.normalized() * adj))),
477                                 -radians(90)
478                                 )
479
480                 orientation_vertex_list.append(p6)
481
482         new_inner_face = []
483         orientation_vertex_list_length = len(orientation_vertex_list)
484         ovll = orientation_vertex_list_length
485
486         for j in range(ovll):
487             q = orientation_vertex_list[j]
488             q1 = orientation_vertex_list[(j - 1) % ovll]
489             q2 = orientation_vertex_list[(j + 1) % ovll]
490             # again, these are just vectors between somewhat displaced corner vertices
491             vec1_ = q - q1
492             vec2_ = q - q2
493             ang_ = vec1_.angle(vec2_)
494
495             # the angle between them
496             if round(degrees(ang_)) == 180 or round(degrees(ang_)) == 0.0:
497                 # again... if it's really a line...
498                 v = bme.verts.new(q)
499                 new_inner_face.append(v)
500                 dict_0[j].append(v)
501             else:
502                 # s.a.
503                 if radius is False:
504                     h_ = distance * (1 / cos(ang_ * 0.5))
505                     d = distance
506                 elif radius is True:
507                     h_ = distance / sin(ang_ * 0.5)
508                     d = distance / tan(ang_ * 0.5)
509                 # max(d) is vec1_.magnitude * 0.5
510                 # or vec2_.magnitude * 0.5 respectively
511
512                 # only functional difference v
513                 if d > vec1_.magnitude * 0.5:
514                     d = vec1_.magnitude * 0.5
515
516                 if d > vec2_.magnitude * 0.5:
517                     d = vec2_.magnitude * 0.5
518                 # only functional difference ^
519
520                 q3 = q - (vec1_.normalized() * d)
521                 q4 = q - (vec2_.normalized() * d)
522                 # these are new verts somewhat offset from the corners
523                 rp_ = q - ((q - ((q3 + q4) * 0.5)).normalized() * h_)
524                 # reference point inside the curvature
525                 axis_ = vec1_.cross(vec2_)
526                 # this should really be just the face normal
527                 vec3_ = rp_ - q3
528                 vec4_ = rp_ - q4
529                 rot_ang = vec3_.angle(vec4_)
530                 cornerverts = []
531
532                 for o in range(number_of_sides + 1):
533                     # this calculates the actual new vertices
534                     q5 = angle_rotation(rp_, q4, axis_, rot_ang * o / number_of_sides)
535                     v = bme.verts.new(q5)
536
537                     # creates new bmesh vertices from it
538                     bme.verts.index_update()
539
540                     dict_0[j].append(v)
541                     cornerverts.append(v)
542
543                 cornerverts.reverse()
544                 new_inner_face.extend(cornerverts)
545
546         if out is False:
547             f = bme.faces.new(new_inner_face)
548             f.select_set(True)
549         elif out is True and kp is True:
550             f = bme.faces.new(new_inner_face)
551             f.select_set(True)
552
553         n2_ = len(dict_0)
554         # these are the new side faces, those that don't depend on cornertype
555         for o in range(n2_):
556             list_a = dict_0[o]
557             list_b = dict_0[(o + 1) % n2_]
558             bme.faces.new([list_a[0], list_b[0], list_b[-1], list_a[1]])
559             bme.faces.index_update()
560         # cornertype 1 - ngon faces
561         if type_enum == 'opt0':
562             for k in dict_0:
563                 if len(dict_0[k]) > 2:
564                     bme.faces.new(dict_0[k])
565                     bme.faces.index_update()
566         # cornertype 2 - triangulated faces
567         if type_enum == 'opt1':
568             for k_ in dict_0:
569                 q_ = dict_0[k_][0]
570                 dict_0[k_].pop(0)
571                 n3_ = len(dict_0[k_])
572                 for kk in range(n3_ - 1):
573                     bme.faces.new([dict_0[k_][kk], dict_0[k_][(kk + 1) % n3_], q_])
574                     bme.faces.index_update()
575
576     del_ = [bme.faces.remove(f) for f in list_del]
577
578     if del_:
579         del del_
580
581
582 # Operator
583
584 class MESH_OT_face_inset_fillet(Operator):
585     bl_idname = "mesh.face_inset_fillet"
586     bl_label = "Face Inset Fillet"
587     bl_description = ("Inset selected and Fillet (make round) the corners \n"
588                      "of the newly created Faces")
589     bl_options = {"REGISTER", "UNDO"}
590
591     # inset amount
592     inset_amount : bpy.props.FloatProperty(
593             name="Inset amount",
594             description="Define the size of the Inset relative to the selection",
595             default=0.04,
596             min=0, max=100.0,
597             step=1,
598             precision=3
599             )
600     # number of sides
601     number_of_sides : bpy.props.IntProperty(
602             name="Number of sides",
603             description="Define the roundness of the corners by specifying\n"
604                         "the subdivision count",
605             default=4,
606             min=1, max=100,
607             step=1
608             )
609     distance : bpy.props.FloatProperty(
610             name="",
611             description="Use distance or radius for corners' size calculation",
612             default=0.04,
613             min=0.00001, max=100.0,
614             step=1,
615             precision=3
616             )
617     out : bpy.props.BoolProperty(
618             name="Outside",
619             description="Inset the Faces outwards in relation to the selection\n"
620                         "Note: depending on the geometry, can give unsatisfactory results",
621             default=False
622             )
623     radius : bpy.props.BoolProperty(
624             name="Radius",
625             description="Use radius for corners' size calculation",
626             default=False
627             )
628     type_enum : bpy.props.EnumProperty(
629             items=[('opt0', "N-gon", "N-gon corners - Keep the corner Faces uncut"),
630                    ('opt1', "Triangle", "Triangulate corners")],
631             name="Corner Type",
632             default="opt0"
633             )
634     kp : bpy.props.BoolProperty(
635             name="Keep faces",
636             description="Do not delete the inside Faces\n"
637                         "Only available if the Out option is checked",
638             default=False
639             )
640
641     def draw(self, context):
642         layout = self.layout
643
644         layout.label(text="Corner Type:")
645
646         row = layout.row()
647         row.prop(self, "type_enum", text="")
648
649         row = layout.row(align=True)
650         row.prop(self, "out")
651
652         if self.out is True:
653             row.prop(self, "kp")
654
655         row = layout.row()
656         row.prop(self, "inset_amount")
657
658         row = layout.row()
659         row.prop(self, "number_of_sides")
660
661         row = layout.row()
662         row.prop(self, "radius")
663
664         row = layout.row()
665         dist_rad = "Radius" if self.radius else "Distance"
666         row.prop(self, "distance", text=dist_rad)
667
668     def execute(self, context):
669         # this really just prepares everything for the main function
670         inset_amount = self.inset_amount
671         number_of_sides = self.number_of_sides
672         distance = self.distance
673         out = self.out
674         radius = self.radius
675         type_enum = self.type_enum
676         kp = self.kp
677
678         edit_mode_out()
679         ob_act = context.active_object
680         bme = bmesh.new()
681         bme.from_mesh(ob_act.data)
682         # this
683         face_index_list = [f.index for f in bme.faces if f.select and f.is_valid]
684
685         if len(face_index_list) == 0:
686             self.report({'WARNING'},
687                         "No suitable Face selection found. Operation cancelled")
688             edit_mode_in()
689
690             return {'CANCELLED'}
691
692         elif len(face_index_list) != 0:
693             face_inset_fillet(bme, face_index_list,
694                               inset_amount, distance, number_of_sides,
695                               out, radius, type_enum, kp)
696
697         bme.to_mesh(ob_act.data)
698         edit_mode_in()
699
700         return {'FINISHED'}
701     
702 # ********** Edit Multiselect **********
703 class VIEW3D_MT_Edit_MultiMET(Menu):
704     bl_label = "Multi Select"
705
706     def draw(self, context):
707         layout = self.layout
708         layout.operator_context = 'INVOKE_REGION_WIN'
709
710         layout.operator("multiedit.allselect", text="All Select Modes", icon='RESTRICT_SELECT_OFF')
711
712
713 # Select Tools
714 class VIEW3D_MT_Select_Vert(Menu):
715     bl_label = "Select Vert"
716
717     def draw(self, context):
718         layout = self.layout
719         layout.operator_context = 'INVOKE_REGION_WIN'
720
721         layout.operator("multiedit.vertexselect", text="Vertex Select Mode", icon='VERTEXSEL')
722         layout.operator("multiedit.vertedgeselect", text="Vert & Edge Select", icon='EDGESEL')
723         layout.operator("multiedit.vertfaceselect", text="Vert & Face Select", icon='FACESEL')
724
725
726 class VIEW3D_MT_Select_Edge(Menu):
727     bl_label = "Select Edge"
728
729     def draw(self, context):
730         layout = self.layout
731         layout.operator_context = 'INVOKE_REGION_WIN'
732
733         layout.operator("multiedit.edgeselect", text="Edge Select Mode", icon='EDGESEL')
734         layout.operator("multiedit.vertedgeselect", text="Edge & Vert Select", icon='VERTEXSEL')
735         layout.operator("multiedit.edgefaceselect", text="Edge & Face Select", icon='FACESEL')
736
737
738 class VIEW3D_MT_Select_Face(Menu):
739     bl_label = "Select Face"
740
741     def draw(self, context):
742         layout = self.layout
743         layout.operator_context = 'INVOKE_REGION_WIN'
744
745         layout.operator("multiedit.faceselect", text="Face Select Mode", icon='FACESEL')
746         layout.operator("multiedit.vertfaceselect", text="Face & Vert Select", icon='VERTEXSEL')
747         layout.operator("multiedit.edgefaceselect", text="Face & Edge Select", icon='EDGESEL')
748
749
750  # multiple edit select modes.
751 class VIEW3D_OT_multieditvertex(Operator):
752     bl_idname = "multiedit.vertexselect"
753     bl_label = "Vertex Mode"
754     bl_description = "Vert Select Mode On"
755     bl_options = {'REGISTER', 'UNDO'}
756
757     def execute(self, context):
758         if context.object.mode != "EDIT":
759             bpy.ops.object.mode_set(mode="EDIT")
760             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
761         if bpy.ops.mesh.select_mode != "EDGE, FACE":
762             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
763             return {'FINISHED'}
764
765
766 class VIEW3D_OT_multieditedge(Operator):
767     bl_idname = "multiedit.edgeselect"
768     bl_label = "Edge Mode"
769     bl_description = "Edge Select Mode On"
770     bl_options = {'REGISTER', 'UNDO'}
771
772     def execute(self, context):
773         if context.object.mode != "EDIT":
774             bpy.ops.object.mode_set(mode="EDIT")
775             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
776         if bpy.ops.mesh.select_mode != "VERT, FACE":
777             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
778             return {'FINISHED'}
779
780
781 class VIEW3D_OT_multieditface(Operator):
782     bl_idname = "multiedit.faceselect"
783     bl_label = "Multiedit Face"
784     bl_description = "Face Select Mode On"
785     bl_options = {'REGISTER', 'UNDO'}
786
787     def execute(self, context):
788         if context.object.mode != "EDIT":
789             bpy.ops.object.mode_set(mode="EDIT")
790             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
791         if bpy.ops.mesh.select_mode != "VERT, EDGE":
792             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
793             return {'FINISHED'}
794
795 class VIEW3D_OT_multieditvertedge(Operator):
796     bl_idname = "multiedit.vertedgeselect"
797     bl_label = "Multiedit Face"
798     bl_description = "Vert & Edge Select Modes On"
799     bl_options = {'REGISTER', 'UNDO'}
800
801     def execute(self, context):
802         if context.object.mode != "EDIT":
803             bpy.ops.object.mode_set(mode="EDIT")
804             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
805         if bpy.ops.mesh.select_mode != "VERT, EDGE, FACE":
806             bpy.ops.object.mode_set(mode="EDIT")
807             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
808             bpy.ops.mesh.select_mode(use_extend=True, use_expand=False, type='EDGE')
809             return {'FINISHED'}
810
811 class VIEW3D_OT_multieditvertface(Operator):
812     bl_idname = "multiedit.vertfaceselect"
813     bl_label = "Multiedit Face"
814     bl_description = "Vert & Face Select Modes On"
815     bl_options = {'REGISTER', 'UNDO'}
816
817     def execute(self, context):
818         if context.object.mode != "EDIT":
819             bpy.ops.object.mode_set(mode="EDIT")
820             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
821         if bpy.ops.mesh.select_mode != "VERT, EDGE, FACE":
822             bpy.ops.object.mode_set(mode="EDIT")
823             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
824             bpy.ops.mesh.select_mode(use_extend=True, use_expand=False, type='FACE')
825             return {'FINISHED'}
826
827
828 class VIEW3D_OT_multieditedgeface(Operator):
829     bl_idname = "multiedit.edgefaceselect"
830     bl_label = "Mode Face Edge"
831     bl_description = "Edge & Face Select Modes On"
832     bl_options = {'REGISTER', 'UNDO'}
833
834     def execute(self, context):
835         if context.object.mode != "EDIT":
836             bpy.ops.object.mode_set(mode="EDIT")
837             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
838         if bpy.ops.mesh.select_mode != "VERT, EDGE, FACE":
839             bpy.ops.object.mode_set(mode="EDIT")
840             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
841             bpy.ops.mesh.select_mode(use_extend=True, use_expand=False, type='FACE')
842             return {'FINISHED'}
843
844
845 class VIEW3D_OT_multieditall(Operator):
846     bl_idname = "multiedit.allselect"
847     bl_label = "All Edit Select Modes"
848     bl_description = "Vert & Edge & Face Select Modes On"
849     bl_options = {'REGISTER', 'UNDO'}
850
851     def execute(self, context):
852         if context.object.mode != "EDIT":
853             bpy.ops.object.mode_set(mode="EDIT")
854             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
855         if bpy.ops.mesh.select_mode != "VERT, EDGE, FACE":
856             bpy.ops.object.mode_set(mode="EDIT")
857             bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
858             bpy.ops.mesh.select_mode(use_extend=True, use_expand=False, type='EDGE')
859             bpy.ops.mesh.select_mode(use_extend=True, use_expand=False, type='FACE')
860             return {'FINISHED'}
861
862
863 # ########################################
864 # ##### GUI and registration #############
865 # ########################################
866
867 # menu containing all tools
868 class VIEW3D_MT_edit_mesh_tools(Menu):
869     bl_label = "Mesh Tools"
870
871     def draw(self, context):
872         layout = self.layout
873         layout.operator("mesh.remove_doubles")
874         layout.operator("mesh.dissolve_limited")
875         layout.operator("mesh.flip_normals")
876         props = layout.operator("mesh.quads_convert_to_tris")
877         props.quad_method = props.ngon_method = 'BEAUTY'
878         layout.operator("mesh.tris_convert_to_quads")
879         layout.operator('mesh.vertex_chamfer', text="Vertex Chamfer")
880         layout.operator("mesh.bevel", text="Bevel Vertices").vertex_only = True
881         layout.operator('mesh.offset_edges', text="Offset Edges")
882         layout.operator('mesh.fillet_plus', text="Fillet Edges")
883         layout.operator("mesh.face_inset_fillet",
884                             text="Face Inset Fillet")
885         layout.operator("mesh.extrude_reshape",
886                         text="Push/Pull Faces")
887         layout.operator("object.mextrude",
888                         text="Multi Extrude")
889         layout.operator('mesh.split_solidify', text="Split Solidify")
890
891             
892
893 # panel containing all tools
894 class VIEW3D_PT_edit_mesh_tools(Panel):
895     bl_space_type = 'VIEW_3D'
896     bl_region_type = 'UI'
897     bl_category = 'Edit'
898     bl_context = "mesh_edit"
899     bl_label = "Mesh Tools"
900     bl_options = {'DEFAULT_CLOSED'}
901
902     def draw(self, context):
903         layout = self.layout
904         col = layout.column(align=True)
905         et = context.window_manager.edittools
906
907         # vert - first line
908         split = col.split(factor=0.80, align=True)
909         if et.display_vert:
910             split.prop(et, "display_vert", text="Vert Tools", icon='DOWNARROW_HLT')
911         else:
912             split.prop(et, "display_vert", text="Vert tools", icon='RIGHTARROW')
913         split.menu("VIEW3D_MT_Select_Vert", text="", icon='VERTEXSEL')
914         # vert - settings
915         if et.display_vert:
916             box = col.column(align=True).box().column()
917             col_top = box.column(align=True)
918             row = col_top.row(align=True)
919             row.operator('mesh.vertex_chamfer', text="Vertex Chamfer")
920             row = col_top.row(align=True)
921             row.operator("mesh.extrude_vertices_move", text="Extrude Vertices")
922             row = col_top.row(align=True)
923             row.operator("mesh.random_vertices", text="Random Vertices")
924             row = col_top.row(align=True)
925             row.operator("mesh.bevel", text="Bevel Vertices").vertex_only = True
926
927         # edge - first line
928         split = col.split(factor=0.80, align=True)
929         if et.display_edge:
930             split.prop(et, "display_edge", text="Edge Tools", icon='DOWNARROW_HLT')
931         else:
932             split.prop(et, "display_edge", text="Edge Tools", icon='RIGHTARROW')
933         split.menu("VIEW3D_MT_Select_Edge", text="", icon='EDGESEL')
934         # Edge - settings
935         if et.display_edge:
936             box = col.column(align=True).box().column()
937             col_top = box.column(align=True)
938             row = col_top.row(align=True)
939             row.operator('mesh.offset_edges', text="Offset Edges")
940             row = col_top.row(align=True)
941             row.operator('mesh.fillet_plus', text="Fillet Edges")
942             row = col_top.row(align=True)
943             row.operator('mesh.edge_roundifier', text="Edge Roundify")
944             row = col_top.row(align=True)
945             row.operator("mesh.extrude_edges_move", text="Extrude Edges")
946             row = col_top.row(align=True)
947             row.operator("mesh.bevel", text="Bevel Edges").vertex_only = False
948
949         # face - first line
950         split = col.split(factor=0.80, align=True)
951         if et.display_face:
952             split.prop(et, "display_face", text="Face Tools", icon='DOWNARROW_HLT')
953         else:
954             split.prop(et, "display_face", text="Face Tools", icon='RIGHTARROW')
955         split.menu("VIEW3D_MT_Select_Face", text="", icon='FACESEL')
956         # face - settings
957         if et.display_face:
958             box = col.column(align=True).box().column()
959             col_top = box.column(align=True)
960             row = col_top.row(align=True)
961             row.operator("mesh.face_inset_fillet",
962                             text="Face Inset Fillet")
963             row = col_top.row(align=True)
964             row.operator("mesh.extrude_reshape",
965                             text="Push/Pull Faces")
966             row = col_top.row(align=True)
967             row.operator("mesh.inset")
968             row = col_top.row(align=True)
969             row.operator("mesh.extrude_faces_move", text="Extrude Individual Faces")
970             row = col_top.row(align=True)
971             row.operator("object.mextrude",
972                             text="Multi Extrude")
973             row = col_top.row(align=True)
974             row.operator('mesh.split_solidify', text="Split Solidify")
975
976
977         # util - first line
978         split = col.split(factor=0.80, align=True)
979         if et.display_util:
980             split.prop(et, "display_util", text="Utility Tools", icon='DOWNARROW_HLT')
981         else:
982             split.prop(et, "display_util", text="Utility Tools", icon='RIGHTARROW')
983         split.menu("VIEW3D_MT_Edit_MultiMET", text="", icon='RESTRICT_SELECT_OFF')
984         # face - settings
985         if et.display_util:
986             box = col.column(align=True).box().column()
987             col_top = box.column(align=True)
988             row = col_top.row(align=True)
989             row.operator("mesh.subdivide")
990             row = col_top.row(align=True)
991             row.operator("mesh.remove_doubles")
992             row = col_top.row(align=True)
993             row.operator("mesh.dissolve_limited")
994             row = col_top.row(align=True)
995             row.operator("mesh.flip_normals")
996             row = col_top.row(align=True)
997             props = row.operator("mesh.quads_convert_to_tris")
998             props.quad_method = props.ngon_method = 'BEAUTY'
999             row = col_top.row(align=True)
1000             row.operator("mesh.tris_convert_to_quads")
1001             
1002 # property group containing all properties for the gui in the panel
1003 class EditToolsProps(PropertyGroup):
1004     """
1005     Fake module like class
1006     bpy.context.window_manager.edittools
1007     """
1008     # general display properties
1009     display_vert: BoolProperty(
1010         name="Bridge settings",
1011         description="Display settings of the Vert tool",
1012         default=False
1013         )
1014     display_edge: BoolProperty(
1015         name="Edge settings",
1016         description="Display settings of the Edge tool",
1017         default=False
1018         )
1019     display_face: BoolProperty(
1020         name="Face settings",
1021         description="Display settings of the Face tool",
1022         default=False
1023         )
1024     display_util: BoolProperty(
1025         name="Face settings",
1026         description="Display settings of the Face tool",
1027         default=False
1028         )
1029
1030 # draw function for integration in menus
1031 def menu_func(self, context):
1032     self.layout.menu("VIEW3D_MT_edit_mesh_tools")
1033     self.layout.separator()
1034
1035 # Add-ons Preferences Update Panel
1036
1037 # Define Panel classes for updating
1038 panels = (
1039         VIEW3D_PT_edit_mesh_tools,
1040         )
1041
1042
1043 def update_panel(self, context):
1044     message = "LoopTools: Updating Panel locations has failed"
1045     try:
1046         for panel in panels:
1047             if "bl_rna" in panel.__dict__:
1048                 bpy.utils.unregister_class(panel)
1049
1050         for panel in panels:
1051             panel.bl_category = context.preferences.addons[__name__].preferences.category
1052             bpy.utils.register_class(panel)
1053
1054     except Exception as e:
1055         print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
1056         pass
1057
1058
1059 class EditToolsPreferences(AddonPreferences):
1060     # this must match the addon name, use '__package__'
1061     # when defining this in a submodule of a python package.
1062     bl_idname = __name__
1063
1064     category: StringProperty(
1065             name="Tab Category",
1066             description="Choose a name for the category of the panel",
1067             default="Edit",
1068             update=update_panel
1069             )
1070
1071     def draw(self, context):
1072         layout = self.layout
1073
1074         row = layout.row()
1075         col = row.column()
1076         col.label(text="Tab Category:")
1077         col.prop(self, "category", text="")
1078
1079
1080 # define classes for registration
1081 classes = (
1082     VIEW3D_MT_edit_mesh_tools,
1083     VIEW3D_PT_edit_mesh_tools,
1084     VIEW3D_MT_Edit_MultiMET,
1085     VIEW3D_MT_Select_Vert,
1086     VIEW3D_MT_Select_Edge,
1087     VIEW3D_MT_Select_Face,
1088     EditToolsProps,
1089     EditToolsPreferences,
1090     MESH_OT_face_inset_fillet,
1091     ME_OT_MExtrude,
1092     VIEW3D_OT_multieditvertex,
1093     VIEW3D_OT_multieditedge,
1094     VIEW3D_OT_multieditface,
1095     VIEW3D_OT_multieditvertedge,
1096     VIEW3D_OT_multieditvertface,
1097     VIEW3D_OT_multieditedgeface,
1098     VIEW3D_OT_multieditall
1099     )
1100
1101
1102 # registering and menu integration
1103 def register():
1104     for cls in classes:
1105         bpy.utils.register_class(cls)
1106     bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(menu_func)
1107     bpy.types.WindowManager.edittools = PointerProperty(type=EditToolsProps)
1108     update_panel(None, bpy.context)
1109
1110     mesh_filletplus.register()
1111     mesh_offset_edges.register()
1112     split_solidify.register()
1113     mesh_vertex_chamfer.register()
1114     random_vertices.register()
1115     mesh_extrude_and_reshape.register()
1116     mesh_edge_roundifier.register()
1117     mesh_edgetools.register()
1118
1119 # unregistering and removing menus
1120 def unregister():
1121     for cls in reversed(classes):
1122         bpy.utils.unregister_class(cls)
1123     bpy.types.VIEW3D_MT_edit_mesh_context_menu.remove(menu_func)
1124     try:
1125         del bpy.types.WindowManager.edittools
1126     except Exception as e:
1127         print('unregister fail:\n', e)
1128         pass
1129
1130     mesh_filletplus.unregister()
1131     mesh_offset_edges.unregister()
1132     split_solidify.unregister()
1133     mesh_vertex_chamfer.unregister()
1134     random_vertices.unregister()
1135     mesh_extrude_and_reshape.unregister()
1136     mesh_edge_roundifier.unregister()
1137     mesh_edgetools.unregister()
1138
1139 if __name__ == "__main__":
1140     register()