def execute(self, context): bpy.ops.object.editmode_toggle() bm = bmesh.new() bm.from_mesh(context.active_object.data) # For easy access to verts, edges, and faces: bVerts = bm.verts bEdges = bm.edges bFaces = bm.faces fVerts = [] normal = None # Find the selected face. This will provide the plane to project onto: for f in bFaces: if f.select: for v in f.verts: fVerts.append(v) f.normal_update() normal = f.normal f.select = False break for e in bEdges: if e.select: v1 = e.verts[0] v2 = e.verts[1] if v1 in fVerts and v2 in fVerts: e.select = False continue intersection = intersect_line_plane(v1.co, v2.co, fVerts[0].co, normal) if intersection != None: d1 = distance_point_to_plane(v1.co, fVerts[0].co, normal) d2 = distance_point_to_plane(v2.co, fVerts[0].co, normal) # If they have different signs, then the edge crosses the plane: if abs(d1 + d2) < abs(d1 - d2): # Make the first vertice the positive vertice: if d1 < d2: v2, v1 = v1, v2 new = list(bmesh.utils.edge_split(e, v1, 0.5)) new[1].co = intersection e.select = False new[0].select = False if self.pos: bEdges.remove(new[0]) if self.neg: bEdges.remove(e) bm.to_mesh(context.active_object.data) bpy.ops.object.editmode_toggle() ## bpy.ops.mesh.remove_doubles() return {'FINISHED'}
def setup_anchors(points, size, max_distance=3): n = len(points) neighbours_length = 0 tries = 0 while neighbours_length < 20 and tries < 5: random_start_point = points[randint(0, len(points) - 1)] neighbours = [ p for p in points if (p - random_start_point).length < max_distance ] neighbours_length = len(neighbours) tries += 1 if tries < 5: random_indices = sample([i for i in range(len(neighbours))], 20) random_selection = [neighbours[i] for i in random_indices] plane_co, plane_normal = get_plane_from_points(random_selection) new_points = [] barycenter = Vector() shuffle(points) for i, point in enumerate(neighbours): barycenter += point if (point - barycenter / (i + 1)).length < max_distance and abs( geometry.distance_point_to_plane( point, plane_co, plane_normal)) < .6 * size: new_points.append(point) new_points = convex_indexing(new_points, plane_normal) return new_points, plane_normal else: return [], Vector()
def is_coplanar_stroke(s, tol=0.0002, verbose=False) -> bool: ''' Get a GP stroke object and tell if all points are coplanar (with a tolerance). ''' if len(s.points) < 4: # less than 4 points is necessarily coplanar return True obj = bpy.context.object mat = obj.matrix_world pct = len(s.points) a = mat @ s.points[0].co b = mat @ s.points[pct//3].co c = mat @ s.points[pct//3*2].co ab = b-a ac = c-a # get normal (perpendicular Vector) plane_no = ab.cross(ac)#.normalized() for i, p in enumerate(s.points): ## let a tolerance value of at least 0.0002 # if abs(geometry.distance_point_to_plane(p.co, a, plane_no)) > tol: if abs(geometry.distance_point_to_plane(mat @ p.co, a, plane_no)) > tol: if verbose: print(f'point{i} is not co-planar') # (distance to plane {geometry.distance_point_to_plane(p.co, a, plane_no)}) return False return True
def select_geometry(self, bm): for f in bm.faces: f.select = False bm.select_flush(False) if self.select_type == "NON-MANIFOLD": edges = [e for e in bm.edges if not e.is_manifold] for e in edges: e.select = True elif self.select_type == "NON-PLANAR": faces = [f for f in bm.faces if len(f.verts) > 3] for f in faces: distances = [distance_point_to_plane(v.co, f.calc_center_median(), f.normal) for v in f.verts] if any([d for d in distances if abs(d) > self.planar_threshold]): f.select_set(True) elif self.select_type == "TRIS": faces = [f for f in bm.faces if len(f.verts) == 3] for f in faces: f.select = True elif self.select_type == "NGONS": faces = [f for f in bm.faces if len(f.verts) > 4] for f in faces: f.select = True
def execute(self, context): C = context D = bpy.data ob = C.active_object #if bpy.app.debug != True: # bpy.app.debug = True # if C.active_object.show_extra_indices != True: # C.active_object.show_extra_indices = True if ob.mode == 'OBJECT': me = C.object.data bm = bmesh.new() bm.from_mesh(me) else: obj = C.edit_object me = obj.data bm = bmesh.from_edit_mesh(me) bm.select_history.validate() if len(bm.select_history) < 3: self.report({'INFO'}, 'Pick three vertices first') return {'CANCELLED'} points3Index = [] points3 = [] _ordering = bm.select_history if self.ref_order == "first" else list( bm.select_history)[::-1] for i in _ordering: if len(points3) >= 3: break elif isinstance(i, bmesh.types.BMVert): points3.append(i.co) points3Index.append(i.index) print(points3Index) if len(points3) < 3: self.report({'INFO'}, 'At least three vertices are needed being selected') return {'CANCELLED'} points3Normal = normal(*points3) for v in bm.verts: if v.select and v.index not in points3Index: _move = True if self.filter_distance > 0.0: _move = abs( distance_point_to_plane( v.co, points3[0], points3Normal)) < self.filter_distance if _move == True: v.co = intersect_line_plane(v.co, v.co + points3Normal, points3[0], points3Normal) if ob.mode == 'OBJECT': bm.to_mesh(me) bm.free() else: bmesh.update_edit_mesh(me, True) return {'FINISHED'}
def __prepare_terrain_points__(self): """Reverses the order of terrain points if last point is closer to the node in it's forward direction. """ for variant_index in self.__tp_per_variant: # no terrain points for given variant, nothing to order reverse if len(self.__tp_per_variant[variant_index]) <= 0: continue # now if tail is closer to node on it's forward axis, we reverse list plane_co = Vector(self.__position) plane_no = Vector(self.__direction) head_distance = distance_point_to_plane(self.__tp_per_variant[variant_index][0][0], plane_co, plane_no) tail_distance = distance_point_to_plane(self.__tp_per_variant[variant_index][-1][0], plane_co, plane_no) if not (head_distance <= tail_distance - 0.001): # 0.001 taken from Maya exporter self.__tp_per_variant[variant_index].reverse()
def compute_point_tri_dist(p, plane_origin, plane_a, plane_b, norm, tolerance): dist = distance_point_to_plane(p, plane_origin, norm) closest = p - norm * dist side = dist > 0 dist_abs = abs(dist) is_in_plane = dist_abs < tolerance closest_in_plane = intersect_point_tri(closest, plane_origin , plane_a, plane_b) is_in_segment = is_in_plane and closest_in_plane return dist_abs, is_in_segment, is_in_plane, list(closest), bool(closest_in_plane), side
def execute(self, context): """ method called from ui """ # init local catt_export = context.scene.catt_export # check for non flat faces if self.arg == 'check_nonflat_faces': # discard if no object selected sel_objects = bpy.context.selected_objects if( len(sel_objects) == 0 ): self.report({'INFO'}, 'No object selected.') return {'CANCELLED'} # switch to edit mode bpy.ops.object.mode_set(mode = 'EDIT') # context = bpy.context # obj = context.edit_object mesh = bpy.context.edit_object.data # select None bpy.ops.mesh.select_all(action='DESELECT') bm = bmesh.from_edit_mesh(mesh) ngons = [f for f in bm.faces if len(f.verts) > 3] # init locals has_non_flat_faces = False planar_tolerance = 1e-6 # loop over faces for ngon in ngons: # define a plane from first 3 points co = ngon.verts[0].co norm = normal([v.co for v in ngon.verts[:3]]) # set face selected ngon.select = not all( [abs(distance_point_to_plane(v.co, co, norm)) < planar_tolerance for v in ngon.verts[3:]] ) # flag at least one non flat face detected if ngon.select: has_non_flat_faces = True # update mesh bmesh.update_edit_mesh(mesh) # disable edit mode if no non-flat face detected if not has_non_flat_faces: bpy.ops.object.mode_set(mode = 'OBJECT') self.report({'INFO'}, 'No non-flat face detected.') return {'FINISHED'}
def point_inside_loop_almost3D(pt, verts, no, p_pt = None, threshold = .01, debug = False): ''' http://blenderartists.org/forum/showthread.php?259085-Brainstorming-for-Virtual-Buttons&highlight=point+inside+loop args: pt - 3d point to test of type Mathutils.Vector verts - 3d points representing the loop TODO: verts[0] == verts[-1] or implied? list with elements of type Mathutils.Vector no - plane normal plane_pt - a point on the plane. if None, COM of verts will be used threshold - maximum distance to consider pt "coplanar" default = .01 debug - Bool, default False. Will print performance if True return: Bool True if point is inside the loop ''' if debug: start = time.time() #sanity checks if len(verts) < 3: print('loop must have 3 verts to be a loop and even then its sketchy') return False if no.length == 0: print('normal vector must be non zero') return False if not p_pt: p_pt = get_com(verts) if distance_point_to_plane(pt, p_pt, no) > threshold: return False (X_prime, Y_prime) = generic_axes_from_plane_normal(p_pt, no) verts_prime = [] for v in verts: v_trans = v - p_pt vx = v_trans.dot(X_prime) vy = v_trans.dot(Y_prime) verts_prime.append(Vector((vx, vy))) #transform the test point into the new plane x,y space pt_trans = pt - p_pt pt_prime = Vector((pt_trans.dot(X_prime), pt_trans.dot(Y_prime))) pt_in_loop = point_inside_loop2d(verts_prime, pt_prime) return pt_in_loop
def _calc_lattice_axis_length(vertex_coords, plane_co, plane_no): max_dist = 0 min_dist = 0 for v in vertex_coords: dist = distance_point_to_plane(v, plane_co, plane_no) if dist > max_dist: max_dist = dist elif dist < min_dist: min_dist = dist length = max_dist + abs(min_dist) return length
def volume(obj): """ Compute the volume of the object `obj`. The mesh has to be manifold, and face normals coherently oriented towards the outside. """ bpy.ops.object.mode_set(mode='OBJECT') volume = 0 orig = Vector((0, 0, 0)) for face in obj.data.polygons: if face.normal.length == 0.0: continue distance = distance_point_to_plane(orig, face.center, face.normal) # minus sign, assuming normals are directed towards the outside volume -= face.area * distance / 3 return volume
def get_coplanar_stroke_vector(s, tol=0.0002, verbose=False): ''' Get a GP stroke object and tell if all points are coplanar (with a tolerance). return normal vector if coplanar else None ''' if len(s.points) < 4: # less than 4 points is necessarily coplanar # but not "evaluable" so return None if verbose: print('less than 4 points') return obj = bpy.context.object mat = obj.matrix_world pct = len(s.points) a = mat @ s.points[0].co b = mat @ s.points[pct//3].co c = mat @ s.points[pct//3*2].co """ a = s.points[0].co b = s.points[1].co c = s.points[-2].co """ ab = b-a ac = c-a #get normal (perpendicular Vector) plane_no = ab.cross(ac)#.normalized() val = plane_no # print('plane_no: ', plane_no) for i, p in enumerate(s.points): ## let a tolerance value of at least 0.0002 maybe more # if abs(geometry.distance_point_to_plane(p.co, a, plane_no)) > tol: if abs(geometry.distance_point_to_plane(mat @ p.co, a, plane_no)) > tol: if verbose: print(f'point{i} is not co-planar') # (distance to plane {geometry.distance_point_to_plane(p.co, a, plane_no)}) return # val = None return val
def align(aligny=Vector((0, 1, 0)), alignz=Vector((0, 0, 1))): ''' Get a Rotation Matrix. The Matrix Local +Y Axis gets aligned with aligny. The +Z Local Axis gets aligned with alignz as best as possible, without disturbing the Y alignment. This implementation looks a little brutish to the Coders eyes. Better ideas are very welcome. ''' X = Vector((1, 0, 0)) Y = Vector((0, 1, 0)) Z = Vector((0, 0, 1)) if alignz.length == 0: alignz = Z.copy() mat = Matrix().to_3x3() #Align local-Y axis with aligny axis, angle = axisangle(Y, aligny) if axis.length == 0: axis = X rot_to_y = Matrix.Rotation(angle, 3, axis) bm1 = get_axis_aligned_bm(rot_to_y) #Align local-Z with projected alignz eul = rot_to_y.to_euler() target_localx = aligny.cross(alignz).normalized() target_localz = target_localx.cross(aligny).normalized() angle = target_localz.angle(bm1.verts[2].co) ### NEED SOME TEST FOR ANGLE FLIPPING eul.rotate_axis('Y', angle) mat = eul.to_matrix().to_4x4() ### Test if rotation is good bmf = get_axis_aligned_bm(mat) dist = distance_point_to_plane(bmf.verts[2].co, target_localz, target_localz.cross(alignz)) error = 1e-06 ### Flip the angle if abs(dist) > error: eul = rot_to_y.to_euler() eul.rotate_axis('Y', -angle) mat = eul.to_matrix().to_4x4() bm1.free() bmf.free() return mat.to_4x4()
def _calc_lattice_axis_midpoint_location(vertex_coords, plane_co, plane_no): max_dist = 0 min_dist = 0 max_vert_co = Vector((0, 0, 0)) min_vert_co = Vector((0, 0, 0)) for v in vertex_coords: dist = distance_point_to_plane(v, plane_co, plane_no) if dist > max_dist: max_dist = dist max_vert_co = v elif dist < min_dist: min_dist = dist min_vert_co = v midpoint_co = (max_vert_co + min_vert_co) / 2 if midpoint_co == Vector((0, 0, 0)): midpoint_co = plane_co return midpoint_co
def sync_cameras(ainode, camera, depsgraph): ob_eval = camera.evaluated_get(depsgraph) data = ob_eval.data # Basic stuff AiNodeSetStr(ainode, "name", ob_eval.name) AiNodeSetMatrix(ainode, "matrix", generate_aimatrix(ob_eval.matrix_world)) # Lens data fov = calc_horizontal_fov(ob_eval) AiNodeSetFlt(ainode, "fov", math.degrees(fov)) AiNodeSetFlt(ainode, "exposure", data.arnold.exposure) # DOF if data.dof.focus_object: distance = geometry.distance_point_to_plane( ob_eval.matrix_world.to_translation(), data.dof_object.matrix_world.to_translation(), ob_eval.matrix_world.col[2][:3]) else: distance = data.dof.focus_distance AiNodeSetFlt(ainode, "aperture_size", data.arnold.aperture_size) AiNodeSetInt(ainode, "aperture_blades", data.arnold.aperture_blades) AiNodeSetFlt(ainode, "aperture_rotation", data.arnold.aperture_rotation) AiNodeSetFlt(ainode, "aperture_blade_curvature", data.arnold.aperture_blade_curvature) AiNodeSetFlt(ainode, "aperture_aspect_ratio", data.arnold.aperture_aspect_ratio) # Clipping AiNodeSetFlt(ainode, "near_clip", data.clip_start) AiNodeSetFlt(ainode, "far_clip", data.clip_end) # Shutter AiNodeSetFlt(ainode, "shutter_start", data.arnold.shutter_start) AiNodeSetFlt(ainode, "shutter_end", data.arnold.shutter_end) AiNodeSetStr(ainode, "shutter_type", data.arnold.shutter_type) AiNodeSetStr(ainode, "rolling_shutter", data.arnold.rolling_shutter) AiNodeSetFlt(ainode, "rolling_shutter_duration", data.arnold.rolling_shutter_duration)
def align(aligny=Vector((0,1,0)), alignz=Vector((0,0,1))): ''' Get a Rotation Matrix. The Matrix Local +Y Axis gets aligned with aligny. The +Z Local Axis gets aligned with alignz as best as possible, without disturbing the Y alignment. This implementation looks a little brutish to the Coders eyes. Better ideas are very welcome. ''' X=Vector((1,0,0)) Y=Vector((0,1,0)) Z=Vector((0,0,1)) if alignz.length == 0: alignz = Z.copy() mat = Matrix().to_3x3() #Align local-Y axis with aligny axis, angle = axisangle(Y, aligny) if axis.length == 0: axis = X rot_to_y = Matrix.Rotation(angle,3,axis) bm1 = get_axis_aligned_bm(rot_to_y) #Align local-Z with projected alignz eul = rot_to_y.to_euler() target_localx = aligny.cross(alignz).normalized() target_localz = target_localx.cross(aligny).normalized() angle = target_localz.angle(bm1.verts[2].co) ### NEED SOME TEST FOR ANGLE FLIPPING eul.rotate_axis('Y', angle) mat = eul.to_matrix().to_4x4() ### Test if rotation is good bmf = get_axis_aligned_bm(mat) dist = distance_point_to_plane(bmf.verts[2].co, target_localz, target_localz.cross(alignz)) error = 1e-06 ### Flip the angle if abs(dist)>error: eul = rot_to_y.to_euler() eul.rotate_axis('Y', -angle) mat = eul.to_matrix().to_4x4() bm1.free() bmf.free() return mat.to_4x4()
def execute(self, context): bpy.ops.object.editmode_toggle() bm = bmesh.new() bm.from_mesh(context.active_object.data) bFaces = bm.faces bEdges = bm.edges bVerts = bm.verts fVerts = [] # Find the selected face. This will provide the plane to project onto: for f in bFaces: if f.select: for v in f.verts: fVerts.append(v) f.normal_update() normal = f.normal f.select = False break for v in bVerts: if v.select: if v in fVerts: v.select = False continue d = distance_point_to_plane(v.co, fVerts[0].co, normal) if self.make_copy: temp = v v = bVerts.new() v.co = temp.co vector = normal vector.length = abs(d) v.co = v.co - (vector * sign(d)) v.select = False bm.to_mesh(context.active_object.data) bpy.ops.object.editmode_toggle() return {'FINISHED'}
def center_of_gravity(obj): """ Return a Vector with the coordinates of the center of gravity of the object, relatively to the object location. It assumes a uniform distribution of mass. It can only work with triangular and quad meshes. """ x = y = z = 0 orig = Vector((0, 0, 0)) for face in obj.data.polygons: if face.normal.length == 0.0: continue distance = distance_point_to_plane(orig, face.center, face.normal) nb_edges = len(face.vertices) v = [obj.data.vertices[f].co for f in face.vertices] if nb_edges == 3: face_vol = - face.area * distance / 3 x += face_vol * (v[0][0] + v[1][0] + v[2][0]) / 4 y += face_vol * (v[0][1] + v[1][1] + v[2][1]) / 4 z += face_vol * (v[0][2] + v[1][2] + v[2][2]) / 4 elif nb_edges == 4: # let's cut the quad in triangles! # triangle 1: vertices 0, 1, 2 area1 = area_tri(v[0], v[1], v[2]) face_vol1 = - area1 * distance / 3 x += face_vol1 * (v[0][0] + v[1][0] + v[2][0]) / 4 y += face_vol1 * (v[0][1] + v[1][1] + v[2][1]) / 4 z += face_vol1 * (v[0][2] + v[1][2] + v[2][2]) / 4 # triangle 2: vertices 2, 3, 0 area2 = area_tri(v[2], v[3], v[0]) face_vol2 = - area2 * distance / 3 x += face_vol2 * (v[0][0] + v[2][0] + v[3][0]) / 4 y += face_vol2 * (v[0][1] + v[2][1] + v[3][1]) / 4 z += face_vol2 * (v[0][2] + v[2][2] + v[3][2]) / 4 else: raise OrientationException return Vector((x, y, z))
def main(self, context): obj = context.active_object me = obj.data bm = bmesh.from_edit_mesh(me) face_sets = [] faces = [] force_orthogonal = self.properties.force_orthogonal method = self.properties.method # store data for f in bm.faces: if f.select: if method == 'AVERAGE': faces.append(f) elif method == 'INDIVIDUAL': face_sets.append([f]) if method == 'AVERAGE': face_sets.append(faces) for fs in face_sets: normal = Vector() center = Vector() for f in fs: normal += f.normal center += f.calc_center_median_weighted() normal.normalize() center /= len(fs) if force_orthogonal: x = abs(normal.x) y = abs(normal.y) z = abs(normal.z) if x > y and x > z: normal.x /= x normal.y = 0.0 normal.z = 0.0 if y > x and y > z: normal.x = 0.0 normal.y /= y normal.z = 0.0 if z > y and z > x: normal.x = 0.0 normal.y = 0.0 normal.z /= z for f in fs: for v in f.verts: d = geometry.distance_point_to_plane(v.co, center, normal) v.co -= normal * d bmesh.update_edit_mesh(me)
def cross_section_seed(bme, mx, point, normal, seed_index, debug = True): ''' Takes a mesh and associated world matrix of the object and returns a cross secion in local space. Args: bme: Blender BMesh mx: World matrix (type Mathutils.Matrix) point: any point on the cut plane in world coords (type Mathutils.Vector) normal: plane normal direction (type Mathutisl.Vector) seed: face index, typically achieved by raycast exclude_edges: list of edge indices (usually already tested from previous iterations) ''' times = [] times.append(time.time()) #bme = bmesh.new() #bme.from_mesh(me) #bme.normal_update() #if debug: #n = len(times) #times.append(time.time()) #print('succesfully created bmesh in %f sec' % (times[n]-times[n-1])) verts =[] eds = [] #convert point and normal into local coords #in the mesh into world space.This saves 2*(Nverts -1) matrix multiplications imx = mx.inverted() pt = imx * point no = imx.to_3x3() * normal #local normal edge_mapping = {} #perhaps we should use bmesh becaus it stores the great cycles..answer yup #first initial search around seeded face. #if none, we may go back to brute force #but prolly not :-) seed_edge = None seed_search = 0 prev_eds = [] seeds =[] if seed_index > len(bme.faces) - 1: print('looks like we hit an Ngon, tentative support') #perhaps this should be done before we pass bme to this op? #we may perhaps need to re raycast the new faces? ngons = [] for f in bme.faces: if len(f.verts) > 4: ngons.append(f) #we should never get to this point because we are pre #triangulating the ngons before this function in the #final work flow but this leaves not chance and keeps #options to reuse this for later. if len(ngons): new_geom = bmesh.ops.triangulate(bme, faces = ngons, use_beauty = True) new_faces = new_geom['faces'] #now we must find a new seed index since we have added new geometry for f in new_faces: if point_in_tri(pt, f.verts[0].co, f.verts[1].co, f.verts[2].co): print('found the point inthe tri') if distance_point_to_plane(pt, f.verts[0].co, f.normal) < .001: seed_index = f.index print('found a new index to start with') break #if len(bme.faces[seed_index].edges) > 4: #print('no NGon Support for initial seed yet! try again') #return None for ed in bme.faces[seed_index].edges: seed_search += 1 prev_eds.append(ed.index) A = ed.verts[0].co B = ed.verts[1].co result = cross_edge(A, B, pt, no) if result[0] == 'CROSS': #add the point, add the mapping move forward edge_mapping[len(verts)] = [f.index for f in ed.link_faces] verts.append(result[1]) potential_faces = [face for face in ed.link_faces if face.index != seed_index] if len(potential_faces): seeds.append(potential_faces[0]) seed_edge = True else: seed_edge = False if not seed_edge: print('failed to find a good face to start with, cancelling until your programmer gets smarter') return None #we have found one edge that crosses, now, baring any terrible disconnections in the mesh, #we traverse through the link faces, wandering our way through....removing edges from our list total_tests = 0 #by the way we append the verts in the first face...we find A then B then start at A... so there is a little reverse in teh vert order at the middle. verts.reverse() for element in seeds: #this will go both ways if they dont meet up. element_tests = 0 while element and total_tests < 10000: total_tests += 1 element_tests += 1 #first, we know that this face is not coplanar..that's good #if new_face.no.cross(no) == 0: #print('coplanar face, stopping calcs until your programmer gets smarter') #return None if type(element) == bmesh.types.BMFace: element = face_cycle(element, pt, no, prev_eds, verts, edge_mapping) elif type(element) == bmesh.types.BMVert: element = vert_cycle(element, pt, no, prev_eds, verts, edge_mapping) print('completed %i tests in this seed search' % element_tests) print('%i vertices found so far' % len(verts)) #The following tests for a closed loop #if the loop found itself on the first go round, the last test #will only get one try, and find no new crosses #trivially, mast make sure that the first seed we found wasn't #on a non manifold edge, which should never happen closed_loop = element_tests == 1 and len(seeds) == 2 print('walked around cross section in %i tests' % total_tests) print('found this many vertices: %i' % len(verts)) if debug: n = len(times) times.append(time.time()) print('calced intersections %f sec' % (times[n]-times[n-1])) #iterate through smartly to create edge keys #no longer have to do this...verts are created in order if closed_loop: for i in range(0,len(verts)-1): eds.append((i,i+1)) #the edge loop closure eds.append((i+1,0)) else: #two more verts found than total tests #one vert per element test in the last loop #split the loop into the verts into the first seed and 2nd seed seed_1_verts = verts[:len(verts)-(element_tests)] #yikes maybe this index math is right seed_2_verts = verts[len(verts)-(element_tests):] seed_2_verts.reverse() seed_2_verts.extend(seed_1_verts) for i in range(0,len(seed_1_verts)-1): eds.append((i,i+1)) verts = seed_2_verts if debug: n = len(times) times.append(time.time()) print('calced connectivity %f sec' % (times[n]-times[n-1])) if len(verts): #new_me = bpy.data.meshes.new('Cross Section') #new_me.from_pydata(verts,eds,[]) #if debug: #n = len(times) #times.append(time.time()) #print('Total Time: %f sec' % (times[-1]-times[0])) return (verts, eds) else: return None
def key_func(v): return abs(geom.distance_point_to_plane(v.co, v1.co, normal))
def point_inside_loop_almost3D(pt, verts, no, p_pt = None, threshold = .01, debug = False): ''' http://blenderartists.org/forum/showthread.php?259085-Brainstorming-for-Virtual-Buttons&highlight=point+inside+loop args: pt - 3d point to test of type Mathutils.Vector verts - 3d points representing the loop TODO: verts[0] == verts[-1] or implied? list with elements of type Mathutils.Vector no - plane normal plane_pt - a point on the plane. if None, COM of verts will be used threshold - maximum distance to consider pt "coplanar" default = .01 debug - Bool, default False. Will print performance if True return: Bool True if point is inside the loop ''' if debug: start = time.time() #sanity checks if len(verts) < 3: print('loop must have 3 verts to be a loop and even then its sketchy') return False if no.length == 0: print('normal vector must be non zero') return False if not p_pt: p_pt = get_com(verts) if distance_point_to_plane(pt, p_pt, no) > threshold: return False #get the equation of a plane ax + by + cz = D #Given point P, normal N ...any point R in plane satisfies # Nx * (Rx - Px) + Ny * (Ry - Py) + Nz * (Rz - Pz) = 0 #now pick any xy, yz or xz and solve for the other point a = no[0] b = no[1] c = no[2] Px = p_pt[0] Py = p_pt[1] Pz = p_pt[2] D = a * Px + b * Py + c * Pz #generate a randomply perturbed R from the known p_pt R = p_pt + Vector((random.random(), random.random(), random.random())) #z = D/c - a/c * x - b/c * y if c != 0: Rz = D/c - a/c * R[0] - b/c * R[1] R[2] = Rz #y = D/b - a/b * x - c/b * z elif b!= 0: Ry = D/b - a/b * R[0] - c/b * R[2] R[1] = Ry #x = D/a - b/a * y - c/a * z elif a != 0: Rx = D/a - b/a * R[1] - c/a * R[2] R[0] = Rz else: print('undefined plane you wanker!') return(False) #no R represents any other point in the plane #we will use this to edefin an arbitrary local #x' y' and z' X_prime = R - p_pt X_prime.normalize() Y_prime = no.cross(X_prime) Y_prime.normalize() verts_prime = [] for v in verts: v_trans = v - p_pt vx = v_trans.dot(X_prime) vy = v_trans.dot(Y_prime) verts_prime.append(Vector((vx, vy))) #transform the test point into the new plane x,y space pt_trans = pt - p_pt pt_prime = Vector((pt_trans.dot(X_prime), pt_trans.dot(Y_prime))) pt_in_loop = point_inside_loop2d(verts_prime, pt_prime) return pt_in_loop
def cross_section_walker_endpoints(bme, pt, no, f_ind_from, e_ind_from, co_from, f_ind_to, co_to, epsilon, limit_set = None, max_iters = 10000): ''' bme - bmesh pt - a point on cutting plane: mathutils.Vector no - the normal of the cutting plane: mathutils.Vector f_ind_from - index of face which we are walking from: Int e_ind_from - index of the edge which we are stepping over: Int co_from - location of intersectino of e_ind_from edge and cutting plane: mathutils.Vector f_end_to - index of face which we are walking toward co_to - location of end point, not necessarily and edge intersection, often a racy_cast result in middle of a face returns tuple (verts,ed_inds, looped, found) by walking around a bmesh near the given plane verts: List of intersections of edges and cutting plane (in order) mathutils.Vector co_2 is excluded eds crossed: lost of the edges which were intersected(in orter) Int. looped is bool indicating if walk wrapped around bmesh, only true if did not find other face found is a bool indicating if the walk was able to find face f_ind_to at point co_to ''' bme.verts.ensure_lookup_table() bme.edges.ensure_lookup_table() bme.faces.ensure_lookup_table() # returned values verts = [co_from] eds_crossed = [bme.edges[e_ind_from]] faces_crossed = [bme.faces[f_ind_from]] looped = False found = False error = None #verify that the points are coplanar to the cut plane d0 = distance_point_to_plane(co_from, pt, no) df = distance_point_to_plane(co_to, pt, no) if d0 > epsilon or df > epsilon: print('not coplanar by epsilons standdards') print((d0, df, epsilon)) return ([co_from, co_to],[], [],False, False, 'EPSILON') # track what faces we've seen f_inds_dict = {f_ind_from: 0} #track what edges we've seen (more important with Ngons, we may traverse a face multiple times) e_inds_dict = {e_ind_from: 0} # get blender version bver = '%03d.%03d.%03d' % (bpy.app.version[0],bpy.app.version[1],bpy.app.version[2]) if bver > '002.072.000': bme.edges.ensure_lookup_table(); f_cur = next(f for f in bme.edges[e_ind_from].link_faces if f.index != f_ind_from) #There is occasionally error here find_current = f_cur.index faces_crossed += [f_cur] #find the edges we might cross at the end, make sure where we are headed is valid #co_end will be bweteen edges 0,1 2,3 3,4 even if f_end is a concve ngon sith 4,6,8... intersections valid_end = False f_end = bme.faces[f_ind_to] eds_is = find_bmedges_crossing_plane(pt, no, f_end.edges, epsilon) for i in range(0,int(len(eds_is)/2)): p0 = eds_is[2*i][1] p1 = eds_is[2*i + 1][1] line_loc, line_pct = intersect_point_line(co_to, p0, p1) if line_pct >= 0 and line_pct <= 1: #we have found the 2 edges which co_to lies between! end_edges_dict = {eds_is[2*i][0].index: 0} end_edges_dict[eds_is[2*i+1][0].index] = 1 valid_end = True if not valid_end: print('co_to is not close to f_ind_to or not between 2 edge intersections of f_to and cut plane') return ([co_from, co_to],[], [], False, False, 'END_POINT') while True: # find edges in the face that cross the plane cross_eds = find_sorted_bmedges_crossing_plane(pt, no, f_cur.edges, epsilon, e_ind_from, co_from) edge, i = cross_eds[0] verts += [i] eds_crossed += [edge] if len(edge.link_faces) == 1: error = 'NON_MANIFOLD' break #end of non manifold mesh if edge.index in end_edges_dict: #we found an edge in the ending face. #print('found an edge in the ending face') #verts += [co_to] #tack on the final point? found = True error = None break # get next face, edge, co f_next = next(f for f in edge.link_faces if f.index != find_current) faces_crossed += [f_next] find_next = f_next.index eind_next = edge.index co_next = i if find_next in f_inds_dict: #we might have looped, ngons can be crossed multiple times print('we have seen the next face before') if len(bme.faces[find_next].edges) <= 4: #can only cross a quad or tri onces print('quad or tri, we have looped to where we started') looped = True if f_inds_dict[find_next] != 0: # loop is P-shaped (loop with a tail) print('p shaped loop len %i, clipping tail %i' % (len(verts), f_inds_dict[find_next])) verts = verts[f_inds_dict[find_next]:] # clip off tail error = 'P_LOOP' break elif len(bme.faces[find_next].edges) > 4 and f_inds_dict[find_next] == 0: print('more than 4 edges, and the first face we started with') next_crosses = find_sorted_bmedges_crossing_plane(pt, no, f_next.edges, epsilon, eind_next, co_next) if all(e.index in e_inds_dict for e, i in next_crosses[1:]): #all the other edges of the face have been seen, we have looped print('looped, all the other edges in the ngon has been tested, and it was the first ngon we tested') looped = True error = 'NGON_SPECIAL' break elif eind_next in e_inds_dict: print('looped when found an already crossed edge') looped = True verts.pop() error = 'NGON_SPECIAL' break elif limit_set and f_next not in limit_set: error = 'LIMIT_SET' break else: # leave breadcrumb if find_next not in the dict, we may cross the face mutliple #times so we don't want to add it repeatedly f_inds_dict[find_next] = len(f_inds_dict) #always record the tested edges, allows us to find out if we have looped on an arm #of an extruded E shaped NGon e_inds_dict[eind_next] = len(e_inds_dict) f_ind_from = find_current e_ind_from = eind_next co_from = co_next f_cur = f_next find_current = find_next return (verts,eds_crossed, faces_crossed, looped, found, error)
def construct_face(self, context, grid_coord, tile_xy, origin_xy, grid_up, grid_right, up_vector, right_vector, plane_normal, face_index=None, check_exists=True, shift_vec=None, threshold=None, add_cursor=True): """ Create a new face at grid_coord or remap the existing face :param context: :param grid_coord: :param tile_xy: :param origin_xy: :param grid_up: :param grid_right: :param up_vector: :param right_vector: :param plane_normal: :param face_index: :param check_exists: :param shift_vec: :param threshold: :param add_cursor: :return: """ scene = context.scene data = scene.sprytile_data hit_loc = None hit_normal = None if check_exists: hit_loc, hit_normal, face_index, hit_dist = self.raycast_grid_coord( context, grid_coord[0], grid_coord[1], grid_up, grid_right, plane_normal) did_build = False # No face index, assume build face if face_index is None or face_index < 0: face_position = scene.cursor_location + grid_coord[ 0] * grid_right + grid_coord[1] * grid_up face_verts = self.get_build_vertices(face_position, grid_right, grid_up, up_vector, right_vector) face_index = self.create_face(context, face_verts) did_build = True # Face index was given and check exists flag is off, check for actual face elif check_exists is False: hit_loc, hit_normal, face_index, hit_dist = self.raycast_grid_coord( context, grid_coord[0], grid_coord[1], grid_up, grid_right, plane_normal) if face_index is None or face_index < 0: return None # Didn't create face, only want to remap face. Check for coplanarity and dot if did_build is False: check_dot = abs(plane_normal.dot(hit_normal)) check_dot -= 1 check_coplanar = distance_point_to_plane(hit_loc, scene.cursor_location, plane_normal) check_coplanar = abs(check_coplanar) < 0.05 check_dot = abs(check_dot) < 0.05 # Can't remap face if not check_coplanar or not check_dot: return None sprytile_uv.uv_map_face(context, up_vector, right_vector, tile_xy, origin_xy, face_index, self.bmesh) if did_build and data.auto_merge: if threshold is None: threshold = (1 / data.world_pixels) * 1.25 face = self.bmesh.faces[face_index] face_position += grid_right * 0.5 + grid_up * 0.5 face_position += plane_normal * 0.01 face_index = self.merge_doubles(context, face, face_position, -plane_normal, threshold) # Auto merge refreshes the mesh automatically self.refresh_mesh = not data.auto_merge if add_cursor and hit_loc is not None: self.add_virtual_cursor(hit_loc) return face_index
def create_capsule(mesh, diameter=0.5, length=2, use_rot=False): length = length if diameter < length else diameter if diameter < 0: raise ValueError("Cannot create capsule with a diameter less than 0!") bm = bmesh.new() kwargs = {} if USE_LEGACY: kwargs["diameter"] = diameter else: kwargs["radius"] = diameter bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, **kwargs) bm.to_mesh(mesh) center = Vector() axis = Vector((0, 0, 1)) # Get top and bottom halves of vertices top = [] top_faces = [] bottom = [] bottom_faces = [] amount = (length - diameter) * 2 vec = Vector((0, 0, amount)) for v in bm.verts: if distance_point_to_plane(v.co, center, axis) >= 0: top.append(v.co) for face in v.link_faces: if not face in top_faces: top_faces.append(face) elif distance_point_to_plane(v.co, center, axis) <= 0: bottom.append(v.co) for face in v.link_faces: if not face in bottom_faces and face not in top_faces: bottom_faces.append(face) # Extrude top half ret = bmesh.ops.extrude_face_region(bm, geom=top_faces) extruded = ret["geom"] del ret translate_verts = [ v for v in extruded if isinstance(v, bmesh.types.BMVert) ] bmesh.ops.translate(bm, vec=vec / 2, verts=translate_verts) # Extrude bottom half ret = bmesh.ops.extrude_face_region(bm, geom=bottom_faces) extruded = ret["geom"] del ret translate_verts = [ v for v in extruded if isinstance(v, bmesh.types.BMVert) ] bmesh.ops.translate(bm, vec=-vec / 2, verts=translate_verts) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) bm.to_mesh(mesh) bm.free() if use_rot: mesh.transform(Matrix.Rotation(radians(90.0), 4, "X")) return mesh
def cross_section_walker_endpoints(bme, pt, no, f_ind_from, e_ind_from, co_from, f_ind_to, co_to, epsilon, limit_set = None, max_iters = 10000): ''' bme - bmesh pt - a point on cutting plane: mathutils.Vector no - the normal of the cutting plane: mathutils.Vector f_ind_from - index of face which we are walking from: Int e_ind_from - index of the edge which we are stepping over: Int co_from - location of intersectino of e_ind_from edge and cutting plane: mathutils.Vector f_end_to - index of face which we are walking toward co_to - location of end point, not necessarily and edge intersection, often a racy_cast result in middle of a face returns tuple (verts,ed_inds, looped, found) by walking around a bmesh near the given plane verts: List of intersections of edges and cutting plane (in order) mathutils.Vector co_2 is excluded eds crossed: lost of the edges which were intersected(in orter) Int. looped is bool indicating if walk wrapped around bmesh, only true if did not find other face found is a bool indicating if the walk was able to find face f_ind_to at point co_to ''' bme.verts.ensure_lookup_table() bme.edges.ensure_lookup_table() bme.faces.ensure_lookup_table() # returned values verts = [co_from] eds_crossed = [bme.edges[e_ind_from]] faces_crossed = [bme.faces[f_ind_from]] looped = False found = False error = None #verify that the points are coplanar to the cut plane d0 = distance_point_to_plane(co_from, pt, no) df = distance_point_to_plane(co_to, pt, no) if d0 > epsilon or df > epsilon: print('not coplanar by epsilons standdards') print((d0, df, epsilon)) return ([co_from, co_to],[], [],False, False, 'EPSILON') # track what faces we've seen f_inds_dict = {f_ind_from: 0} #track what edges we've seen (more important with Ngons, we may traverse a face multiple times) e_inds_dict = {e_ind_from: 0} # get blender version bver = '%03d.%03d.%03d' % (bpy.app.version[0],bpy.app.version[1],bpy.app.version[2]) if bver > '002.072.000': bme.edges.ensure_lookup_table(); f_cur = next(f for f in bme.edges[e_ind_from].link_faces if f.index != f_ind_from) find_current = f_cur.index #find the edges we might cross at the end, make sure where we are headed is valid #co_end will be bweteen edges 0,1 2,3 3,4 even if f_end is a concve ngon sith 4,6,8... intersections valid_end = False f_end = bme.faces[f_ind_to] eds_is = find_bmedges_crossing_plane(pt, no, f_end.edges, epsilon) for i in range(0,int(len(eds_is)/2)): p0 = eds_is[2*i][1] p1 = eds_is[2*i + 1][1] line_loc, line_pct = intersect_point_line(co_to, p0, p1) if line_pct >= 0 and line_pct <= 1: #we have found the 2 edges which co_to lies between! end_edges_dict = {eds_is[2*i][0].index: 0} end_edges_dict[eds_is[2*i+1][0].index] = 1 valid_end = True if not valid_end: print('co_to is not close to f_ind_to or not between 2 edge intersections of f_to and cut plane') return ([co_from, co_to],[], [], False, False, 'END_POINT') while True: # find edges in the face that cross the plane cross_eds = find_sorted_bmedges_crossing_plane(pt, no, f_cur.edges, epsilon, e_ind_from, co_from) edge, i = cross_eds[0] verts += [i] eds_crossed += [edge] if len(edge.link_faces) == 1: error = 'NON_MANIFOLD' break #end of non manifold mesh if edge.index in end_edges_dict: #we found an edge in the ending face. #print('found an edge in the ending face') #verts += [co_to] #tack on the final point? found = True error = None break # get next face, edge, co f_next = next(f for f in edge.link_faces if f.index != find_current) faces_crossed += [f_next] find_next = f_next.index eind_next = edge.index co_next = i if find_next in f_inds_dict: #we might have looped, ngons can be crossed multiple times print('we have seen the next face before') if len(bme.faces[find_next].edges) <= 4: #can only cross a quad or tri onces print('quad or tri, we have looped to where we started') looped = True if f_inds_dict[find_next] != 0: # loop is P-shaped (loop with a tail) print('p shaped loop len %i, clipping tail %i' % (len(verts), f_inds_dict[find_next])) verts = verts[f_inds_dict[find_next]:] # clip off tail error = 'P_LOOP' break elif len(bme.faces[find_next].edges) > 4 and f_inds_dict[find_next] == 0: print('more than 4 edges, and the first face we started with') next_crosses = find_sorted_bmedges_crossing_plane(pt, no, f_next.edges, epsilon, eind_next, co_next) if all(e.index in e_inds_dict for e, i in next_crosses[1:]): #all the other edges of the face have been seen, we have looped print('looped, all the other edges in the ngon has been tested, and it was the first ngon we tested') looped = True error = 'NGON_SPECIAL' break elif eind_next in e_inds_dict: print('looped when found an already crossed edge') looped = True verts.pop() error = 'NGON_SPECIAL' break elif limit_set and f_next not in limit_set: error = 'LIMIT_SET' break else: # leave breadcrumb if find_next not in the dict, we may cross the face mutliple #times so we don't want to add it repeatedly f_inds_dict[find_next] = len(f_inds_dict) #always record the tested edges, allows us to find out if we have looped on an arm #of an extruded E shaped NGon e_inds_dict[eind_next] = len(e_inds_dict) f_ind_from = find_current e_ind_from = eind_next co_from = co_next f_cur = f_next find_current = find_next return (verts,eds_crossed, faces_crossed, looped, found, error)
def convex_hull_3d(vecs, eps:'距離がこれ以下なら同一平面と見做す'=1e-6): """三次元又は二次元の凸包を求める""" if len(vecs) <= 1: return list(range(len(vecs))) verts = [_Vert(i, v) for i, v in enumerate(vecs)] # なるべく離れている二頂点を求める medium = reduce(lambda a, b: a + b, vecs) / len(vecs) v1 = max(verts, key=lambda v: (v.co - medium).length) v2 = max(verts, key=lambda v: (v.co - v1.co).length) line = v2.co - v1.co if line.length <= eps: # 全ての頂点が重なる return [0] verts.remove(v1) verts.remove(v2) if not verts: return [v1.index, v2.index] # 三角形を構成する為の頂点を求める v3 = max(verts, key=lambda v: line.cross(v.co - v1.co).length) if line.normalized().cross(v3.co - v1.co).length <= eps: # 全ての頂点が同一線上にある return [v1.index, v2.index] verts.remove(v3) if not verts: return [v1.index, v2.index, v3.index] # 四面体を構成する為の頂点を求める normal = geom.normal(v1.co, v2.co, v3.co) def key_func(v): return abs(geom.distance_point_to_plane(v.co, v1.co, normal)) v4 = max(verts, key=key_func) if key_func(v4) <= eps: # 全ての頂点が平面上にある quat = normal.rotation_difference(Vector((0, 0, 1))) vecs_2d = [(quat * v).to_2d() for v in vecs] return convex_hull_2d(vecs_2d, eps) verts.remove(v4) # 四面体作成 # ^ normal # v3 | # / |\ # v1 /____\v2 # \ / # \ / # v4 if geom.distance_point_to_plane(v4.co, v1.co, normal) < 0.0: faces = [_Face(v1, v2, v3), _Face(v1, v4, v2), _Face(v2, v4, v3), _Face(v3, v4, v1)] else: faces = [_Face(v1, v3, v2), _Face(v1, v2, v4), _Face(v2, v3, v4), _Face(v3, v1, v4)] # 残りの頂点を各面に分配 _divide_outer_verts(faces, verts, eps) # edge_faces作成 edge_faces = defaultdict(list) for face in faces: for ekey in face.edge_keys: edge_faces[ekey].append(face) while True: added = False for i in range(len(faces)): try: face = faces[i] except: break if not face.outer_verts: continue v1 = max(face.outer_verts, key=lambda v: face.distance(v.co)) if face.distance(v1.co) > eps: # 凸包になるようにv1から放射状に面を貼る added = True # 隠れて不要となる面を求める remove_faces = set() _find_remove_faces_re(remove_faces, v1.co, face, edge_faces, eps) # remove_facesを多面体から除去して穴を開ける for f in remove_faces: for ekey in f.edge_keys: edge_faces[ekey].remove(f) faces.remove(f) # 穴に面を貼る new_faces = [] ekey_count = defaultdict(int) for f in remove_faces: for ekey in f.edge_keys: ekey_count[ekey] += 1 for ekey, cnt in ekey_count.items(): if cnt != 1: continue linkface = edge_faces[ekey][0] v2, v3 = ekey if linkface.verts[linkface.verts.index(v2) - 1] != v3: v2, v3 = v3, v2 new_face = _Face(v1, v2, v3) for key in new_face.edge_keys: edge_faces[key].append(new_face) new_faces.append(new_face) faces.extend(new_faces) # 頂点の再分配 outer_verts = reduce(lambda a, b: a + b, (f.outer_verts for f in remove_faces)) if v1 in outer_verts: outer_verts.remove(v1) _divide_outer_verts(new_faces, outer_verts, eps) else: face.outer_verts = [] if not added: break return [[v.index for v in f.verts] for f in faces]
def distance(self, v): return geom.distance_point_to_plane(v, self.verts[0].co, self.normal)
def OBB(vecs, r_indices=None, eps=1e-6): """Convex hull を用いたOBBを返す。 Z->Y->Xの順で長さが最少となる軸を求める。 :param vecs: list of Vector :type vecs: list | tuple :param r_indices: listを渡すとconvexhullの結果を格納する :type r_indices: None | list :param eps: 種々の計算の閾値 :return: (matrix, obb_size) matrix: type: Matrx OBBの回転と中心を表す。vecsが二次元ベクトルの場合は3x3, 三次元なら4x4。 obb_size: type: Vector OBBの各軸の長さ。vecsと同じ次元。 :rtype: (Matrix, Vector) """ if not vecs: return None, None # 2D ---------------------------------------------------------------------- if len(vecs[0]) == 2: mat = Matrix.Identity(3) bb_size = Vector((0, 0)) indices = convex_hull_2d(vecs, eps) if r_indices: r_indices[:] = indices if len(indices) == 1: mat.col[2][:2] = vecs[0] elif len(indices) == 2: v1 = vecs[indices[0]] v2 = vecs[indices[1]] xaxis = (v2 - v1).normalized() angle = math.atan2(xaxis[1], xaxis[0]) mat2 = Matrix.Rotation(angle, 2) mat.col[0][:2] = mat2.col[0] mat.col[1][:2] = mat2.col[1] mat.col[2][:2] = (v1 + v2) / 2 bb_size[0] = (v2 - v1).length else: yaxis = _closest_axis_on_plane(vecs, indices) angle = math.atan2(yaxis[1], yaxis[0]) - math.pi / 2 # X軸 mat2 = Matrix.Rotation(angle, 2) imat2 = Matrix.Rotation(-angle, 2) rotvecs = [imat2 * v for v in vecs] loc = Vector((0, 0)) for i in range(2): rotvecs.sort(key=lambda v: v[i]) bb_size[i] = rotvecs[-1][i] - rotvecs[0][i] loc[i] = (rotvecs[0][i] + rotvecs[-1][i]) / 2 mat.col[0][:2] = mat2.col[0] mat.col[1][:2] = mat2.col[1] mat.col[2][:2] = mat2 * loc return mat, bb_size # 3D ---------------------------------------------------------------------- mat = Matrix.Identity(4) bb_size = Vector((0, 0, 0)) indices = convex_hull(vecs, eps) if r_indices: r_indices[:] = indices if isinstance(indices[0], int): # 2d if len(indices) == 1: mat.col[3][:3] = vecs[0] return mat, bb_size elif len(indices) == 2: # 同一線上 v1 = vecs[indices[0]] v2 = vecs[indices[1]] xaxis = (v2 - v1).normalized() quat = Vector((1, 0, 0)).rotation_difference(xaxis) mat = quat.to_matrix().to_4x4() mat.col[3][:3] = (v1 + v2) / 2 bb_size[0] = (v2 - v1).length return mat, bb_size else: # 同一平面上 medium = reduce(lambda a, b: a + b, vecs) / len(vecs) v1 = max(vecs, key=lambda v: (v - medium).length) v2 = max(vecs, key=lambda v: (v - v1).length) line = v2 - v1 v3 = max(vecs, key=lambda v: line.cross(v - v1).length) zaxis = geom.normal(v1, v2, v3) if zaxis[2] < 0.0: zaxis.negate() quat = zaxis.rotation_difference(Vector((0, 0, 1))) rotvecs = [quat * v for v in vecs] indices_2d = indices else: # 3d indices_set = set(chain(*indices)) zaxis = None dist = 0.0 # 最も距離の近い面(平面)と頂点を求める for tri in indices: v1, v2, v3 = [vecs[i] for i in tri] normal = geom.normal(v1, v2, v3) d = 0.0 for v4 in (vecs[i] for i in indices_set if i not in tri): f = abs(geom.distance_point_to_plane(v4, v1, normal)) d = max(f, d) if zaxis is None or d < dist: zaxis = -normal dist = d quat = zaxis.rotation_difference(Vector((0, 0, 1))) rotvecs = [(quat * v).to_2d() for v in vecs] indices_2d = convex_hull_2d(rotvecs, eps) yaxis = _closest_axis_on_plane(rotvecs, indices_2d) yaxis = quat.inverted() * yaxis.to_3d() xaxis = yaxis.cross(zaxis) xaxis.normalize() # 不要? mat.col[0][:3] = xaxis mat.col[1][:3] = yaxis mat.col[2][:3] = zaxis # OBBの大きさと中心を求める imat = mat.inverted() rotvecs = [imat * v for v in vecs] loc = Vector() for i in range(3): rotvecs.sort(key=lambda v: v[i]) bb_size[i] = rotvecs[-1][i] - rotvecs[0][i] loc[i] = (rotvecs[0][i] + rotvecs[-1][i]) / 2 mat.col[3][:3] = mat * loc return mat, bb_size
def execute(self, context, scene, ray_origin, ray_vector, is_start): data = scene.sprytile_data grid = sprytile_utils.get_grid(context, context.object.sprytile_gridid) tile_xy = (grid.tile_selection[0], grid.tile_selection[1]) # Get vectors for grid, without rotation up_vector, right_vector, plane_normal = sprytile_utils.get_current_grid_vectors( scene, with_rotation=False ) # Rotate the vectors rotation = Quaternion(plane_normal, data.mesh_rotate) up_vector = rotation * up_vector right_vector = rotation * right_vector # Used to move raycast slightly along ray vector shift_vec = ray_vector.normalized() * 0.001 # raycast grid to get the grid position under the mouse grid_coord, grid_right, grid_up, plane_pos = sprytile_utils.raycast_grid( scene, context, up_vector, right_vector, plane_normal, ray_origin, ray_vector, as_coord=True ) # Record starting position of stroke if is_start: self.start_coord = grid_coord # Not starting, filter out when can build elif self.start_coord is not None: tolerance_min = (floor(grid.tile_selection[2] * 0.25), floor(grid.tile_selection[3] * 0.25)) coord_offset = ( (grid_coord[0] - self.start_coord[0]) % grid.tile_selection[2], (grid_coord[1] - self.start_coord[1]) % grid.tile_selection[3] ) if coord_offset[0] > 0 or coord_offset[1] > 0: return # Get the area to build offset_tile_id, offset_grid, coord_min, coord_max = sprytile_utils.get_grid_area( grid.tile_selection[2], grid.tile_selection[3], data.uv_flip_x, data.uv_flip_y ) # Check if joining multi tile faces grid_no_spacing = sprytile_utils.grid_no_spacing(grid) is_single_pixel = sprytile_utils.grid_is_single_pixel(grid) do_join = is_single_pixel if do_join is False: do_join = grid_no_spacing and data.auto_join # Raycast under mouse hit_loc, hit_normal, hit_face_idx, hit_dist = self.modal.raycast_object( context.object, ray_origin, ray_vector) # Hit a face if hit_face_idx is not None: # Determine if face can be painted plane_hit = intersect_line_plane(ray_origin, ray_origin + ray_vector, scene.cursor_location, plane_normal) plane_dist = (plane_hit - ray_origin).magnitude difference = abs(hit_dist - plane_dist) # If valid for painting instead of creating… if difference < 0.01 or hit_dist < plane_dist: if do_join is False: return check_dot = abs(plane_normal.dot(hit_normal)) check_dot -= 1 check_coplanar = distance_point_to_plane(hit_loc, scene.cursor_location, plane_normal) check_coplanar = abs(check_coplanar) < 0.05 check_dot = abs(check_dot) < 0.05 # Paint if coplanar and dot angle checks out if check_coplanar and check_dot: face, verts, uvs, target_grid, data, target_img, tile_xy = tool_paint.ToolPaint.process_preview( self.modal, context, scene, hit_face_idx) sprytile_uv.apply_uvs(context, face, uvs, target_grid, self.modal.bmesh, data, target_img, tile_xy, origin_xy=tile_xy) # Hit a face, that could have been painted, don't do anything else return # Build mode with auto join if do_join: origin_coord = scene.cursor_location + \ (grid_coord[0] + coord_min[0]) * grid_right + \ (grid_coord[1] + coord_min[1]) * grid_up self.modal.add_virtual_cursor(origin_coord) size_x = (coord_max[0] - coord_min[0]) + 1 size_y = (coord_max[1] - coord_min[1]) + 1 size_x *= grid.grid[0] size_y *= grid.grid[1] grid_right *= size_x / grid.grid[0] grid_up *= size_y / grid.grid[1] verts = self.modal.get_build_vertices(origin_coord, grid_right, grid_up, up_vector, right_vector) face_index = self.modal.create_face(context, verts) if face_index is None: return vtx_center = Vector((0, 0, 0)) for vtx in verts: vtx_center += vtx vtx_center /= len(verts) origin_xy = (grid.tile_selection[0], grid.tile_selection[1]) target_img = sprytile_utils.get_grid_texture(context.object, grid) uvs = sprytile_uv.get_uv_pos_size(data, target_img.size, grid, origin_xy, size_x, size_y, up_vector, right_vector, verts, vtx_center) sprytile_uv.apply_uvs(context, self.modal.bmesh.faces[face_index], uvs, grid, self.modal.bmesh, data, target_img, origin_xy) if data.auto_merge: threshold = (1 / data.world_pixels) * min(2, grid.grid[0], grid.grid[1]) face = self.modal.bmesh.faces[face_index] self.modal.merge_doubles(context, face, vtx_center, -plane_normal, threshold) # Build mode without auto join else: virtual_cursor = scene.cursor_location + \ (grid_coord[0] * grid_right) + \ (grid_coord[1] * grid_up) self.modal.add_virtual_cursor(virtual_cursor) # Loop through grid coordinates to build for i in range(len(offset_grid)): grid_offset = offset_grid[i] tile_offset = offset_tile_id[i] grid_pos = [grid_coord[0] + grid_offset[0], grid_coord[1] + grid_offset[1]] tile_pos = [tile_xy[0] + tile_offset[0], tile_xy[1] + tile_offset[1]] self.modal.construct_face(context, grid_pos, tile_pos, tile_xy, grid_up, grid_right, up_vector, right_vector, plane_normal, shift_vec=shift_vec)
def execute(self, context, scene, ray_origin, ray_vector): grid = sprytile_utils.get_grid(context, context.object.sprytile_gridid) tile_xy = (grid.tile_selection[0], grid.tile_selection[1]) up_vector, right_vector, plane_normal = sprytile_utils.get_current_grid_vectors( scene) hit_loc, hit_normal, face_index, hit_dist = self.modal.raycast_object( context.object, ray_origin, ray_vector) # Used to move raycast slightly along ray vector shift_vec = ray_vector.normalized() * 0.001 # If raycast hit the mesh... if face_index is not None: # The face is valid for painting if hit face # is facing same way as plane normal and is coplanar to target plane check_dot = abs(plane_normal.dot(hit_normal)) check_dot -= 1 check_coplanar = distance_point_to_plane(hit_loc, scene.cursor_location, plane_normal) check_coplanar = abs(check_coplanar) < 0.05 check_dot = abs(check_dot) < 0.05 # Hit a face that is valid for painting if check_dot and check_coplanar: self.modal.add_virtual_cursor(hit_loc) # Change UV of this face instead face_up, face_right = self.modal.get_face_up_vector( context, face_index) if face_up is not None and face_up.dot(up_vector) < 0.95: data = context.scene.sprytile_data rotate_matrix = Matrix.Rotation(data.mesh_rotate, 4, hit_normal) up_vector = rotate_matrix * face_up right_vector = rotate_matrix * face_right sprytile_uv.uv_map_face(context, up_vector, right_vector, tile_xy, face_index, self.modal.bmesh) if scene.sprytile_data.cursor_flow: self.modal.flow_cursor(context, face_index, hit_loc) return # Raycast did not hit the mesh, raycast to the virtual grid face_position, x_vector, y_vector, plane_cursor = sprytile_utils.raycast_grid( scene, context, up_vector, right_vector, plane_normal, ray_origin, ray_vector) # Failed to hit the grid if face_position is None: return # If raycast hit mesh, compare distance of grid hit and mesh hit if hit_loc is not None: grid_hit_dist = (face_position - ray_origin).magnitude # Mesh hit closer than grid hit, don't do anything if hit_dist < grid_hit_dist: return # store plane_cursor, for deciding where to move actual cursor if auto cursor mode is on self.modal.add_virtual_cursor(plane_cursor) # Build face and UV map it face_vertices = self.modal.get_build_vertices(face_position, x_vector, y_vector, up_vector, right_vector) face_index = self.modal.create_face(context, face_vertices) face_up, face_right = self.modal.get_face_up_vector( context, face_index) if face_up is not None and face_up.dot(up_vector) < 0.95: data = context.scene.sprytile_data rotate_matrix = Matrix.Rotation(data.mesh_rotate, 4, plane_normal) up_vector = rotate_matrix * face_up right_vector = rotate_matrix * face_right sprytile_uv.uv_map_face(context, up_vector, right_vector, tile_xy, face_index, self.modal.bmesh) if scene.sprytile_data.auto_merge: face = self.modal.bmesh.faces[face_index] face.select = True # Find the face center, to raycast from later face_center = context.object.matrix_world * face.calc_center_bounds( ) # Move face center back a little for ray casting face_center -= shift_vec threshold = (1 / context.scene.sprytile_data.world_pixels) * 2 bpy.ops.mesh.remove_doubles(threshold=threshold, use_unselected=True) for el in [ self.modal.bmesh.faces, self.modal.bmesh.verts, self.modal.bmesh.edges ]: el.index_update() el.ensure_lookup_table() # Modified the mesh, refresh and raycast to find the new face index self.modal.update_bmesh_tree(context) loc, norm, new_face_idx, hit_dist = self.modal.raycast_object( context.object, face_center, ray_vector) if new_face_idx is not None: self.modal.bmesh.faces[new_face_idx].select = False face_index = new_face_idx else: face_index = -1 # Auto merge refreshes the mesh automatically self.modal.refresh_mesh = not scene.sprytile_data.auto_merge if scene.sprytile_data.cursor_flow and face_index is not None and face_index > -1: self.modal.flow_cursor(context, face_index, plane_cursor)
def build_preview(self, context, scene, ray_origin, ray_vector): obj = context.object data = scene.sprytile_data grid_id = obj.sprytile_gridid target_grid = sprytile_utils.get_grid(context, grid_id) # Reset can build flag self.can_build = False target_img = sprytile_utils.get_grid_texture(obj, target_grid) if target_img is None: self.modal.clear_preview_data() return # If building on base layer, get from current virtual grid up_vector, right_vector, plane_normal = sprytile_utils.get_current_grid_vectors( scene, False) # Building on decal layer, get from face under mouse if data.work_layer == 'DECAL_1' and data.lock_normal is False: location, hit_normal, face_index, distance = self.modal.raycast_object( context.object, ray_origin, ray_vector) # For decals, if not hitting the object don't draw preview if hit_normal is None: self.modal.clear_preview_data() return # Do a coplanar check between hit location and cursor grid_origin = scene.cursor_location.copy() grid_origin += hit_normal * data.mesh_decal_offset check_coplanar = distance_point_to_plane(location, grid_origin, hit_normal) check_coplanar = abs(check_coplanar) < 0.05 if check_coplanar is False: self.modal.clear_preview_data() return face_up, face_right = self.modal.get_face_up_vector( context, face_index, 0.4, bias_right=True) if face_up is not None and face_right is not None: plane_normal = hit_normal up_vector = face_up right_vector = face_right else: self.modal.clear_preview_data() return rotation = Quaternion(plane_normal, data.mesh_rotate) up_vector = rotation * up_vector right_vector = rotation * right_vector # Raycast to the virtual grid face_position, x_vector, y_vector, plane_cursor = sprytile_utils.raycast_grid( scene, context, up_vector, right_vector, plane_normal, ray_origin, ray_vector) if face_position is None: self.modal.clear_preview_data() return # Passed can build checks, set flag to true self.can_build = True offset_tile_id, offset_grid, coord_min, coord_max = sprytile_utils.get_grid_area( target_grid.tile_selection[2], target_grid.tile_selection[3], data.uv_flip_x, data.uv_flip_y) grid_no_spacing = sprytile_utils.grid_no_spacing(target_grid) # No spacing in grid, automatically join the preview together if grid_no_spacing: origin_coord = face_position + coord_min[0] * x_vector + coord_min[ 1] * y_vector size_x = (coord_max[0] - coord_min[0]) + 1 size_y = (coord_max[1] - coord_min[1]) + 1 size_x *= target_grid.grid[0] size_y *= target_grid.grid[1] x_vector *= size_x / target_grid.grid[0] y_vector *= size_y / target_grid.grid[1] preview_verts = self.modal.get_build_vertices( origin_coord, x_vector, y_vector, up_vector, right_vector) vtx_center = Vector((0, 0, 0)) for vtx in preview_verts: vtx_center += vtx vtx_center /= len(preview_verts) origin_xy = (target_grid.tile_selection[0], target_grid.tile_selection[1]) preview_uvs = sprytile_uv.get_uv_pos_size( data, target_img.size, target_grid, origin_xy, size_x, size_y, up_vector, right_vector, preview_verts, vtx_center) self.modal.set_preview_data(preview_verts, preview_uvs) return # Spaced grids need to be tiled preview_verts = [] preview_uvs = [] for i in range(len(offset_tile_id)): grid_offset = offset_grid[i] tile_offset = offset_tile_id[i] x_offset = x_vector * grid_offset[0] y_offset = y_vector * grid_offset[1] coord_position = face_position + x_offset + y_offset coord_verts = self.modal.get_build_vertices( coord_position, x_vector, y_vector, up_vector, right_vector) # Get the center of the preview verts vtx_center = Vector((0, 0, 0)) for vtx in coord_verts: vtx_center += vtx vtx_center /= len(coord_verts) # Calculate the tile with offset tile_xy = (target_grid.tile_selection[0] + tile_offset[0], target_grid.tile_selection[1] + tile_offset[1]) coord_uvs = sprytile_uv.get_uv_positions(data, target_img.size, target_grid, up_vector, right_vector, tile_xy, coord_verts, vtx_center) preview_verts.extend(coord_verts) preview_uvs.extend(coord_uvs) self.modal.set_preview_data(preview_verts, preview_uvs)
def convex_hull_3d(vecs, eps: '距離がこれ以下なら同一平面と見做す' = 1e-6): """三次元又は二次元の凸包を求める""" if len(vecs) <= 1: return list(range(len(vecs))) verts = [_Vert(i, v) for i, v in enumerate(vecs)] # なるべく離れている二頂点を求める medium = reduce(lambda a, b: a + b, vecs) / len(vecs) v1 = max(verts, key=lambda v: (v.co - medium).length) v2 = max(verts, key=lambda v: (v.co - v1.co).length) line = v2.co - v1.co if line.length <= eps: # 全ての頂点が重なる return [0] verts.remove(v1) verts.remove(v2) if not verts: return [v1.index, v2.index] # 三角形を構成する為の頂点を求める v3 = max(verts, key=lambda v: line.cross(v.co - v1.co).length) if line.normalized().cross(v3.co - v1.co).length <= eps: # 全ての頂点が同一線上にある return [v1.index, v2.index] verts.remove(v3) if not verts: return [v1.index, v2.index, v3.index] # 四面体を構成する為の頂点を求める normal = geom.normal(v1.co, v2.co, v3.co) def key_func(v): return abs(geom.distance_point_to_plane(v.co, v1.co, normal)) v4 = max(verts, key=key_func) if key_func(v4) <= eps: # 全ての頂点が平面上にある quat = normal.rotation_difference(Vector((0, 0, 1))) vecs_2d = [(quat * v).to_2d() for v in vecs] return convex_hull_2d(vecs_2d, eps) verts.remove(v4) # 四面体作成 # ^ normal # v3 | # / |\ # v1 /____\v2 # \ / # \ / # v4 if geom.distance_point_to_plane(v4.co, v1.co, normal) < 0.0: faces = [ _Face(v1, v2, v3), _Face(v1, v4, v2), _Face(v2, v4, v3), _Face(v3, v4, v1) ] else: faces = [ _Face(v1, v3, v2), _Face(v1, v2, v4), _Face(v2, v3, v4), _Face(v3, v1, v4) ] # 残りの頂点を各面に分配 _divide_outer_verts(faces, verts, eps) # edge_faces作成 edge_faces = defaultdict(list) for face in faces: for ekey in face.edge_keys: edge_faces[ekey].append(face) while True: added = False for i in range(len(faces)): try: face = faces[i] except: break if not face.outer_verts: continue v1 = max(face.outer_verts, key=lambda v: face.distance(v.co)) if face.distance(v1.co) > eps: # 凸包になるようにv1から放射状に面を貼る added = True # 隠れて不要となる面を求める remove_faces = set() _find_remove_faces_re(remove_faces, v1.co, face, edge_faces, eps) # remove_facesを多面体から除去して穴を開ける for f in remove_faces: for ekey in f.edge_keys: edge_faces[ekey].remove(f) faces.remove(f) # 穴に面を貼る new_faces = [] ekey_count = defaultdict(int) for f in remove_faces: for ekey in f.edge_keys: ekey_count[ekey] += 1 for ekey, cnt in ekey_count.items(): if cnt != 1: continue linkface = edge_faces[ekey][0] v2, v3 = ekey if linkface.verts[linkface.verts.index(v2) - 1] != v3: v2, v3 = v3, v2 new_face = _Face(v1, v2, v3) for key in new_face.edge_keys: edge_faces[key].append(new_face) new_faces.append(new_face) faces.extend(new_faces) # 頂点の再分配 outer_verts = reduce(lambda a, b: a + b, (f.outer_verts for f in remove_faces)) if v1 in outer_verts: outer_verts.remove(v1) _divide_outer_verts(new_faces, outer_verts, eps) else: face.outer_verts = [] if not added: break return [[v.index for v in f.verts] for f in faces]
def construct_face(self, context, grid_coord, grid_size, tile_xy, tile_origin, grid_up, grid_right, up_vector, right_vector, plane_normal, require_base_layer=False, work_layer_mask=0, threshold=None): """ Create a new face at grid_coord or remap the existing face :type work_layer_mask: bitmask integer :param context: :param grid_coord: Grid coordinate to create at :param grid_size: Tile unit size of face :param tile_xy: Tilegrid coordinate to map :param tile_origin: Origin of tilegrid coordinate, for mapping data :param grid_up: :param grid_right: :param up_vector: :param right_vector: :param plane_normal: :param require_base_layer: :param threshold: :return: """ scene = context.scene data = scene.sprytile_data # Run a raycast on target work layer mask hit_loc, hit_normal, face_index, hit_dist = self.raycast_grid_coord( context, grid_coord[0], grid_coord[1], grid_up, grid_right, plane_normal, work_layer_mask=work_layer_mask) # Didn't hit target layer, and require base layer if face_index is None and require_base_layer: # Check if there is a base layer underneath base_hit_loc, hit_normal, base_face_index, base_hit_dist = self.raycast_grid_coord( context, grid_coord[0], grid_coord[1], grid_up, grid_right, plane_normal) # Didn't hit required base layer, do nothing if base_face_index is None: return None # Calculate where the origin of the grid is grid_origin = scene.cursor_location.copy() # If doing mesh decal, offset the grid origin if data.work_layer == 'DECAL_1': grid_origin += plane_normal * data.mesh_decal_offset did_build = False # No face index, assume build face if face_index is None or face_index < 0: face_position = grid_origin + grid_coord[ 0] * grid_right + grid_coord[1] * grid_up face_verts = self.get_build_vertices(face_position, grid_right * grid_size[0], grid_up * grid_size[1], up_vector, right_vector) face_index = self.create_face(context, face_verts) did_build = True if face_index is None or face_index < 0: return None # Didn't create face, only want to remap face. Check for coplanarity and dot if did_build is False: check_dot = abs(plane_normal.dot(hit_normal)) check_dot -= 1 check_coplanar = distance_point_to_plane(hit_loc, grid_origin, plane_normal) check_coplanar = abs(check_coplanar) < 0.05 check_dot = abs(check_dot) < 0.05 # Can't remap face if not check_coplanar or not check_dot: return None sprytile_uv.uv_map_face(context, up_vector, right_vector, tile_xy, tile_origin, face_index, self.bmesh, grid_size) if did_build and data.auto_merge: if threshold is None: threshold = (1 / data.world_pixels) * 1.25 face = self.bmesh.faces[face_index] face_position += grid_right * 0.5 + grid_up * 0.5 face_position += plane_normal * 0.01 face_index = self.merge_doubles(context, face, face_position, -plane_normal, threshold) # Auto merge refreshes the mesh automatically self.refresh_mesh = not data.auto_merge return face_index
def auto_detect_curve_bounded_plane(self, mesh, tolerance=0.001): bm = bmesh.new() bm.from_mesh(mesh) bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) bm.faces.ensure_lookup_table() faces = bm.faces normal = faces[0].normal point = faces[0].verts[0].co for face in faces: pt_f = face.verts[0].co if ( normal.dot(face.normal) < 1 - tolerance or abs(geometry.distance_point_to_plane(pt_f, point, normal)) > tolerance ): raise ValueError("All faces must be coplanar and have same orientation") loop_edges = set() for face in faces: potential_edges = set(face.edges) for face2 in faces: if face == face2: continue potential_edges -= set(face2.edges) loop_edges |= potential_edges # Create loops from edges loops = [] while loop_edges: edge = loop_edges.pop() loop = [edge] has_found_connected_edge = True while has_found_connected_edge: has_found_connected_edge = False for edge in loop_edges.copy(): edge_verts = set(edge.verts) if edge_verts & set(loop[0].verts): loop.insert(0, edge) loop_edges.remove(edge) has_found_connected_edge = True elif edge_verts & set(loop[-1].verts): loop.append(edge) loop_edges.remove(edge) has_found_connected_edge = True loops.append(loop) # Determine outer loop max_area = 0 outer_loop = None inner_loops = [] for loop in loops: loop_vertices = [] total_edges = len(loop) for i, edge in enumerate(loop): if i + 1 == total_edges and edge.verts[0] in loop[i - 1].verts: loop_vertices.append(edge.verts[0]) elif i + 1 == total_edges and edge.verts[1] in loop[i - 1].verts: loop_vertices.append(edge.verts[1]) elif edge.verts[0] in loop[i + 1].verts: loop_vertices.append(edge.verts[1]) elif edge.verts[1] in loop[i + 1].verts: loop_vertices.append(edge.verts[0]) loop_bm = bmesh.new() for vert in loop_vertices: loop_bm.verts.new(vert.co) face = loop_bm.faces.new(loop_bm.verts) loop_vertex_indices = [v.index for v in loop_vertices] face_area = face.calc_area() if face_area > max_area: max_area = face_area outer_loop = loop_vertex_indices inner_loops.append(loop_vertex_indices) loop_bm.free() inner_loops.remove(outer_loop) bm.to_mesh(mesh) mesh.update() bm.free() return {"outer_curve": outer_loop, "inner_curves": inner_loops}
def main(self, context): obj = context.active_object me = obj.data bm = bmesh.from_edit_mesh(me) face_sets = [] faces = [] force_orthogonal = self.properties.force_orthogonal method = self.properties.method # store data for f in bm.faces: if f.select: if method == 'AVERAGE': faces.append (f) elif method == 'INDIVIDUAL': face_sets.append ([f]) if method == 'AVERAGE': face_sets.append (faces) for fs in face_sets: normal = Vector () center = Vector () for f in fs: normal += f.normal center += f.calc_center_median_weighted () normal.normalize () center /= len (fs) if force_orthogonal: x = abs (normal.x) y = abs (normal.y) z = abs (normal.z) if x > y and x > z: normal.x /= x normal.y = 0.0 normal.z = 0.0 if y > x and y > z: normal.x = 0.0 normal.y /= y normal.z = 0.0 if z > y and z > x: normal.x = 0.0 normal.y = 0.0 normal.z /= z for f in fs: for v in f.verts: d = geometry.distance_point_to_plane (v.co, center, normal) v.co -= normal * d bmesh.update_edit_mesh(me)
def execute(self, context): automerge = False self.used_modal = False sel_check, place, noloc, place, dupe = False, False, False, False, False normal, tangent, place_quat = None, None, None mode = bpy.context.mode[:] sel_mode = bpy.context.tool_settings.mesh_select_mode[:] obj = bpy.context.active_object if not obj: self.report({"INFO"}, "Unrotator: No Active Object?") return {"CANCELLED"} if self.ke_unrotator_option == "DUPE": dupe, noloc = True, False elif self.ke_unrotator_option == "NO_LOC": noloc, dupe = True, False connect = bpy.context.scene.kekit.unrotator_connect nolink = bpy.context.scene.kekit.unrotator_nolink nosnap = bpy.context.scene.kekit.unrotator_nosnap actual_invert = bpy.context.scene.kekit.unrotator_invert self.center = bpy.context.scene.kekit.unrotator_center if self.center_override: self.center = True # Check mouse over target ----------------------------------------------------------------------- bpy.ops.object.mode_set(mode="OBJECT") hit_obj, hit_wloc, hit_normal, hit_face = mouse_raycast(context, self.mouse_pos, evaluated=True) # print("HIT:", hit_obj, hit_face, hit_normal) if mode == "OBJECT" and not nosnap: self.rot_offset = 0 self.center = False if mode == "EDIT_MESH": bpy.ops.object.mode_set(mode="EDIT") automerge = bool(context.scene.tool_settings.use_mesh_automerge) if automerge: context.scene.tool_settings.use_mesh_automerge = False # Get selected obj bm obj_mtx = obj.matrix_world.copy() od = obj.data bm = bmesh.from_edit_mesh(od) # Check selections sel_poly = [p for p in bm.faces if p.select] sel_edges = [e for e in bm.edges if e.select] sel_verts = [v for v in bm.verts if v.select] active_h = bm.select_history.active active_point = None # Making sure an active element is in the selection if sel_mode[1] and sel_edges: if active_h not in sel_edges: bm.select_history.add(sel_edges[-1]) active_h = bm.select_history.active active_point = active_h.verts[0] elif sel_mode[0] and sel_verts: if active_h not in sel_verts: bm.select_history.add(sel_verts[-1]) active_h = bm.select_history.active active_point = active_h elif sel_mode[2] and sel_poly: if active_h not in sel_poly: bm.select_history.add(sel_poly[-1]) active_h = bm.select_history.active active_point = active_h.verts[0] # Verify valid selections if len(sel_verts) >= 3 and active_h: if sel_mode[0]: sel_check = True elif sel_mode[1] and len(sel_edges) >= 2: sel_check = True elif sel_mode[2] and len(sel_poly): sel_check = True if not sel_check: self.report({ "INFO" }, "Unrotator: Selection Error - Incorrect/No geo Selected? Active Element?" ) return {"CANCELLED"} # Expand Linked, or not ------------------------------------------------------------------ if connect: bpy.ops.mesh.select_linked() linked_verts = [v for v in bm.verts if v.select] elif not connect: linked_verts = sel_verts # fall-back # still calc the linked avg pos islands = get_islands(bm, sel_verts) for island in islands: if any(v in sel_verts for v in island): linked_verts = island break # Linked Selection center pos bm.verts.ensure_lookup_table() linked_verts_co = [v.co for v in linked_verts] avg_pos = obj_mtx @ Vector(average_vector(linked_verts_co)) sel_cos = [v.co for v in sel_verts] avg_sel_pos = obj_mtx @ Vector(average_vector(sel_cos)) center_d = Vector(avg_sel_pos - avg_pos).normalized() # bmdupe if dupe: bmesh.ops.duplicate(bm, geom=([v for v in bm.verts if v.select] + [e for e in bm.edges if e.select] + [p for p in bm.faces if p.select])) # ----------------------------------------------------------------------------------------- # Check mouse over target - place check # ----------------------------------------------------------------------------------------- if type(hit_face) == int: if hit_obj.name == obj.name: bm.faces.ensure_lookup_table() hit_face_verts = bm.faces[hit_face].verts[:] if any(i in hit_face_verts for i in linked_verts): # print("Mouse over same Obj and selected geo - Unrotate only mode") place = False else: # print("Mouse over same Obj but unselected geo - Unrotate & Place in same mesh mode") place = True n, t = self.calc_face_vectors(bm.faces[hit_face], obj_mtx, obj, len(hit_face_verts)) n.negate() place_quat = rotation_from_vector(n, t).to_quaternion() if self.center: bm.faces.ensure_lookup_table() hit_wloc = average_vector( [obj_mtx @ v.co for v in hit_face_verts]) elif hit_obj.name != obj.name: # print("Mouse over different Object - Unrotate & Place mode") place = True hit_face_verts = hit_obj.data.polygons[ hit_face].vertices[:] n, t = self.calc_face_vectors( hit_obj.data.polygons[hit_face], hit_obj.matrix_world, hit_obj, len(hit_face_verts)) n.negate() place_quat = rotation_from_vector(n, t).to_quaternion() if self.center: hit_vcos = [ hit_obj.matrix_world @ hit_obj.data.vertices[i].co for i in hit_face_verts ] hit_wloc = average_vector(hit_vcos) # ----------------------------------------------------------------------------------------- # Selection processing for normal and tangent vecs # ----------------------------------------------------------------------------------------- if sel_mode[0]: # VERT MODE --------------------------------------------------------------------------- h = tri_points_order( [sel_verts[0].co, sel_verts[1].co, sel_verts[2].co]) # Vectors from 3 points, hypoT ignored p1, p2, p3 = obj_mtx @ sel_verts[h[0]].co, obj_mtx @ sel_verts[ h[1]].co, obj_mtx @ sel_verts[h[2]].co v2 = p3 - p1 v1 = p2 - p1 normal = v1.cross(v2).normalized() tangent = Vector(v1).normalized() # Direction flip check against center mass d = normal.dot(center_d) if d <= 0: normal.negate() elif sel_mode[1]: # EDGE MODE --------------------------------------------------------------------------- bm.edges.ensure_lookup_table() # Active edge is tangent ev = active_h.verts[:] ev1, ev2 = ev[0].co, ev[1].co tangent = correct_normal(obj_mtx, Vector((ev1 - ev2)).normalized()) # B-tangent is shortest vec to v1 c = [v for v in sel_verts if v not in ev] vecs = [Vector((ev1 - v.co).normalized()) for v in c] v2 = correct_normal(obj_mtx, sorted(vecs)[0]) # Normal is crossp normal = tangent.cross(v2).normalized() # Direction flip check against center mass d = normal.dot(center_d) if d <= 0: normal.negate() elif sel_mode[2]: # POLY MODE --------------------------------------------------------------------------- normal, tangent = self.calc_face_vectors( active_h, obj_mtx, obj, len(sel_verts)) # ----------------------------------------------------------------------------------------- # Process & Execute transforms! # ----------------------------------------------------------------------------------------- if actual_invert: normal.negate() if self.inv: normal.negate() rotm = rotation_from_vector(normal, tangent) rq = rotm.to_quaternion() if self.rot_offset != 0: rq @= Quaternion((0, 0, 1), self.rot_offset) if place_quat is not None: qd = place_quat @ rq.inverted() else: # rot to place at bottom rq @= Quaternion((1, 0, 0), radians(-180.0)) qd = rq.rotation_difference(Quaternion()) rot = qd.to_euler("XYZ") rx, ry, rz = rot.x * -1, rot.y * -1, rot.z * -1 # Old macro method to avoid non-uniform scale issues...slow, but works. bpy.ops.transform.rotate(value=rx, orient_axis='X', orient_type='GLOBAL', use_proportional_edit=False, snap=False, orient_matrix_type='GLOBAL') bpy.ops.transform.rotate(value=ry, orient_axis='Y', orient_type='GLOBAL', use_proportional_edit=False, snap=False, orient_matrix_type='GLOBAL') bpy.ops.transform.rotate(value=rz, orient_axis='Z', orient_type='GLOBAL', use_proportional_edit=False, snap=False, orient_matrix_type='GLOBAL') # Calc offset nv = [v.co for v in linked_verts] npos = obj_mtx @ Vector(average_vector(nv)) if place and not noloc: # Move & offset placement d = distance_point_to_plane(obj_mtx @ active_point.co, npos, hit_normal) offset = hit_normal * d hit_vec = hit_wloc - (npos + offset) bmesh.ops.translate(bm, vec=hit_vec, space=obj_mtx, verts=linked_verts) bmesh.update_edit_mesh(obj.data) else: # Compensate z pos to rot-in-place-ish comp = (npos - avg_pos) * -1 bpy.ops.transform.translate(value=comp) bmesh.update_edit_mesh(obj.data) elif mode == 'OBJECT': # Unrotate only if not hit_obj: obj.select_set(True) obj.rotation_euler = (0, 0, self.rot_offset) elif hit_obj.name == obj.name: obj.select_set(True) obj.rotation_euler = (0, 0, self.rot_offset) # Place! elif hit_obj.name != obj.name: obj.select_set(True) self.og_pos = Vector(obj.location) if dupe: if nolink: bpy.ops.object.duplicate(linked=False) else: bpy.ops.object.duplicate(linked=True) obj = bpy.context.active_object obj.select_set(True) mtx = hit_obj.matrix_world eks = hit_obj.data.polygons[hit_face].edge_keys vecs = [] for vp in eks: vc1 = mtx @ hit_obj.data.vertices[vp[0]].co vc2 = mtx @ hit_obj.data.vertices[vp[1]].co vecs.append(vc1 - vc2) short_sort = sorted(vecs) start_vec = short_sort[0] self.setrot = rotation_from_vector(hit_normal, start_vec, rotate90=True, rw=True).to_quaternion() self.setrot @= Quaternion((0, 0, 1), self.rot_offset) if noloc or nosnap: obj.rotation_euler = self.setrot.to_euler() if not noloc: if self.center and nosnap: hit_wloc = hit_obj.matrix_world @ Vector( hit_obj.data.polygons[hit_face].center) obj.location = hit_wloc # no snapping place only obj.location = hit_wloc if not nosnap: obj.rotation_euler = self.setrot.to_euler() self.f_center = hit_obj.matrix_world @ Vector( hit_obj.data.polygons[hit_face].center) self.used_modal = True self.og_snaps = self.get_snap_settings(context) self.set_snap_settings(context, self.modal_snap) context.window_manager.modal_handler_add(self) bpy.ops.transform.translate('INVOKE_DEFAULT') return {'RUNNING_MODAL'} # Selection revert for non-face selections if not sel_mode[2] and mode != "OBJECT": bpy.ops.mesh.select_all(action='DESELECT') if sel_mode[1]: bm.edges.ensure_lookup_table() for v in sel_edges: bm.edges[v.index].select = True elif sel_mode[0]: bm.verts.ensure_lookup_table() for v in sel_verts: bm.verts[v.index].select = True bm.select_history.clear() bm.select_history.add(active_h) bmesh.update_edit_mesh(obj.data) if automerge: context.scene.tool_settings.use_mesh_automerge = True return {"FINISHED"}
def execute(self, context): bpy.ops.object.editmode_toggle() bm = bmesh.new() bm.from_mesh(context.active_object.data) bFaces = bm.faces bEdges = bm.edges bVerts = bm.verts fVerts = [] # Find the selected face. This will provide the plane to project onto: for f in bFaces: if f.select: for v in f.verts: fVerts.append(v) f.normal_update() normal = f.normal f.select = False break for e in bEdges: if e.select: v1 = e.verts[0] v2 = e.verts[1] if v1 in fVerts or v2 in fVerts: e.select = False continue intersection = intersect_line_plane(v1.co, v2.co, fVerts[0].co, normal) if intersection != None: print("Made it this far: intersection != None") # Use abs because we don't care what side of plane we're on: d1 = distance_point_to_plane(v1.co, fVerts[0].co, normal) d2 = distance_point_to_plane(v2.co, fVerts[0].co, normal) # If d1 is closer than we use v1 as our vertice: # "xor" with 'use_force': if (abs(d1) < abs(d2)) is not self.use_force: print("Made it this far: (abs(d1) < abs(d2)) xor force") if self.make_copy: v1 = bVerts.new() v1.co = e.verts[0].co if self.use_normal: vector = normal vector.length = abs(d1) v1.co = v1.co - (vector * sign(d1)) else: print("New coordinate", end = ": ") print(intersection) v1.co = intersection else: if self.make_copy: v2 = bVerts.new() v2.co = e.verts[1].co if self.use_normal: vector = normal vector.length = abs(d2) v2.co = v2.co - (vector * sign(d2)) else: v2.co = intersection e.select = False bm.to_mesh(context.active_object.data) bpy.ops.object.editmode_toggle() return {'FINISHED'}