From 2d307d64caec4cf5dabf774bc8e12e6055129c0b Mon Sep 17 00:00:00 2001 From: Ingo Clemens Date: Sat, 25 Nov 2023 13:17:05 +0100 Subject: [PATCH] New blending feature to blend from the original position to the new adjusted position for a given number of start and/or end points per loop. --- edgeloop.py | 192 +++++++++++++++++++++++++++++++++++++++++++- interpolate.py | 58 +++++++++++++ op_set_edge_flow.py | 23 ++++-- 3 files changed, 264 insertions(+), 9 deletions(-) diff --git a/edgeloop.py b/edgeloop.py index c064012..f8433cb 100644 --- a/edgeloop.py +++ b/edgeloop.py @@ -364,9 +364,53 @@ def set_linear(self, even_spacing): last_vert = vert - def set_flow(self, tension, min_angle, mix): + def set_flow(self, obj, tension, min_angle, mix, blend_start, blend_end, blend_type): + + # -------------------------------------------------------------- + # Get the loop vertices and calculate the start and end + # blending. + # -------------------------------------------------------------- + + # Get the start and end vertices of the selected loop. + loopStart = self.getStartVertices() + loopData = None + # If loop start data has been found get all vertices of the loop + # for blending the end points. + if loopStart: + loopData = getOrderedLoopVerts(obj, loopStart) + + # Create the dictionary with the blend values for the loop + # vertices. + blendVerts = {} + if loopData: + for loopVerts in loopData: + count = len(loopVerts) + for i in range(count): + blendVerts[loopVerts[i].index] = 1.0 + + if blend_start + blend_end >= count: + if blend_start < blend_end: + blend_end = max(count - blend_start - 1, 0) + elif blend_end < blend_start: + blend_start = max(count - blend_end - 1, 0) + else: + midCount = math.floor(count / 2) + blend_start = count - midCount + blend_end = count - blend_start + + if blend_start > 0: + interpolate.setBlendValues(loopVerts, blendVerts, blend_start, start=True) + if blend_end > 0: + interpolate.setBlendValues(loopVerts, blendVerts, blend_end, start=False) + + # -------------------------------------------------------------- + # Surface calculation. + # -------------------------------------------------------------- + visited = set() + processedVerts = set() + for edge in self.edges: target = {} @@ -487,7 +531,149 @@ def set_flow(self, tension, min_angle, mix): result = interpolate.hermite_3d( p1, p2, p3, p4, 0.5, -tension, 0) result = mathutils.Vector(result) - linear = (p2 + p3) * 0.5 + # linear = (p2 + p3) * 0.5 - vert.co = vert.co.lerp(result, mix) + # The previously used lerp function to multiply the + # result is replaced by the end point blending which + # includes the multiplying part. + # vert.co = vert.co.lerp(result, mix) # vert.co = linear.lerp(curved, tension) + + # ------------------------------------------------------ + # Blend the end points. + # ------------------------------------------------------ + if vert.index not in processedVerts: + if vert.index in blendVerts: + value = interpolate.blendValue(blendVerts[vert.index], mix, blend_type) + vert.co = vert.co.lerp(result, value) + else: + vert.co = vert.co.lerp(result, mix) + + processedVerts.add(vert.index) + + def getStartVertices(self): + """Return a list of tuples, containing the start edge and vertex + indicating an edge loop. + + :return: A list of tuples with the start edge and vertex of an + edge loop. + :rtype: list(tuple(bmesh.types.BMEdge, bmesh.types.BMVert)) + """ + verts = set() + for edge in self.edges: + for loop in edge.link_loops: + connectedEdges = loop.vert.link_edges + selectedCount = 0 + for connected in connectedEdges: + if connected.select: + selectedCount += 1 + if selectedCount == 1: + verts.add((edge, loop.vert)) + return verts + + +# ---------------------------------------------------------------------- +# Custom function to get the selected loop edges in the correct order. +# +# This most likely overlaps with existing functionality and might make +# refactoring necessary. But it shouldn't affect performance too much. +# ---------------------------------------------------------------------- + +def getOrderedLoopVerts(obj, edges): + """Return a list with all edge loop vertices for each given edge. + + :param obj: The mesh object. + :type obj: bpy.types.Object + :param edges: The list of start edges and vertices for a selected + edge loop. + :type edges: list(tuple(bmesh.types.BMEdge, bmesh.types.BMVert)) + + :return: A list of loops, each containing a list of vertices of each + edge loop. + :rtype: list(list(bmesh.types.BMVert)) + """ + # In order to get the current selection history it's necessary to + # get the bmesh from the current edit mesh. + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data) + + # Validate and get the selection history. + bm.select_history.validate() + history = bm.select_history.active + + visitedEdges = set() + loops = [] + + for edge, vertex in edges: + # Mark the current edge as visited so that it's only processed + # once. + visitedEdges.add(edge.index) + + # If the start vertex is contained in any of the previously + # collected loops, processing this vertex is not necessary since + # it can be considered the end point of an already processed + # oop. + if any(vertex in loop for loop in loops): + continue + + loopVerts = [] + loopEnd = False + + # Go over all connected edges to collect the loop vertices. + while not loopEnd: + # Add the first vertex of the current edge. + loopVerts.append(vertex) + # Get the faces connected to the current edge. + # The next edge in the loop should be connected to different + # faces. + edgeFaces = [face.index for face in edge.link_faces] + # Get the other vertex of the edge to get the connected + # edges. + vertex = edge.other_vert(vertex) + connectedEdges = vertex.link_edges + + nextEdge = False + + # Go over the connected edges and check it one is selected. + # Already visited edges can be skipped. + for e in connectedEdges: + if e.select and e.index not in visitedEdges: + # Get the connected faces. These should be unique + # from the previous edge. + connectedFaces = e.link_faces + if not any(face.index in edgeFaces for face in connectedFaces): + # Mark the edge as visited. + visitedEdges.add(e.index) + # The next edge is the current one. + edge = e + # Continue. + nextEdge = True + # If there is no next edge add the last vertex to the list. + if not nextEdge: + loopVerts.append(vertex) + loopEnd = True + + # Check for the order of vertices depending on the selected + # edge. + if history: + index = None + if isinstance(history, bmesh.types.BMVert): + index = history.index + elif isinstance(history, bmesh.types.BMEdge): + index = history.verts[0].index + + # If the active vertex is contained in the loop list but not + # located at the start of the list, reverse the index list. + if index is not None: + indices = [v.index for v in loopVerts] + if index in indices: + indexPos = indices.index(index) + if indexPos > 1: + loopVerts.reverse() + + # Add the list of vertices for the loop. + loops.append(loopVerts) + + bpy.ops.object.mode_set(mode='OBJECT') + + return loops diff --git a/interpolate.py b/interpolate.py index e00fc0d..e7051c8 100644 --- a/interpolate.py +++ b/interpolate.py @@ -78,3 +78,61 @@ def hermite_3d(p1, p2, p3, p4, mu, tension, bias): z = hermite_1d(p1[2], p2[2], p3[2], p4[2], mu, tension, bias) return [x, y, z] + + +# ---------------------------------------------------------------------- +# Blending towards the end points. +# ---------------------------------------------------------------------- + +def setBlendValues(loopVerts, blendVerts, count, start=True): + """Set the blending values for all vertices of the given edge loop. + + :param loopVerts: The list of loop vertices. + :type loopVerts: list(bmesh.types.BMVert) + :param blendVerts: The dictionary of loop vertices and their + blending values. + This dictionary gets mutated. + :type blendVerts: dict + :param count: The number of vertices to blend. + :type count: int + :param start: True, if the blending is calculated for the start of + the loop. + :type start: bool + """ + # Get the overall loop length. + totalLength = 0 + + if not start: + loopVerts.reverse() + + partials = [0] + for i in range(count): + i = min(i, len(loopVerts) - 2) + length = (loopVerts[i + 1].co - loopVerts[i].co).length + totalLength += length + partials.append(totalLength) + + for i in range(count): + value = partials[i] / totalLength + blendVerts[loopVerts[i].index] = value + + +def blendValue(value, strength, blendType): + """Blend the given value depending on the interpolation type. + + :param value: The value to blend. + :type value: float + :param strength: The strength of the blending. + :type strength: float + :param blendType: The curve type used for blending. + :type blendType: str + + :return: The interpolated value. + :rtype: float + """ + value = max(0.0, min(1.0, value)) + + if blendType == "SMOOTH": + return value * value * (3 - 2 * value) * strength + else: + return value * strength diff --git a/op_set_edge_flow.py b/op_set_edge_flow.py index ac1562b..0398bda 100644 --- a/op_set_edge_flow.py +++ b/op_set_edge_flow.py @@ -78,13 +78,19 @@ class SetEdgeFlowOP(bpy.types.Operator, SetEdgeLoopBase): bl_idname = "mesh.set_edge_flow" bl_label = "Set edge flow" bl_options = {'REGISTER', 'UNDO'} - bl_description = "adjust edge loops to curvature" + bl_description = "Adjust edge loops to match surface curvature" + curveItems = (("LINEAR", "Linear", ""), + ("SMOOTH", "Smooth", "")) + + mix: FloatProperty(name="Mix", default=1.0, min=0.0, max=1.0, description="Interpolate between inital position and the calculated end position") + blend_start: bpy.props.IntProperty(name="Blend Start", default=0, min=0, description="The number of vertices from the start of the loop used to blend to the adjusted loop position") + blend_end: bpy.props.IntProperty(name="Blend End", default=0, min=0, description="The number of vertices from the end of the loop used to blend to the adjusted loop position") + blend_type: bpy.props.EnumProperty(name="Blend Curve", items=curveItems, description="The interpolation used to blend between the adjusted loop position and the unaffected start and/or end points") tension : IntProperty(name="Tension", default=180, min=-500, max=500, description="Tension can be used to tighten up the curvature") iterations : IntProperty(name="Iterations", default=1, min=1, soft_max=32, description="How often the curveature operation is repeated") #bias = IntProperty(name="Bias", default=0, min=-100, max=100) min_angle : IntProperty(name="Min Angle", default=0, min=0, max=180, subtype='FACTOR', description="After which angle the edgeloop curvature is ignored") - mix: FloatProperty(name="Mix", default=1.0, min=0.0, max=1.0, description="Interpolate between inital position and the calculated end position") def execute(self, context): @@ -101,7 +107,13 @@ def execute(self, context): for obj in self.objects: for i in range(self.iterations): for edgeloop in self.edgeloops[obj]: - edgeloop.set_flow(self.tension / 100.0, math.radians(self.min_angle), self.mix ) + edgeloop.set_flow(obj=obj, + tension=self.tension / 100.0, + min_angle=math.radians(self.min_angle), + mix=self.mix, + blend_start=self.blend_start, + blend_end=self.blend_end, + blend_type=self.blend_type) self.bm[obj].to_mesh(obj.data) @@ -127,8 +139,7 @@ def invoke(self, context, event): self.bias = 0 self.mix = 1.0 #self.min_angle = 0 + self.blend_start = 0 + self.blend_end = 0 return self.execute(context) - - -