# SPDX-License-Identifier: GPL-2.0-or-later # ---------------------------- ADAPTIVE DUPLIFACES --------------------------- # # ------------------------------- version 0.84 ------------------------------- # # # # Creates duplicates of selected mesh to active morphing the shape according # # to target faces. # # # # (c) Alessandro Zomparelli # # (2017) # # # # http://www.co-de-it.com/ # # # # ############################################################################ # import bpy from bpy.types import ( Operator, Panel, PropertyGroup, ) from bpy.props import ( BoolProperty, EnumProperty, FloatProperty, IntProperty, StringProperty, PointerProperty ) from mathutils import Vector, Quaternion, Matrix import numpy as np from math import * import random, time, copy import bmesh from .utils import * class polyhedra_wireframe(Operator): bl_idname = "object.polyhedra_wireframe" bl_label = "Tissue Polyhedra Wireframe" bl_description = "Generate wireframes around the faces.\ \nDoesn't works with boundary edges.\ \n(Experimental)" bl_options = {'REGISTER', 'UNDO'} thickness : FloatProperty( name="Thickness", default=0.1, min=0.001, soft_max=200, description="Wireframe thickness" ) subdivisions : IntProperty( name="Segments", default=1, min=1, soft_max=10, description="Max sumber of segments, used for the longest edge" ) #regular_sections : BoolProperty( # name="Regular Sections", default=False, # description="Turn inner loops into polygons" # ) dissolve_inners : BoolProperty( name="Dissolve Inners", default=False, description="Dissolve inner edges" ) @classmethod def poll(cls, context): try: #bool_tessellated = context.object.tissue_tessellate.generator != None ob = context.object return ob.type == 'MESH' and ob.mode == 'OBJECT'# and bool_tessellated except: return False def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) def execute(self, context): merge_dist = self.thickness*0.001 subs = self.subdivisions start_time = time.time() ob = context.object me = simple_to_mesh(ob) bm = bmesh.new() bm.from_mesh(me) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() # Subdivide edges proportional_subs = True if subs > 1 and proportional_subs: wire_length = [e.calc_length() for e in bm.edges] all_edges = list(bm.edges) max_segment = max(wire_length)/subs split_edges = [[] for i in range(subs+1)] for e, l in zip(all_edges, wire_length): split_edges[int(l//max_segment)].append(e) for i in range(2,subs): perc = {} for e in split_edges[i]: perc[e]=0.1 bmesh.ops.bisect_edges(bm, edges=split_edges[i], cuts=i, edge_percents=perc) ### Create double faces double_faces = [] double_layer_edge = [] double_layer_piece = [] for f in bm.faces: verts0 = [v.co for v in f.verts] verts1 = [v.co for v in f.verts] verts1.reverse() double_faces.append(verts0) double_faces.append(verts1) # Create new bmesh object and data layers bm1 = bmesh.new() # Create faces and assign Edge Layers for verts in double_faces: new_verts = [] for v in verts: vert = bm1.verts.new(v) new_verts.append(vert) bm1.faces.new(new_verts) bm1.verts.ensure_lookup_table() bm1.edges.ensure_lookup_table() bm1.faces.ensure_lookup_table() n_faces = len(bm.faces) n_doubles = len(bm1.faces) polyhedra = [] for e in bm.edges: done = [] # ERROR: Naked edges e_faces = len(e.link_faces) if e_faces < 2: bm.free() bm1.free() message = "Naked edges are not allowed" self.report({'ERROR'}, message) return {'CANCELLED'} edge_vec = e.verts[1].co - e.verts[0].co # run first face for i1 in range(e_faces-1): f1 = e.link_faces[i1] #edge_verts1 = [v.index for v in f1.verts if v in e.verts] verts1 = [v.index for v in f1.verts] va1 = verts1.index(e.verts[0].index) vb1 = verts1.index(e.verts[1].index) # check if order of the edge matches the order of the face dir1 = va1 == (vb1+1)%len(verts1) edge_vec1 = edge_vec if dir1 else -edge_vec # run second face faces2 = [] normals2 = [] for i2 in range(i1+1,e_faces): #for i2 in range(n_faces): if i1 == i2: continue f2 = e.link_faces[i2] f2.normal_update() #edge_verts2 = [v.index for v in f2.verts if v in e.verts] verts2 = [v.index for v in f2.verts] va2 = verts2.index(e.verts[0].index) vb2 = verts2.index(e.verts[1].index) # check if order of the edge matches the order of the face dir2 = va2 == (vb2+1)%len(verts2) # check for normal consistency if dir1 != dir2: # add face faces2.append(f2.index+1) normals2.append(f2.normal) else: # add flipped face faces2.append(-(f2.index+1)) normals2.append(-f2.normal) # find first polyhedra (positive) plane_x = f1.normal # normal plane_y = plane_x.cross(edge_vec1) # tangent face perp edge id1 = (f1.index+1) min_angle0 = 10000 # check consistent faces if id1 not in done: id2 = None min_angle = min_angle0 for i2, n2 in zip(faces2,normals2): v2 = flatten_vector(-n2, plane_x, plane_y) angle = vector_rotation(v2) if angle < min_angle: id2 = i2 min_angle = angle if id2: done.append(id2) new_poly = True # add to existing polyhedron for p in polyhedra: if id1 in p or id2 in p: new_poly = False if id2 not in p: p.append(id2) if id1 not in p: p.append(id1) break # start new polyhedron if new_poly: polyhedra.append([id1, id2]) # find second polyhedra (negative) plane_x = -f1.normal # normal plane_y = plane_x.cross(-edge_vec1) # tangent face perp edge id1 = -(f1.index+1) if id1 not in done: id2 = None min_angle = min_angle0 for i2, n2 in zip(faces2, normals2): v2 = flatten_vector(n2, plane_x, plane_y) angle = vector_rotation(v2) if angle < min_angle: id2 = -i2 min_angle = angle done.append(id2) add = True for p in polyhedra: if id1 in p or id2 in p: add = False if id2 not in p: p.append(id2) if id1 not in p: p.append(id1) break if add: polyhedra.append([id1, id2]) for i in range(len(bm1.faces)): for j in (False,True): if j: id = i+1 else: id = -(i+1) join = [] keep = [] for p in polyhedra: if id in p: join += p else: keep.append(p) if len(join) > 0: keep.append(list(dict.fromkeys(join))) polyhedra = keep for i, p in enumerate(polyhedra): for j in p: bm1.faces[j].material_index = i end_time = time.time() print('Tissue: Polyhedra wireframe, found {} polyhedra in {:.4f} sec'.format(len(polyhedra), end_time-start_time)) delete_faces = [] wireframe_faces = [] not_wireframe_faces = [] flat_faces = [] bm.free() #bmesh.ops.bisect_edges(bm1, edges=bm1.edges, cuts=3) end_time = time.time() print('Tissue: Polyhedra wireframe, subdivide edges in {:.4f} sec'.format(end_time-start_time)) bm1.faces.index_update() #merge_verts = [] for p in polyhedra: delete_faces_poly = [] wireframe_faces_poly = [] faces_id = [(f-1)*2 if f > 0 else (-f-1)*2+1 for f in p] faces_id_neg = [(-f-1)*2 if -f > 0 else (f-1)*2+1 for f in p] merge_verts = [] faces = [bm1.faces[f_id] for f_id in faces_id] for f in faces: delete = False if f.index in delete_faces: continue ''' cen = f.calc_center_median() for e in f.edges: mid = (e.verts[0].co + e.verts[1].co)/2 vec1 = e.verts[0].co - e.verts[1].co vec2 = mid - cen ang = Vector.angle(vec1,vec2) length = vec2.length #length = sin(ang)*length if length < self.thickness/2: delete = True ''' if False: sides = len(f.verts) for i in range(sides): v = f.verts[i].co v0 = f.verts[(i-1)%sides].co v1 = f.verts[(i+1)%sides].co vec0 = v0 - v vec1 = v1 - v ang = (pi - vec0.angle(vec1))/2 length = min(vec0.length, vec1.length)*sin(ang) if length < self.thickness/2: delete = True break if delete: delete_faces_poly.append(f.index) else: wireframe_faces_poly.append(f.index) merge_verts += [v for v in f.verts] if len(wireframe_faces_poly) < 2: delete_faces += faces_id not_wireframe_faces += faces_id_neg else: wireframe_faces += wireframe_faces_poly flat_faces += delete_faces_poly #wireframe_faces = list(dict.fromkeys(wireframe_faces)) bmesh.ops.remove_doubles(bm1, verts=merge_verts, dist=merge_dist) bm1.edges.ensure_lookup_table() bm1.faces.ensure_lookup_table() bm1.faces.index_update() wireframe_faces = [i for i in wireframe_faces if i not in not_wireframe_faces] wireframe_faces = list(dict.fromkeys(wireframe_faces)) flat_faces = list(dict.fromkeys(flat_faces)) end_time = time.time() print('Tissue: Polyhedra wireframe, merge and delete in {:.4f} sec'.format(end_time-start_time)) poly_me = me.copy() bm1.to_mesh(poly_me) poly_me.update() new_ob = bpy.data.objects.new("Polyhedra", poly_me) context.collection.objects.link(new_ob) ############# FRAME ############# bm1.faces.index_update() wireframe_faces = [bm1.faces[i] for i in wireframe_faces] original_faces = wireframe_faces #bmesh.ops.remove_doubles(bm1, verts=merge_verts, dist=0.001) # detect edge loops loops = [] boundaries_mat = [] neigh_face_center = [] face_normals = [] # compute boundary frames new_faces = [] wire_length = [] vert_ids = [] # append regular faces for f in original_faces: loop = list(f.verts) loops.append(loop) boundaries_mat.append([f.material_index for v in loop]) f.normal_update() face_normals.append([f.normal for v in loop]) push_verts = [] inner_loops = [] for loop_index, loop in enumerate(loops): is_boundary = loop_index < len(neigh_face_center) materials = boundaries_mat[loop_index] new_loop = [] loop_ext = [loop[-1]] + loop + [loop[0]] # calc tangents tangents = [] for i in range(len(loop)): # vertices vert0 = loop_ext[i] vert = loop_ext[i+1] vert1 = loop_ext[i+2] # edge vectors vec0 = (vert0.co - vert.co).normalized() vec1 = (vert.co - vert1.co).normalized() # tangent _vec1 = -vec1 _vec0 = -vec0 ang = (pi - vec0.angle(vec1))/2 normal = face_normals[loop_index][i] tan0 = normal.cross(vec0) tan1 = normal.cross(vec1) tangent = (tan0 + tan1).normalized()/sin(ang)*self.thickness/2 tangents.append(tangent) # calc correct direction for boundaries mult = -1 if is_boundary: dir_val = 0 for i in range(len(loop)): surf_point = neigh_face_center[loop_index][i] tangent = tangents[i] vert = loop_ext[i+1] dir_val += tangent.dot(vert.co - surf_point) if dir_val > 0: mult = 1 # add vertices for i in range(len(loop)): vert = loop_ext[i+1] area = 1 new_co = vert.co + tangents[i] * mult * area # add vertex new_vert = bm1.verts.new(new_co) new_loop.append(new_vert) vert_ids.append(vert.index) new_loop.append(new_loop[0]) # add faces #materials += [materials[0]] for i in range(len(loop)): v0 = loop_ext[i+1] v1 = loop_ext[i+2] v2 = new_loop[i+1] v3 = new_loop[i] face_verts = [v1,v0,v3,v2] if mult == -1: face_verts = [v0,v1,v2,v3] new_face = bm1.faces.new(face_verts) # Material by original edges piece_id = 0 new_face.select = True new_faces.append(new_face) wire_length.append((v0.co - v1.co).length) max_segment = max(wire_length)/self.subdivisions #for f,l in zip(new_faces,wire_length): # f.material_index = min(int(l/max_segment), self.subdivisions-1) bm1.verts.ensure_lookup_table() push_verts += [v.index for v in loop_ext] # At this point topology han been build, but not yet thickened end_time = time.time() print('Tissue: Polyhedra wireframe, frames in {:.4f} sec'.format(end_time-start_time)) bm1.verts.ensure_lookup_table() bm1.edges.ensure_lookup_table() bm1.faces.ensure_lookup_table() bm1.verts.index_update() ### Displace vertices ### circle_center = [0]*len(bm1.verts) circle_normal = [0]*len(bm1.verts) smooth_corners = [True] * len(bm1.verts) corners = [[] for i in range(len(bm1.verts))] normals = [0]*len(bm1.verts) vertices = [0]*len(bm1.verts) # Define vectors direction for f in new_faces: v0 = f.verts[0] v1 = f.verts[1] id = v0.index corners[id].append((v1.co - v0.co).normalized()) normals[id] = v0.normal.copy() vertices[id] = v0 smooth_corners[id] = False # Displace vertices for i, vecs in enumerate(corners): if len(vecs) > 0: v = vertices[i] nor = normals[i] ang = 0 for vec in vecs: ang += nor.angle(vec) ang /= len(vecs) div = sin(ang) if div == 0: div = 1 v.co += nor*self.thickness/2/div end_time = time.time() print('Tissue: Polyhedra wireframe, corners displace in {:.4f} sec'.format(end_time-start_time)) # Removing original flat faces flat_faces = [bm1.faces[i] for i in flat_faces] for f in flat_faces: f.material_index = self.subdivisions+1 for v in f.verts: if smooth_corners[v.index]: v.co += v.normal*self.thickness/2 smooth_corners[v.index] = False delete_faces = delete_faces + [f.index for f in original_faces] delete_faces = list(dict.fromkeys(delete_faces)) delete_faces = [bm1.faces[i] for i in delete_faces] bmesh.ops.delete(bm1, geom=delete_faces, context='FACES') bmesh.ops.remove_doubles(bm1, verts=bm1.verts, dist=merge_dist) bm1.faces.ensure_lookup_table() bm1.edges.ensure_lookup_table() bm1.verts.ensure_lookup_table() if self.dissolve_inners: bm1.edges.index_update() dissolve_edges = [] for f in bm1.faces: e = f.edges[2] if e not in dissolve_edges: dissolve_edges.append(e) bmesh.ops.dissolve_edges(bm1, edges=dissolve_edges, use_verts=True, use_face_split=True) all_lines = [[] for e in me.edges] all_end_points = [[] for e in me.edges] for v in bm1.verts: v.select_set(False) for f in bm1.faces: f.select_set(False) _me = me.copy() bm1.to_mesh(me) me.update() new_ob = bpy.data.objects.new("Wireframe", me) context.collection.objects.link(new_ob) for o in context.scene.objects: o.select_set(False) new_ob.select_set(True) context.view_layer.objects.active = new_ob me = _me bm1.free() bpy.data.meshes.remove(_me) #new_ob.location = ob.location new_ob.matrix_world = ob.matrix_world end_time = time.time() print('Tissue: Polyhedra wireframe in {:.4f} sec'.format(end_time-start_time)) return {'FINISHED'}