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