├── InstantMeshesBridge_Updated.py ├── InstantMeshesRemesh_bl_two_eight(OLD).py ├── README.md └── mesh_f2.py /InstantMeshesBridge_Updated.py: -------------------------------------------------------------------------------- 1 | 2 | import shutil 3 | import tempfile 4 | import subprocess 5 | import os 6 | import bpy 7 | bl_info = { 8 | "name": "Instant Meshes Remesh", 9 | "author": "knekke, cgvirus", 10 | "category": "Object", 11 | "blender": (2, 80, 0), 12 | } 13 | 14 | 15 | class InstantMeshesRemeshPrefs(bpy.types.AddonPreferences): 16 | bl_idname = __name__ 17 | 18 | filepath: bpy.props.StringProperty( 19 | name="Instant Meshes Executable", 20 | subtype='FILE_PATH', 21 | ) 22 | 23 | def draw(self, context): 24 | layout = self.layout 25 | layout.label(text="""Please specify the path to 'Instant Meshes.exe' 26 | Get it from https://github.com/wjakob/instant-meshes""") 27 | layout.prop(self, "filepath") 28 | 29 | 30 | # class InstantMeshesRemeshBatch(bpy.types.Operator): 31 | # """Remesh selected objects with last used settings""" 32 | # bl_idname = "object.instant_meshes_remesh_batch" 33 | # bl_label = "Instant Meshes Remesh BATCH" 34 | # bl_options = {'REGISTER', 'UNDO'} 35 | 36 | # def execute(self, context): 37 | # s = bpy.context.selected_objects 38 | # for other_obj in bpy.data.objects: 39 | # other_obj.select_set(state= False) 40 | # for i in s: 41 | # i.select_set(state= True) 42 | # bpy.context.view_layer.objects.active = i 43 | # bpy.ops.object.instant_meshes_remesh() 44 | # for slot, mat in enumerate(i.data.materials): 45 | # bpy.data.objects[i.name+'_remesh'].data.materials[slot] = mat.copy() 46 | # bpy.data.objects[i.name+'_remesh'].data.materials[slot].diffuse_color = (0.3,1,0.3) 47 | # i.select_set(state= False) 48 | # bpy.context.view_layer.objects.active = None 49 | # return {'FINISHED'} 50 | ######################################### 51 | class InstantMeshesRemesh(bpy.types.Operator): 52 | """Remesh by using the Instant Meshes program""" 53 | bl_idname = "object.instant_meshes_remesh" 54 | bl_label = "Instant Meshes Remesh" 55 | bl_options = {'REGISTER', 'UNDO'} 56 | 57 | exported = False 58 | deterministic: bpy.props.BoolProperty(name="Deterministic (slower)", description="Prefer (slower) deterministic algorithms", default=False) 59 | dominant: bpy.props.BoolProperty(name="Dominant", description="Generate a tri/quad dominant mesh instead of a pure tri/quad mesh", default=False) 60 | intrinsic: bpy.props.BoolProperty(name="Intrinsic", description="Intrinsic mode (extrinsic is the default)", default=False) 61 | boundaries: bpy.props.BoolProperty(name="Boundaries", description="Align to boundaries (only applies when the mesh is not closed)", default=False) 62 | crease: bpy.props.IntProperty(name="Crease Degree", description="Dihedral angle threshold for creases", default=0, min=0, max=100) 63 | verts: bpy.props.IntProperty(name="Vertex Count", description="Desired vertex count of the output mesh", default=2000, min=10, max=50000) 64 | smooth: bpy.props.IntProperty(name="Smooth iterations", description="Number of smoothing & ray tracing reprojection steps (default: 2)", default=2, min=0, max=10) 65 | remeshIt: bpy.props.BoolProperty(name="Start Remeshing", description="Activating it will start Remesh", default=False) 66 | openUI: bpy.props.BoolProperty(name="Open in InstantMeshes", description="Opens the selected object in Instant Meshes and imports the result when you are done.", default=False) 67 | 68 | loc = None 69 | rot = None 70 | scl = None 71 | meshname = None 72 | 73 | def execute(self, context): 74 | exe = context.preferences.addons[__name__].preferences.filepath 75 | orig = os.path.join(tempfile.gettempdir(), 'original.obj') 76 | output = os.path.join(tempfile.gettempdir(), 'out.obj') 77 | 78 | if self.remeshIt: 79 | 80 | if not self.exported: 81 | try: 82 | os.remove(orig) 83 | except: 84 | pass 85 | self.meshname = bpy.context.active_object.name 86 | mesh = bpy.context.active_object 87 | # self.loc = mesh.matrix_world.to_translation() 88 | # self.rot = mesh.matrix_world.to_euler('XYZ') 89 | # self.scl = mesh.matrix_world.to_scale() 90 | # bpy.ops.object.location_clear() 91 | # bpy.ops.object.rotation_clear() 92 | # bpy.ops.object.scale_clear() 93 | bpy.ops.export_scene.obj(filepath=orig, 94 | check_existing=False, 95 | axis_forward='-Z', axis_up='Y', 96 | use_selection=True, 97 | use_mesh_modifiers=True, 98 | # use_mesh_modifiers_render=False, 99 | use_edges=True, 100 | use_smooth_groups=False, 101 | use_smooth_groups_bitflags=False, 102 | use_normals=True, 103 | use_uvs=True, ) 104 | 105 | self.exported = True 106 | # mesh.location = self.loc 107 | # mesh.rotation_euler = self.rot 108 | # mesh.scale = self.scl 109 | 110 | mesh = bpy.data.objects[self.meshname] 111 | mesh.hide_viewport = False 112 | options = ['-c', str(self.crease), 113 | '-v', str(self.verts), 114 | '-S', str(self.smooth), 115 | '-o', output] 116 | if self.deterministic: 117 | options.append('-d') 118 | if self.dominant: 119 | options.append('-D') 120 | if self.intrinsic: 121 | options.append('-i') 122 | if self.boundaries: 123 | options.append('-b') 124 | 125 | cmd = [exe] + options + [orig] 126 | 127 | print (cmd) 128 | 129 | if self.openUI: 130 | os.chdir(os.path.dirname(orig)) 131 | shutil.copy2(orig, output) 132 | subprocess.run([exe, output]) 133 | self.openUI = False 134 | else: 135 | subprocess.run(cmd) 136 | 137 | bpy.ops.import_scene.obj(filepath=output, 138 | use_split_objects=False, 139 | use_smooth_groups=False, 140 | use_image_search=False, 141 | axis_forward='-Z', axis_up='Y') 142 | imported_mesh = bpy.context.selected_objects[0] 143 | # imported_mesh.location = self.loc 144 | # imported_mesh.rotation_euler = self.rot 145 | # imported_mesh.scale = self.scl 146 | print(mesh, mesh.name) 147 | imported_mesh.name = mesh.name + '_remesh' 148 | for i in mesh.data.materials: 149 | print('setting mat: ' + i.name) 150 | imported_mesh.data.materials.append(i) 151 | for edge in imported_mesh.data.edges: 152 | edge.use_edge_sharp = False 153 | for other_obj in bpy.data.objects: 154 | other_obj.select_set(state=False) 155 | imported_mesh.select_set(state=True) 156 | imported_mesh.active_material.use_nodes = False 157 | imported_mesh.data.use_auto_smooth = False 158 | 159 | bpy.ops.object.shade_flat() 160 | bpy.ops.mesh.customdata_custom_splitnormals_clear() 161 | 162 | mesh.select_set(state=True) 163 | bpy.context.view_layer.objects.active = mesh 164 | bpy.ops.object.data_transfer(use_reverse_transfer=False, 165 | use_freeze=False, data_type='UV', use_create=True, vert_mapping='NEAREST', 166 | edge_mapping='NEAREST', loop_mapping='NEAREST_POLYNOR', poly_mapping='NEAREST', 167 | use_auto_transform=False, use_object_transform=True, use_max_distance=False, 168 | max_distance=1.0, ray_radius=0.0, islands_precision=0.1, layers_select_src='ACTIVE', 169 | layers_select_dst='ACTIVE', mix_mode='REPLACE', mix_factor=1.0) 170 | mesh.select_set(state=False) 171 | mesh.hide_viewport = True 172 | imported_mesh.select_set(state=False) 173 | os.remove(output) 174 | bpy.context.space_data.overlay.show_wireframes = True 175 | 176 | return {'FINISHED'} 177 | else: 178 | return {'FINISHED'} 179 | ############################################## 180 | 181 | 182 | def menu_func(self, context): 183 | # self.layout.operator(InstantMeshesRemeshBatch.bl_idname) 184 | self.layout.operator(InstantMeshesRemesh.bl_idname) 185 | 186 | 187 | classes = ( 188 | InstantMeshesRemesh, 189 | # InstantMeshesRemeshBatch, 190 | InstantMeshesRemeshPrefs, 191 | ) 192 | 193 | 194 | def register(): 195 | # add operator 196 | from bpy.utils import register_class 197 | for c in classes: 198 | bpy.utils.register_class(c) 199 | 200 | bpy.types.VIEW3D_MT_object.append(menu_func) 201 | try: 202 | os.remove(os.path.join(tempfile.gettempdir(), 'original.obj')) 203 | os.remove(os.path.join(tempfile.gettempdir(), 'out.obj')) 204 | except: 205 | pass 206 | 207 | 208 | def unregister(): 209 | from bpy.utils import unregister_class 210 | bpy.types.VIEW3D_MT_object.remove(menu_func) 211 | 212 | # remove operator and preferences 213 | for c in reversed(classes): 214 | bpy.utils.unregister_class(c) 215 | 216 | 217 | if __name__ == "__main__": 218 | register() 219 | -------------------------------------------------------------------------------- /InstantMeshesRemesh_bl_two_eight(OLD).py: -------------------------------------------------------------------------------- 1 | 2 | bl_info = { 3 | "name": "Instant Meshes Remesh", 4 | "author": "knekke", 5 | "category": "Object", 6 | "blender": (2, 80, 0), 7 | } 8 | 9 | import bpy 10 | import os 11 | import subprocess 12 | import tempfile 13 | import shutil 14 | 15 | 16 | class InstantMeshesRemeshPrefs(bpy.types.AddonPreferences): 17 | bl_idname = __name__ 18 | 19 | filepath : bpy.props.StringProperty( 20 | name="Instant Meshes Executable", 21 | subtype='FILE_PATH', 22 | ) 23 | 24 | def draw(self, context): 25 | layout = self.layout 26 | layout.label(text="""Please specify the path to 'Instant Meshes.exe' 27 | Get it from https://github.com/wjakob/instant-meshes""") 28 | layout.prop(self, "filepath") 29 | 30 | 31 | class InstantMeshesRemeshBatch(bpy.types.Operator): 32 | """Remesh selected objects with last used settings""" 33 | bl_idname = "object.instant_meshes_remesh_batch" 34 | bl_label = "Instant Meshes Remesh BATCH" 35 | bl_options = {'REGISTER', 'UNDO'} 36 | 37 | def execute(self, context): 38 | s = bpy.context.selected_objects 39 | for other_obj in bpy.data.objects: 40 | other_obj.select_set(state= False) 41 | for i in s: 42 | i.select_set(state= True) 43 | bpy.context.view_layer.objects.active = i 44 | bpy.ops.object.instant_meshes_remesh() 45 | for slot, mat in enumerate(i.data.materials): 46 | bpy.data.objects[i.name+'_remesh'].data.materials[slot] = mat.copy() 47 | bpy.data.objects[i.name+'_remesh'].data.materials[slot].diffuse_color = (0.3,1,0.3) 48 | i.select_set(state= False) 49 | bpy.context.view_layer.objects.active = None 50 | return {'FINISHED'} 51 | 52 | class InstantMeshesRemesh(bpy.types.Operator): 53 | """Remesh by using the Instant Meshes program""" 54 | bl_idname = "object.instant_meshes_remesh" 55 | bl_label = "Instant Meshes Remesh" 56 | bl_options = {'REGISTER', 'UNDO'} 57 | 58 | exported = False 59 | deterministic : bpy.props.BoolProperty(name="Deterministic (slower)", description="Prefer (slower) deterministic algorithms", default=False) 60 | dominant : bpy.props.BoolProperty(name="Dominant", description="Generate a tri/quad dominant mesh instead of a pure tri/quad mesh", default=False) 61 | intrinsic : bpy.props.BoolProperty(name="Intrinsic", description="Intrinsic mode (extrinsic is the default)", default=False) 62 | boundaries : bpy.props.BoolProperty(name="Boundaries", description="Align to boundaries (only applies when the mesh is not closed)", default=False) 63 | crease : bpy.props.IntProperty(name="Crease Degree", description="Dihedral angle threshold for creases", default=0, min=0, max=100) 64 | verts : bpy.props.IntProperty(name="Vertex Count", description="Desired vertex count of the output mesh", default=2000, min=200, max=50000) 65 | smooth : bpy.props.IntProperty(name="Smooth iterations", description="Number of smoothing & ray tracing reprojection steps (default: 2)", default=2, min=0, max=10) 66 | openUI : bpy.props.BoolProperty(name="Open in InstantMeshes", description="Opens the selected object in Instant Meshes and imports the result when you are done.", default=False) 67 | 68 | loc = None 69 | rot = None 70 | scl = None 71 | meshname = None 72 | 73 | def execute(self, context): 74 | exe = context.preferences.addons[__name__].preferences.filepath 75 | orig = os.path.join(tempfile.gettempdir(),'original.obj') 76 | output = os.path.join(tempfile.gettempdir(),'out.obj') 77 | 78 | if not self.exported: 79 | try: 80 | os.remove(orig) 81 | except: 82 | pass 83 | self.meshname = bpy.context.active_object.name 84 | mesh = bpy.context.active_object 85 | self.loc = mesh.matrix_world.to_translation() 86 | self.rot = mesh.matrix_world.to_euler('XYZ') 87 | self.scl = mesh.matrix_world.to_scale() 88 | bpy.ops.object.location_clear() 89 | bpy.ops.object.rotation_clear() 90 | bpy.ops.object.scale_clear() 91 | bpy.ops.export_scene.obj(filepath=orig, 92 | check_existing=False, 93 | axis_forward='Y', axis_up='Z', 94 | use_selection=True, 95 | use_mesh_modifiers=True, 96 | use_mesh_modifiers_render=False, 97 | use_edges=True, 98 | use_smooth_groups=False, 99 | use_smooth_groups_bitflags=False, 100 | use_normals=True, 101 | use_uvs=True, 102 | use_materials=False) 103 | self.exported = True 104 | mesh.location = self.loc 105 | mesh.rotation_euler = self.rot 106 | mesh.scale = self.scl 107 | 108 | mesh = bpy.data.objects[self.meshname] 109 | mesh.hide_viewport = False 110 | options = ['-c', str(self.crease), 111 | '-v', str(self.verts), 112 | '-S', str(self.smooth), 113 | '-o', output] 114 | if self.deterministic: 115 | options.append('-d') 116 | if self.dominant: 117 | options.append('-D') 118 | if self.intrinsic: 119 | options.append('-i') 120 | if self.boundaries: 121 | options.append('-b') 122 | 123 | cmd = [exe] + options + [orig] 124 | if self.openUI: 125 | os.chdir(os.path.dirname(orig)) 126 | shutil.copy2(orig, output) 127 | subprocess.run([exe, output]) 128 | self.openUI = False 129 | else: 130 | subprocess.run(cmd) 131 | 132 | bpy.ops.import_scene.obj(filepath=output, 133 | use_smooth_groups=False, 134 | use_image_search=False, 135 | axis_forward='Y', axis_up='Z') 136 | imported_mesh = bpy.context.selected_objects[0] 137 | imported_mesh.location = self.loc 138 | imported_mesh.rotation_euler = self.rot 139 | imported_mesh.scale = self.scl 140 | print(mesh, mesh.name) 141 | imported_mesh.name = mesh.name + '_remesh' 142 | for i in mesh.data.materials: 143 | print('setting mat: ' +i.name) 144 | imported_mesh.data.materials.append(i) 145 | for edge in imported_mesh.data.edges: 146 | edge.use_edge_sharp = False 147 | for other_obj in bpy.data.objects: 148 | other_obj.select_set(state= False) 149 | imported_mesh.select_set (state = True) 150 | bpy.ops.object.shade_flat() 151 | mesh.select_set (state = True) 152 | bpy.context.view_layer.objects.active = mesh 153 | bpy.ops.object.data_transfer(use_reverse_transfer=False, 154 | use_freeze=False, data_type='UV', use_create=True, vert_mapping='NEAREST', 155 | edge_mapping='NEAREST', loop_mapping='NEAREST_POLYNOR', poly_mapping='NEAREST', 156 | use_auto_transform=False, use_object_transform=True, use_max_distance=False, 157 | max_distance=1.0, ray_radius=0.0, islands_precision=0.1, layers_select_src='ACTIVE', 158 | layers_select_dst='ACTIVE', mix_mode='REPLACE', mix_factor=1.0) 159 | mesh.select_set(state= False) 160 | mesh.hide_viewport = True 161 | mesh.hide_render = True 162 | imported_mesh.select_set(state= False) 163 | os.remove(output) 164 | return {'FINISHED'} 165 | 166 | 167 | def menu_func(self, context): 168 | self.layout.operator(InstantMeshesRemeshBatch.bl_idname) 169 | self.layout.operator(InstantMeshesRemesh.bl_idname) 170 | 171 | 172 | classes = ( 173 | InstantMeshesRemesh, 174 | InstantMeshesRemeshBatch, 175 | InstantMeshesRemeshPrefs, 176 | ) 177 | 178 | 179 | def register(): 180 | # add operator 181 | from bpy.utils import register_class 182 | for c in classes: 183 | bpy.utils.register_class(c) 184 | 185 | bpy.types.VIEW3D_MT_object.append(menu_func) 186 | try: 187 | os.remove(os.path.join(tempfile.gettempdir(),'original.obj')) 188 | os.remove(os.path.join(tempfile.gettempdir(),'out.obj')) 189 | except: 190 | pass 191 | 192 | def unregister(): 193 | from bpy.utils import unregister_class 194 | bpy.types.VIEW3D_MT_object.remove(menu_func) 195 | 196 | # remove operator and preferences 197 | for c in reversed(classes): 198 | bpy.utils.unregister_class(c) 199 | 200 | 201 | if __name__ == "__main__": 202 | register() 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blender2.8-addon-updates 2 | addon conversions for Blender 2.8 3 | -------------------------------------------------------------------------------- /mesh_f2.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 | 19 | # 20 | 21 | bl_info = { 22 | "name": "F2", 23 | "author": "Bart Crouch, Alexander Nedovizin, Paul Kotelevets " 24 | "(concept design)", 25 | "version": (1, 7, 3), 26 | "blender": (2, 80, 0), 27 | "location": "Editmode > F", 28 | "warning": "", 29 | "description": "Extends the 'Make Edge/Face' functionality", 30 | "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" 31 | "Scripts/Modeling/F2", 32 | "category": "Mesh", 33 | } 34 | 35 | 36 | import bmesh 37 | import bpy 38 | import itertools 39 | import mathutils 40 | from bpy_extras import view3d_utils 41 | 42 | 43 | # create a face from a single selected edge 44 | def quad_from_edge(bm, edge_sel, context, event): 45 | ob = context.active_object 46 | region = context.region 47 | region_3d = context.space_data.region_3d 48 | 49 | # find linked edges that are open (<2 faces connected) and not part of 50 | # the face the selected edge belongs to 51 | all_edges = [[edge for edge in edge_sel.verts[i].link_edges if \ 52 | len(edge.link_faces) < 2 and edge != edge_sel and \ 53 | sum([face in edge_sel.link_faces for face in edge.link_faces]) == 0] \ 54 | for i in range(2)] 55 | if not all_edges[0] or not all_edges[1]: 56 | return 57 | 58 | # determine which edges to use, based on mouse cursor position 59 | mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y]) 60 | optimal_edges = [] 61 | for edges in all_edges: 62 | min_dist = False 63 | for edge in edges: 64 | vert = [vert for vert in edge.verts if not vert.select][0] 65 | world_pos = ob.matrix_world @ vert.co.copy() 66 | screen_pos = view3d_utils.location_3d_to_region_2d(region, 67 | region_3d, world_pos) 68 | dist = (mouse_pos - screen_pos).length 69 | if not min_dist or dist < min_dist[0]: 70 | min_dist = (dist, edge, vert) 71 | optimal_edges.append(min_dist) 72 | 73 | # determine the vertices, which make up the quad 74 | v1 = edge_sel.verts[0] 75 | v2 = edge_sel.verts[1] 76 | edge_1 = optimal_edges[0][1] 77 | edge_2 = optimal_edges[1][1] 78 | v3 = optimal_edges[0][2] 79 | v4 = optimal_edges[1][2] 80 | 81 | # normal detection 82 | flip_align = True 83 | normal_edge = edge_1 84 | if not normal_edge.link_faces: 85 | normal_edge = edge_2 86 | if not normal_edge.link_faces: 87 | normal_edge = edge_sel 88 | if not normal_edge.link_faces: 89 | # no connected faces, so no need to flip the face normal 90 | flip_align = False 91 | if flip_align: # there is a face to which the normal can be aligned 92 | ref_verts = [v for v in normal_edge.link_faces[0].verts] 93 | if v3 in ref_verts: 94 | va_1 = v3 95 | va_2 = v1 96 | elif normal_edge == edge_sel: 97 | va_1 = v1 98 | va_2 = v2 99 | else: 100 | va_1 = v2 101 | va_2 = v4 102 | if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \ 103 | (va_2 == ref_verts[0] and va_1 == ref_verts[-1]): 104 | # reference verts are at start and end of the list -> shift list 105 | ref_verts = ref_verts[1:] + [ref_verts[0]] 106 | if ref_verts.index(va_1) > ref_verts.index(va_2): 107 | # connected face has same normal direction, so don't flip 108 | flip_align = False 109 | 110 | # material index detection 111 | ref_faces = edge_sel.link_faces 112 | if not ref_faces: 113 | ref_faces = edge_sel.verts[0].link_faces 114 | if not ref_faces: 115 | ref_faces = edge_sel.verts[1].link_faces 116 | if not ref_faces: 117 | mat_index = False 118 | smooth = False 119 | else: 120 | mat_index = ref_faces[0].material_index 121 | smooth = ref_faces[0].smooth 122 | 123 | # create quad 124 | try: 125 | if v3 == v4: 126 | # triangle (usually at end of quad-strip 127 | verts = [v3, v1, v2] 128 | else: 129 | # normal face creation 130 | verts = [v3, v1, v2, v4] 131 | if flip_align: 132 | verts.reverse() 133 | face = bm.faces.new(verts) 134 | if mat_index: 135 | face.material_index = mat_index 136 | face.smooth = smooth 137 | except: 138 | # face already exists 139 | return 140 | 141 | # change selection 142 | edge_sel.select = False 143 | for vert in edge_sel.verts: 144 | vert.select = False 145 | for edge in face.edges: 146 | if edge.index < 0: 147 | edge.select = True 148 | v3.select = True 149 | v4.select = True 150 | 151 | # adjust uv-map 152 | if __name__ != '__main__': 153 | addon_prefs = context.preferences.addons[__name__].preferences 154 | if addon_prefs.adjustuv: 155 | for (key, uv_layer) in bm.loops.layers.uv.items(): 156 | uv_ori = {} 157 | for vert in [v1, v2, v3, v4]: 158 | for loop in vert.link_loops: 159 | if loop.face.index > -1: 160 | uv_ori[loop.vert.index] = loop[uv_layer].uv 161 | if len(uv_ori) == 4 or len(uv_ori) == 3: 162 | for loop in face.loops: 163 | loop[uv_layer].uv = uv_ori[loop.vert.index] 164 | 165 | # toggle mode, to force correct drawing 166 | bpy.ops.object.mode_set(mode='OBJECT') 167 | bpy.ops.object.mode_set(mode='EDIT') 168 | 169 | 170 | # create a face from a single selected vertex, if it is an open vertex 171 | def quad_from_vertex(bm, vert_sel, context, event): 172 | ob = context.active_object 173 | me = ob.data 174 | region = context.region 175 | region_3d = context.space_data.region_3d 176 | 177 | # find linked edges that are open (<2 faces connected) 178 | edges = [edge for edge in vert_sel.link_edges if len(edge.link_faces) < 2] 179 | if len(edges) < 2: 180 | return 181 | 182 | # determine which edges to use, based on mouse cursor position 183 | min_dist = False 184 | mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y]) 185 | for a, b in itertools.combinations(edges, 2): 186 | other_verts = [vert for edge in [a, b] for vert in edge.verts \ 187 | if not vert.select] 188 | mid_other = (other_verts[0].co.copy() + other_verts[1].co.copy()) \ 189 | / 2 190 | new_pos = 2 * (mid_other - vert_sel.co.copy()) + vert_sel.co.copy() 191 | world_pos = ob.matrix_world @ new_pos 192 | screen_pos = view3d_utils.location_3d_to_region_2d(region, region_3d, 193 | world_pos) 194 | dist = (mouse_pos - screen_pos).length 195 | if not min_dist or dist < min_dist[0]: 196 | min_dist = (dist, (a, b), other_verts, new_pos) 197 | 198 | # create vertex at location mirrored in the line, connecting the open edges 199 | edges = min_dist[1] 200 | other_verts = min_dist[2] 201 | new_pos = min_dist[3] 202 | vert_new = bm.verts.new(new_pos) 203 | 204 | # normal detection 205 | flip_align = True 206 | normal_edge = edges[0] 207 | if not normal_edge.link_faces: 208 | normal_edge = edges[1] 209 | if not normal_edge.link_faces: 210 | # no connected faces, so no need to flip the face normal 211 | flip_align = False 212 | if flip_align: # there is a face to which the normal can be aligned 213 | ref_verts = [v for v in normal_edge.link_faces[0].verts] 214 | if other_verts[0] in ref_verts: 215 | va_1 = other_verts[0] 216 | va_2 = vert_sel 217 | else: 218 | va_1 = vert_sel 219 | va_2 = other_verts[1] 220 | if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \ 221 | (va_2 == ref_verts[0] and va_1 == ref_verts[-1]): 222 | # reference verts are at start and end of the list -> shift list 223 | ref_verts = ref_verts[1:] + [ref_verts[0]] 224 | if ref_verts.index(va_1) > ref_verts.index(va_2): 225 | # connected face has same normal direction, so don't flip 226 | flip_align = False 227 | 228 | # material index detection 229 | ref_faces = vert_sel.link_faces 230 | if not ref_faces: 231 | mat_index = False 232 | smooth = False 233 | else: 234 | mat_index = ref_faces[0].material_index 235 | smooth = ref_faces[0].smooth 236 | 237 | # create face between all 4 vertices involved 238 | verts = [other_verts[0], vert_sel, other_verts[1], vert_new] 239 | if flip_align: 240 | verts.reverse() 241 | face = bm.faces.new(verts) 242 | if mat_index: 243 | face.material_index = mat_index 244 | face.smooth = smooth 245 | 246 | # change selection 247 | vert_new.select = True 248 | vert_sel.select = False 249 | 250 | # adjust uv-map 251 | if __name__ != '__main__': 252 | addon_prefs = context.preferences.addons[__name__].preferences 253 | if addon_prefs.adjustuv: 254 | for (key, uv_layer) in bm.loops.layers.uv.items(): 255 | uv_others = {} 256 | uv_sel = None 257 | uv_new = None 258 | # get original uv coordinates 259 | for i in range(2): 260 | for loop in other_verts[i].link_loops: 261 | if loop.face.index > -1: 262 | uv_others[loop.vert.index] = loop[uv_layer].uv 263 | break 264 | if len(uv_others) == 2: 265 | mid_other = (list(uv_others.values())[0] + 266 | list(uv_others.values())[1]) / 2 267 | for loop in vert_sel.link_loops: 268 | if loop.face.index > -1: 269 | uv_sel = loop[uv_layer].uv 270 | break 271 | if uv_sel: 272 | uv_new = 2 * (mid_other - uv_sel) + uv_sel 273 | 274 | # set uv coordinates for new loops 275 | if uv_new: 276 | for loop in face.loops: 277 | if loop.vert.index == -1: 278 | x, y = uv_new 279 | elif loop.vert.index in uv_others: 280 | x, y = uv_others[loop.vert.index] 281 | else: 282 | x, y = uv_sel 283 | loop[uv_layer].uv = (x, y) 284 | 285 | # toggle mode, to force correct drawing 286 | bpy.ops.object.mode_set(mode='OBJECT') 287 | bpy.ops.object.mode_set(mode='EDIT') 288 | 289 | 290 | # autograb preference in addons panel 291 | class F2AddonPreferences(bpy.types.AddonPreferences): 292 | bl_idname = __name__ 293 | adjustuv: bpy.props.BoolProperty( 294 | name = "Adjust UV", 295 | description = "Automatically update UV unwrapping", 296 | default = True) 297 | autograb: bpy.props.BoolProperty( 298 | name = "Auto Grab", 299 | description = "Automatically puts a newly created vertex in grab mode", 300 | default = False) 301 | 302 | def draw(self, context): 303 | layout = self.layout 304 | layout.prop(self, "autograb") 305 | layout.prop(self, "adjustuv") 306 | 307 | 308 | class MeshF2(bpy.types.Operator): 309 | """Tooltip""" 310 | bl_idname = "mesh.f2" 311 | bl_label = "Make Edge/Face" 312 | bl_description = "Extends the 'Make Edge/Face' functionality" 313 | bl_options = {'REGISTER', 'UNDO'} 314 | 315 | @classmethod 316 | def poll(cls, context): 317 | # check we are in mesh editmode 318 | ob = context.active_object 319 | return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') 320 | 321 | def invoke(self, context, event): 322 | bm = bmesh.from_edit_mesh(context.active_object.data) 323 | sel = [v for v in bm.verts if v.select] 324 | if len(sel) > 2: 325 | # original 'Make Edge/Face' behaviour 326 | try: 327 | bpy.ops.mesh.edge_face_add('INVOKE_DEFAULT') 328 | except: 329 | return {'CANCELLED'} 330 | elif len(sel) == 1: 331 | # single vertex selected -> mirror vertex and create new face 332 | quad_from_vertex(bm, sel[0], context, event) 333 | if __name__ != '__main__': 334 | addon_prefs = context.preferences.addons[__name__].\ 335 | preferences 336 | if addon_prefs.autograb: 337 | bpy.ops.transform.translate('INVOKE_DEFAULT') 338 | elif len(sel) == 2: 339 | edges_sel = [ed for ed in bm.edges if ed.select] 340 | if len(edges_sel) != 1: 341 | # 2 vertices selected, but not on the same edge 342 | bpy.ops.mesh.edge_face_add() 343 | else: 344 | # single edge selected -> new face from linked open edges 345 | quad_from_edge(bm, edges_sel[0], context, event) 346 | 347 | return {'FINISHED'} 348 | 349 | 350 | # registration 351 | classes = ( 352 | MeshF2, 353 | F2AddonPreferences, 354 | ) 355 | 356 | addon_keymaps = [] 357 | 358 | 359 | def register(): 360 | # add operator 361 | for c in classes: 362 | bpy.utils.register_class(c) 363 | 364 | # add keymap entry 365 | kcfg = bpy.context.window_manager.keyconfigs.addon 366 | if kcfg: 367 | km = kcfg.keymaps.new(name='Mesh', space_type='EMPTY') 368 | kmi = km.keymap_items.new("mesh.f2", 'F', 'PRESS') 369 | addon_keymaps.append((km, kmi)) 370 | 371 | 372 | def unregister(): 373 | # remove keymap entry 374 | for km, kmi in addon_keymaps: 375 | km.keymap_items.remove(kmi) 376 | addon_keymaps.clear() 377 | 378 | # remove operator and preferences 379 | for c in reversed(classes): 380 | bpy.utils.unregister_class(c) 381 | 382 | 383 | if __name__ == "__main__": 384 | register() 385 | --------------------------------------------------------------------------------