# ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. # # ##### END GPL LICENSE BLOCK ##### bl_info = { "name": "Topokit 2", "author": "dustractor", "version": (2, 0), "blender": (2, 60, 0), "location": "edit mesh vertices/edges/faces menus", "description": "", "warning": "", "wiki_url": "", "tracker_url": "", "category": "Mesh"} import bpy # In between calls, this stores any data that is expensive or static, # matched to the size of the mesh and the id of the operator that created it cachedata = dict() # and the object keeps the key to the cachedata bpy.types.Object.tkkey = bpy.props.IntVectorProperty(size=4) # just a mix-in for the operators... class meshpoller: @classmethod def poll(self,context): try: assert context.active_object.type == "MESH" except: return False finally: return True #BEGIN VERTICES SECTION # This one works similarly to normal 'grow' (ctrl + NUMPAD_PLUS), # except the original selection is not part of the result, # # 0--0--0 0--1--0 # | | | | | | # 0--1--0 --> 1--0--1 # | | | | | | # 0--0--0 0--1--0 # class MESH_OT_vneighbors_edgewise(meshpoller,bpy.types.Operator): bl_idname = "mesh.v2v_by_edge" bl_label = "Neighbors by Edge" bl_options = {"REGISTER","UNDO"} def execute(self,context): global cachedata bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object mesh = obj.data meshkey = (len(mesh.vertices),len(mesh.edges),len(mesh.polygons),id(self)) next_state = bytearray(meshkey[0]) if (meshkey == obj.tkkey) and (meshkey in cachedata): vert_to_vert_map,prev_state = cachedata[meshkey] else: vert_to_vert_map = {i:{} for i in range(meshkey[0])} for a,b in mesh.edge_keys: vert_to_vert_map[a][b] = 1 vert_to_vert_map[b][a] = 1 obj.tkkey = meshkey prev_state = None if not prev_state: selected_vert_indices = filter(lambda _:mesh.vertices[_].select,range(len(mesh.vertices))) else: selected_vert_indices = filter(lambda _:mesh.vertices[_].select and not prev_state[_],range(len(mesh.vertices))) for v in selected_vert_indices: for neighbor_index in vert_to_vert_map[v]: next_state[neighbor_index] = True mesh.vertices.foreach_set("select",next_state) cachedata[meshkey] = (vert_to_vert_map,next_state) bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} # This one is an alternate / counterpart to the previous. # Think: diagonal opposite corners of a quad # NOTE: does not apply to a triangle, since verts have no 'opposite' # # 0--0--0 1--0--1 # | | | | | | # 0--1--0 --> 0--0--0 # | | | | | | # 0--0--0 1--0--1 # class MESH_OT_vneighbors_facewise(meshpoller,bpy.types.Operator): bl_idname = "mesh.v2v_facewise" bl_label = "Neighbors by Face - Edge" bl_options = {"REGISTER","UNDO"} def execute(self,context): global cachedata bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object mesh = obj.data meshkey = (len(mesh.vertices),len(mesh.edges),len(mesh.polygons),id(self)) next_state = bytearray(meshkey[0]) if (meshkey == obj.tkkey) and (meshkey in cachedata): vert_to_vert_map = cachedata[meshkey] else: vert_to_vert_map = {i:{} for i in range(meshkey[0])} for a,b in mesh.edge_keys: vert_to_vert_map[a][b] = 1 vert_to_vert_map[b][a] = 1 obj.tkkey = meshkey faces = filter(lambda face:(len(face.vertices)==4) and (face.select == False),mesh.polygons) for f in faces: has = False t = set() for v in f.vertices: if mesh.vertices[v].select: has = True t.update(vert_to_vert_map[v]) if has: for v in f.vertices: if not mesh.vertices[v].select: if v not in t: next_state[v]=1 mesh.vertices.foreach_set("select",next_state) cachedata[meshkey] = vert_to_vert_map bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} def vvmenuitem(self,context): self.layout.operator(MESH_OT_vneighbors_edgewise.bl_idname) self.layout.operator(MESH_OT_vneighbors_facewise.bl_idname) #for the sake of completeness, yes there is one alg missing - one for both... #END VERTICES SECTION #BEGIN EDGES SECTION # +--0--+--0--+--0--+ +--0--+--0--+--0--+ # | | | | | | | | # 0 0 0 0 0 1 1 0 # | | | | | | | | # +--0--+--1--+--0--+ ---> +--0--+--0--+--0--+ # | | | | | | | | # 0 0 0 0 0 1 1 0 # | | | | | | | | # +--0--+--0--+--0--+ +--0--+--0--+--0--+ class MESH_OT_eneighbors_shared_v_f(meshpoller,bpy.types.Operator): bl_idname = "mesh.e2e_evfe" bl_label = "Neighbors by Vert+Face" bl_options = {"REGISTER","UNDO"} def execute(self,context): global cachedata bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object mesh = obj.data meshkey = (len(mesh.vertices),len(mesh.edges),len(mesh.polygons),id(self)) state_mask = bytearray(meshkey[1]) if (meshkey == obj.tkkey) and (meshkey in cachedata): edge_to_edges_dict = cachedata else: edge_key_to_index = {k:i for i,k in enumerate(mesh.edge_keys)} edge_to_edges_dict = {i:set() for i in range(len(mesh.edges))} for f in mesh.polygons: fed=[edge_key_to_index[k] for k in f.edge_keys] for k in f.edge_keys: edge_to_edges_dict[edge_key_to_index[k]].update(fed) obj.tkkey = meshkey for e in filter(lambda _:mesh.edges[_].select,edge_to_edges_dict): k1 = set(mesh.edges[e].key) for n in edge_to_edges_dict[e]: k2 = set(mesh.edges[n].key) if not k1.isdisjoint(k2): state_mask[n] = True for e in mesh.edges: e.select ^= state_mask[e.index] cachedata[meshkey] = edge_key_to_index bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} # +--0--+--0--+--0--+ +--0--+--0--+--0--+ # | | | | | | | | # 0 0 0 0 0 1 1 0 # | | | | | | | | # +--0--+--1--+--0--+ ---> +--1--+--0--+--1--+ # | | | | | | | | # 0 0 0 0 0 1 1 0 # | | | | | | | | # +--0--+--0--+--0--+ +--0--+--0--+--0--+ class MESH_OT_eneighbors_shared_v(meshpoller,bpy.types.Operator): bl_idname = "mesh.e2e_eve" bl_label = "Neighbors by Vert" bl_options = {"REGISTER","UNDO"} def execute(self,context): bpy.ops.object.mode_set(mode="OBJECT") mesh = context.active_object.data state_mask = bytearray(len(mesh.edges)) for e in mesh.edges: state_mask[e.index] = mesh.vertices[e.vertices[0]].select ^ mesh.vertices[e.vertices[1]].select mesh.edges.foreach_set('select',state_mask) bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} # +--0--+--0--+--0--+ +--0--+--1--+--0--+ # | | | | | | | | # 0 0 0 0 0 1 1 0 # | | | | | | | | # +--0--+--1--+--0--+ ---> +--0--+--0--+--0--+ # | | | | | | | | # 0 0 0 0 0 1 1 0 # | | | | | | | | # +--0--+--0--+--0--+ +--0--+--1--+--0--+ class MESH_OT_eneighbors_shared_f(meshpoller,bpy.types.Operator): bl_idname = "mesh.e2e_efe" bl_label = "Neighbors by Face" bl_options = {"REGISTER","UNDO"} def execute(self,context): global cachedata bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object mesh = obj.data meshkey = (len(mesh.vertices),len(mesh.edges),len(mesh.polygons),id(self)) if (meshkey == obj.tkkey) and (meshkey in cachedata): edge_to_edges_dict = cachedata else: edge_key_to_index = {k:i for i,k in enumerate(mesh.edge_keys)} edge_to_edges_dict = {i:set() for i in range(len(mesh.edges))} for f in mesh.polygons: fed=[edge_key_to_index[k] for k in f.edge_keys] for k in f.edge_keys: edge_to_edges_dict[edge_key_to_index[k]].update(fed) obj.tkkey = meshkey state_mask,esel = (bytearray(meshkey[1]),bytearray(meshkey[1])) mesh.edges.foreach_get('select',esel) for e in filter(lambda _:mesh.edges[_].select,range(meshkey[1])): for n in edge_to_edges_dict[e]: state_mask[n] = 1 for e in range(meshkey[1]): esel[e] ^= state_mask[e] mesh.edges.foreach_set('select',esel) cachedata[meshkey] = edge_to_edges_dict bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} # notice that on these next two, the original selection stays # +--0--+--0--+--0--+ +--0--+--1--+--0--+ # | | | | | | | | # 0 0 0 0 0 0 0 0 # | | | | | | | | # +--0--+--1--+--0--+ ---> +--0--+--1--+--0--+ # | | | | | | | | # 0 0 0 0 0 0 0 0 # | | | | | | | | # +--0--+--0--+--0--+ +--0--+--1--+--0--+ class MESH_OT_eneighbors_shared_f_notv(meshpoller,bpy.types.Operator): bl_idname = "mesh.e2e_efnve" bl_label = "Lateral Neighbors" bl_options = {"REGISTER","UNDO"} def execute(self,context): global cachedata bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object mesh = obj.data meshkey = (len(mesh.vertices),len(mesh.edges),len(mesh.polygons),id(self)) state_mask = bytearray(meshkey[1]) if (meshkey == obj.tkkey) and (meshkey in cachedata): edge_to_face_map,edge_key_to_index = cachedata[meshkey] else: edge_key_to_index = {} edge_to_face_map = {i:set() for i in range(meshkey[1])} for i,k in enumerate(mesh.edge_keys): edge_key_to_index[k] = i for f in mesh.polygons: for k in f.edge_keys: edge_to_face_map[edge_key_to_index[k]].add(f.index) obj.tkkey = meshkey selected_edge_indices = filter(lambda _:mesh.edges[_].select,range(meshkey[1])) for e in selected_edge_indices: for f in edge_to_face_map[e]: for k in mesh.polygons[f].edge_keys: hasv_in = False for v in mesh.edges[e].key: if v in k: hasv_in = True if hasv_in: continue else: state_mask[edge_key_to_index[k]] = True for e in filter(lambda _:state_mask[_],range(meshkey[1])): mesh.edges[e].select |= state_mask[e] cachedata[meshkey] = (edge_to_face_map,edge_key_to_index) bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} # +--0--+--0--+--0--+ +--0--+--0--+--0--+ # | | | | | | | | # 0 0 0 0 0 0 0 0 # | | | | | | | | # +--0--+--1--+--0--+ ---> +--1--+--1--+--1--+ # | | | | | | | | # 0 0 0 0 0 0 0 0 # | | | | | | | | # +--0--+--0--+--0--+ +--0--+--0--+--0--+ class MESH_OT_eneighbors_shared_v_notf(meshpoller,bpy.types.Operator): bl_idname = "mesh.e2e_evnfe" bl_label = "Longitudinal Edges" bl_options = {"REGISTER","UNDO"} def execute(self,context): global cachedata bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object mesh = obj.data meshkey = (len(mesh.vertices),len(mesh.edges),len(mesh.polygons),id(self)) state_mask = bytearray(meshkey[1]) vstate = bytearray(meshkey[0]) mesh.vertices.foreach_get('select',vstate) if (meshkey == obj.tkkey) and (meshkey in cachedata): edge_to_face_map,vert_to_vert_map,edge_key_to_index = cachedata[meshkey] else: edge_key_to_index = {} vert_to_vert_map = {i:set() for i in range(meshkey[0])} edge_to_face_map = {i:set() for i in range(meshkey[1])} for i,k in enumerate(mesh.edge_keys): edge_key_to_index[k] = i vert_to_vert_map[k[0]].add(k[1]) vert_to_vert_map[k[1]].add(k[0]) for f in mesh.polygons: for k in f.edge_keys: edge_to_face_map[edge_key_to_index[k]].add(f.index) obj.tkkey = meshkey selected_edge_indices = filter(lambda _:mesh.edges[_].select,range(meshkey[1])) for e in selected_edge_indices: for v in mesh.edges[e].key: state_mask[v] ^=1 for f in edge_to_face_map[e]: for v in mesh.polygons[f].vertices: vstate[v] = 1 for v in filter(lambda _:state_mask[_],range(meshkey[1])): for n in vert_to_vert_map[v]: if not vstate[n] and (n != v): mesh.edges[edge_key_to_index[(min(v,n),max(v,n))]].select = True cachedata[meshkey] = (edge_to_face_map,vert_to_vert_map,edge_key_to_index) bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} #deselects faces, leaving only edges selected class MESH_OT_just_the_edges(meshpoller,bpy.types.Operator): bl_idname = "mesh.je" bl_label = "Just the Edge Selection" bl_options = {"REGISTER","UNDO"} def execute(self,context): global cachedata bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object mesh = obj.data meshkey = (len(mesh.vertices),len(mesh.edges),len(mesh.polygons),id(self)) state_mask = bytearray(meshkey[1]) if (meshkey == obj.tkkey) and (meshkey in cachedata): edge_key_to_index = cachedata[meshkey] else: edge_key_to_index = {k:i for i,k in enumerate(mesh.edge_keys)} obj.tkkey = meshkey for f in filter(lambda _:mesh.polygons[_].select,range(meshkey[2])): for k in mesh.polygons[f].edge_keys: state_mask[edge_key_to_index[k]] = 1 for e in range(meshkey[1]): mesh.edges[e].select ^= state_mask[e] cachedata[meshkey] = edge_key_to_index bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} # deselects edges which are at the edge of a face-selection, # causing selection to 'shrink in' class MESH_OT_inner_edges(meshpoller,bpy.types.Operator): bl_idname = "mesh.ie" bl_label = "Inner Edge Selection" bl_options = {"REGISTER","UNDO"} def execute(self,context): global cachedata bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object mesh = obj.data meshkey = (len(mesh.vertices),len(mesh.edges),len(mesh.polygons),id(self)) state_mask = bytearray(meshkey[1]) if (meshkey == obj.tkkey) and (meshkey in cachedata): edge_to_face_map = cachedata[meshkey] else: edge_key_to_index = {k:i for i,k in enumerate(mesh.edge_keys)} edge_to_face_map = {i:set() for i in range(meshkey[1])} for f in mesh.polygons: for k in f.edge_keys: edge_to_face_map[edge_key_to_index[k]].add(f.index) obj.tkkey = meshkey for e in filter(lambda _:mesh.edges[_].select,range(meshkey[1])): for f in edge_to_face_map[e]: if mesh.polygons[f].select: state_mask[e] ^=1 for e in range(meshkey[1]): mesh.edges[e].select ^= state_mask[e] cachedata[meshkey] = edge_to_face_map bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} def eemenuitem(self,context): self.layout.operator(MESH_OT_eneighbors_shared_v_f.bl_idname) self.layout.operator(MESH_OT_eneighbors_shared_v.bl_idname) self.layout.operator(MESH_OT_eneighbors_shared_f.bl_idname) self.layout.operator(MESH_OT_eneighbors_shared_f_notv.bl_idname) self.layout.operator(MESH_OT_eneighbors_shared_v_notf.bl_idname) self.layout.operator(MESH_OT_just_the_edges.bl_idname) self.layout.operator(MESH_OT_inner_edges.bl_idname) #END EDGES SECTION #BEGIN FACES SECTION # here is another one which functions very similarly to the ctrl+NUMPAD_PLUS 'growth' # but it deselects the original selection, of course. # This would be your checkerboard-type growth. # [0][0][0] [0][1][0] # [0][1][0] ---> [1][0][1] # [0][0][0] [0][1][0] class MESH_OT_fneighbors_shared_e(meshpoller,bpy.types.Operator): bl_idname = "mesh.f2f_fef" bl_label = "Neighbors by Edge" bl_options = {"REGISTER","UNDO"} def execute(self,context): global cachedata bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object mesh = obj.data meshkey = (len(mesh.vertices),len(mesh.edges),len(mesh.polygons),id(self)) if (meshkey == obj.tkkey) and (meshkey in cachedata): face_to_face_map = cachedata[meshkey] else: edge_key_to_index = {k:i for i,k in enumerate(mesh.edge_keys)} edge_to_face_map = {i:set() for i in range(meshkey[1])} for f in mesh.polygons: for k in f.edge_keys: edge_to_face_map[edge_key_to_index[k]].add(f.index) face_to_face_map = {i:set() for i in range(meshkey[2])} for f in mesh.polygons: for k in f.edge_keys: face_to_face_map[f.index].update(edge_to_face_map[edge_key_to_index[k]]) obj.tkkey = meshkey mask_state = bytearray(meshkey[2]) for f in filter(lambda _:mesh.polygons[_].select,range(meshkey[2])): for n in face_to_face_map[f]: mask_state[n] = True for f in range(meshkey[2]): mesh.polygons[f].select ^= mask_state[f] cachedata[meshkey] = face_to_face_map bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} # [0][0][0] [1][0][1] # [0][1][0] ---> [0][0][0] # [0][0][0] [1][0][1] class MESH_OT_fneighbors_shared_v_note(meshpoller,bpy.types.Operator): bl_idname = "mesh.f2f_fvnef" bl_label = "Neighbors by Vert not Edge" bl_options = {"REGISTER","UNDO"} def execute(self,context): global cachedata bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object mesh = obj.data meshkey = (len(mesh.vertices),len(mesh.edges),len(mesh.polygons),id(self)) if (meshkey == obj.tkkey) and (meshkey in cachedata): edge_key_to_index = cachedata[meshkey] else: edge_key_to_index = {k:i for i,k in enumerate(mesh.edge_keys)} obj.tkkey = meshkey state_mask = bytearray(meshkey[2]) face_verts = set() for f in filter(lambda _:mesh.polygons[_].select,range(meshkey[2])): face_verts.update(mesh.polygons[f].vertices) for f in filter(lambda _:not mesh.polygons[_].select,range(meshkey[2])): ct = 0 for v in mesh.polygons[f].vertices: ct += (v in face_verts) if ct == 1: state_mask[f] = 1 mesh.polygons.foreach_set('select',state_mask) cachedata[meshkey] = edge_key_to_index bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} # http://en.wikipedia.org/wiki/Conway's_Game_of_Life class MESH_OT_conway(meshpoller,bpy.types.Operator): bl_idname = "mesh.conway" bl_label = "Conway" bl_options = {"REGISTER","UNDO"} def execute(self,context): global cachedata bpy.ops.object.mode_set(mode="OBJECT") obj = context.active_object mesh = obj.data meshkey = (len(mesh.vertices),len(mesh.edges),len(mesh.polygons),id(self)) if (meshkey == obj.tkkey) and (meshkey in cachedata): vert_to_face_map = cachedata[meshkey] else: vert_to_face_map = {i:set() for i in range(meshkey[0])} for f in mesh.polygons: for v in f.vertices: vert_to_face_map[v].add(f.index) obj.tkkey = meshkey sel = set() uns = set() F = {i:set() for i in range(meshkey[2])} for f in range(meshkey[2]): for v in mesh.polygons[f].vertices: for n in filter(lambda _: mesh.polygons[_].select and (_ != f),vert_to_face_map[v]): F[f].add(n) for f in F: if len(F[f]) == 3: sel.add(f) elif len(F[f]) != 2: uns.add(f) for f in range(meshkey[2]): if f in sel: mesh.polygons[f].select = True if f in uns: mesh.polygons[f].select = False cachedata[meshkey] = vert_to_face_map bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} def ffmenuitem(self,context): self.layout.operator(MESH_OT_fneighbors_shared_e.bl_idname) self.layout.operator(MESH_OT_fneighbors_shared_v_note.bl_idname) self.layout.operator(MESH_OT_conway.bl_idname) def register(): bpy.utils.register_module(__name__) bpy.types.VIEW3D_MT_edit_mesh_vertices.append(vvmenuitem) bpy.types.VIEW3D_MT_edit_mesh_edges.append(eemenuitem) bpy.types.VIEW3D_MT_edit_mesh_faces.append(ffmenuitem) def unregister(): bpy.utils.unregister_module(__name__) bpy.types.VIEW3D_MT_edit_mesh_vertices.remove(vvmenuitem) bpy.types.VIEW3D_MT_edit_mesh_edges.remove(eemenuitem) bpy.types.VIEW3D_MT_edit_mesh_faces.remove(ffmenuitem) if __name__ == "__main__": register()