Przeglądaj źródła

Add: GetCurvePoint, GetNearestFactorOnCurve

this commit uses some old code from Chaintools, almost unedited.
in FindNearestPointOnWireMesh, I'm not 100% sure I am calculating the distance correctly
(what's the deal with the sqrt's when I am already getting the length of the vector?)
so I may have to revisit it. but it seems to work real well.
Joseph Brandenburg 6 miesięcy temu
rodzic
commit
094d2c6e85
5 zmienionych plików z 277 dodań i 33 usunięć
  1. 2 0
      __init__.py
  2. 63 9
      misc_nodes.py
  3. 25 0
      misc_nodes_socket_templates.py
  4. 46 1
      misc_nodes_ui.py
  5. 141 23
      utilities.py

+ 2 - 0
__init__.py

@@ -128,6 +128,7 @@ utility_category = [
         NodeItem("UtilityCompare"),
         NodeItem("UtilityPrint"),
         NodeItem("UtilitySeparateVector"),
+        NodeItem("UtilityGetNearestFactorOnCurve"),
         NodeItem("UtilityKDChoosePoint"),
         NodeItem("UtilityKDChooseXForm"),
     ]
@@ -138,6 +139,7 @@ matrix_category = [
         NodeItem("UtilityNumberOfCurveSegments"),
         NodeItem("UtilityMatrixFromCurveSegment"),
         NodeItem("UtilityPointFromCurve"),
+        NodeItem("UtilityGetCurvePoint"),
         NodeItem("UtilityPointFromBoneMatrix"),
         NodeItem("UtilitySetBoneLength"),
         NodeItem("UtilityGetBoneLength"),

+ 63 - 9
misc_nodes.py

@@ -1,8 +1,7 @@
 from .node_container_common import *
 from .base_definitions import MantisNode, NodeSocket
-
 from .xForm_containers import xFormArmature, xFormBone
-
+from .misc_nodes_socket_templates import *
 from math import pi, tau
 
 def TellClasses():
@@ -26,6 +25,8 @@ def TellClasses():
              UtilityMatricesFromCurve,
              UtilityNumberOfCurveSegments,
              UtilityMatrixFromCurveSegment,
+             UtilityGetCurvePoint,
+             UtilityGetNearestFactorOnCurve,
              UtilityKDChoosePoint,
              UtilityKDChooseXForm,
              UtilityMetaRig,
@@ -77,14 +78,15 @@ def matrix_from_head_tail(head, tail, normal=None):
         m = m.transposed().to_4x4()
     return m
 
-def get_ribbon_mesh_from_curve(curve_name : str, execution_id : str, bContext):
+def get_mesh_from_curve(curve_name : str, execution_id : str, bContext, ribbon=True):
         from bpy import data
         curve = data.objects.get(curve_name)
         assert curve.type == 'CURVE', f"ERROR: object is not a curve: {curve_name}"
         from .utilities import mesh_from_curve
-        m_name = curve_name+'.'+str(hash(curve_name+'.'+execution_id))
+        curve_type='ribbon' if ribbon else 'wire'
+        m_name = curve_name+'.'+str(hash(curve_name+'.'+curve_type+'.'+execution_id))
         if not (m := data.meshes.get(m_name)):
-            m = mesh_from_curve(curve, bContext)
+            m = mesh_from_curve(curve, bContext, ribbon)
             m.name = m_name
         return m
 
@@ -302,7 +304,7 @@ class UtilityMatrixFromCurve(MantisNode):
             mat[3][3] = 1.0
         else:
             if bContext is None: bContext = bpy.context # is this wise?
-            m = get_ribbon_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
+            m = get_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
             from .utilities import data_from_ribbon_mesh
             #
             num_divisions = self.evaluate_input("Total Divisions")
@@ -359,7 +361,7 @@ class UtilityPointFromCurve(MantisNode):
             raise GraphError(f"ERROR: Object {curve.name} is not a curve.")
         else:
             if bContext is None: bContext = bpy.context # is this wise?
-            m = get_ribbon_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
+            m = get_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
             from .utilities import data_from_ribbon_mesh
             #
             num_divisions = 1
@@ -406,7 +408,7 @@ class UtilityMatricesFromCurve(MantisNode):
             m[3][3] = 1.0
         else:
             if bContext is None: bContext = bpy.context # is this wise?
-            mesh = get_ribbon_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
+            mesh = get_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
             from .utilities import data_from_ribbon_mesh
             num_divisions = self.evaluate_input("Total Divisions")
             factors = [0.0] + [(1/num_divisions*(i+1)) for i in range(num_divisions)]
@@ -497,7 +499,7 @@ class UtilityMatrixFromCurveSegment(MantisNode):
             raise GraphError(f"ERROR: Object {curve.name} is not a curve.")
         else:
             if bContext is None: bContext = bpy.context # is this wise?
-            m = get_ribbon_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
+            m = get_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
             from .utilities import data_from_ribbon_mesh
             # this section is dumb, but it is because the data_from_ribbon_mesh
             #  function is designed to pull data from many splines at once (for optimization)
@@ -538,6 +540,58 @@ class UtilityMatrixFromCurveSegment(MantisNode):
     def bFinalize(self, bContext=None):
         cleanup_curve(self.evaluate_input("Curve"), self.base_tree.execution_id)
 
+class UtilityGetCurvePoint(MantisNode):
+    def __init__(self, signature, base_tree):
+        super().__init__(signature, base_tree, GetCurvePointSockets)
+        self.init_parameters()
+        self.node_type = "UTILITY"
+    
+    def bPrepare(self, bContext=None):
+        import bpy
+        curve = bpy.data.objects.get(self.evaluate_input("Curve"))
+        if not curve:
+            raise RuntimeError(f"No curve found for {self}.")
+        elif curve.type != "CURVE":
+            raise GraphError(f"ERROR: Object {curve.name} is not a curve.")
+        spline = curve.data.splines[self.evaluate_input("Spline Index")]
+        if spline.type == 'BEZIER':
+            bez_pt = spline.bezier_points[self.evaluate_input("Index")]
+            self.parameters["Point"]=bez_pt.co
+            self.parameters["Left Handle"]=bez_pt.handle_left
+            self.parameters["Right Handle"]=bez_pt.handle_right
+        else:
+            pt = spline.points[self.evaluate_input("Index")]
+            self.parameters["Point"]=(pt.co[0], pt.co[1], pt.co[2])
+        self.prepared, self.executed = True, True
+
+class UtilityGetNearestFactorOnCurve(MantisNode):
+    def __init__(self, signature, base_tree):
+        super().__init__(signature, base_tree, GetNearestFactorOnCurveSockets)
+        self.init_parameters()
+        self.node_type = "UTILITY"
+    
+    def bPrepare(self, bContext = None,):
+        import bpy
+        curve = bpy.data.objects.get(self.evaluate_input("Curve"))
+        if not curve:
+            raise RuntimeError(f"No curve found for {self}.")
+        elif curve.type != "CURVE":
+            raise GraphError(f"ERROR: Object {curve.name} is not a curve.")
+        else:
+            if bContext is None: bContext = bpy.context # is this wise?
+            m = get_mesh_from_curve(curve.name,
+                                    self.base_tree.execution_id,
+                                    bContext, ribbon=False)
+            # this is confusing but I am not re-writing these old functions
+            from .utilities import FindNearestPointOnWireMesh as nearest_point
+            spline_index = self.evaluate_input("Spline Index")
+            ref_pt = self.evaluate_input("Reference Point")
+            splines_points = [ [] for i in range (spline_index)]
+            splines_points.append([ref_pt])
+            pt =  nearest_point(m, splines_points)[spline_index][0]
+            self.parameters["Factor"] = pt
+        self.prepared, self.executed = True, True
+
 class UtilityKDChoosePoint(MantisNode):
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)

+ 25 - 0
misc_nodes_socket_templates.py

@@ -0,0 +1,25 @@
+from .base_definitions import MantisSocketTemplate as SockTemplate
+
+GetCurvePointSockets=[
+    CurveTemplate := SockTemplate(name="Curve", bl_idname='EnumCurveSocket', 
+        is_input=True,),
+    SplineIndexTemplate := SockTemplate(name="Spline Index",
+        bl_idname='UnsignedIntSocket', is_input=True, default_value=0,),
+    IndexTemplate := SockTemplate(name="Index",
+        bl_idname='UnsignedIntSocket', is_input=True, default_value=0,),
+    OutputPointTemplate := SockTemplate(name="Point",
+        bl_idname='VectorSocket', is_input=False,),
+    OutputLeftHandleTemplate := SockTemplate(name="Left Handle",
+        bl_idname='VectorSocket', is_input=False, hide=True),
+    OutputRightHandleTemplate := SockTemplate(name="Right Handle",
+        bl_idname='VectorSocket', is_input=False, hide=True),
+]
+
+GetNearestFactorOnCurveSockets=[
+    CurveTemplate,
+    SplineIndexTemplate,
+    ReferencePointTemplate := SockTemplate(name="Reference Point",
+        bl_idname='VectorSocket', is_input=True,),
+    OutputFactorTemplate := SockTemplate(name="Factor",
+        bl_idname='FloatSocket', is_input=False,),
+]

+ 46 - 1
misc_nodes_ui.py

@@ -1,7 +1,7 @@
 import bpy
 from bpy.types import Node
 from .base_definitions import MantisUINode, get_signature_from_edited_tree
-
+from .misc_nodes_socket_templates import *
 
 from .utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
@@ -30,6 +30,8 @@ def TellClasses():
              UtilityMatricesFromCurve,
              UtilityNumberOfCurveSegments,
              UtilityMatrixFromCurveSegment,
+             UtilityGetCurvePoint,
+             UtilityGetNearestFactorOnCurve,
              UtilityKDChoosePoint,
              UtilityKDChooseXForm,
             #  ScaleBoneLengthNode,
@@ -304,6 +306,38 @@ class UtilityMatrixFromCurveSegment(Node, MantisUINode):
         self.inputs.new('UnsignedIntSocket', 'Segment Index')
         self.outputs.new("MatrixSocket", "Matrix")
         self.initialized = True
+
+class UtilityGetCurvePoint(Node, MantisUINode):
+    bl_idname = 'UtilityGetCurvePoint'
+    bl_label = "Get Curve Point"
+    bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
+    mantis_node_class_name=bl_idname
+
+    def init(self, context):
+        self.init_sockets(GetCurvePointSockets)
+        self.initialized = True
+
+    def display_update(self, parsed_tree, context):
+        self.outputs["Point"].hide=False
+        self.outputs["Left Handle"].hide=True
+        self.outputs["Right Handle"].hide=True
+        spline_index = self.inputs['Spline Index'].default_value
+        index = self.inputs['Index'].default_value
+        curve = self.inputs['Curve'].default_value
+        if self.inputs['Spline Index'].is_linked or self.inputs['Index'].is_linked \
+            or self.inputs['Curve'].is_linked:
+            mantis_node = parsed_tree.get(get_signature_from_edited_tree(self, context))
+            spline_index = mantis_node.evaluate_input("Spline Index")
+            index = mantis_node.evaluate_input("Index")
+            curve = mantis_node.evaluate_input("Curve")
+        if curve := bpy.data.objects.get(curve):
+            if curve.type != "CURVE":
+                self.outputs["Point"].hide=True
+            spline = curve.data.splines[spline_index]
+            if spline.type == 'BEZIER':
+                self.outputs["Left Handle"].hide=False
+                self.outputs["Right Handle"].hide=False
     
 class UtilityMatricesFromCurve(Node, MantisUINode):
     """Gets a matrix from a curve."""
@@ -335,6 +369,17 @@ def display_update_choose_nearest(self, parsed_tree, context):
     else:
         self.outputs.display_shape = 'CIRCLE'
 
+class UtilityGetNearestFactorOnCurve(Node, MantisUINode):
+    bl_idname = 'UtilityGetNearestFactorOnCurve'
+    bl_label = "Get Factor on Curve at Point"
+    bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
+    mantis_node_class_name=bl_idname
+
+    def init(self, context):
+        self.init_sockets(GetNearestFactorOnCurveSockets)
+        self.initialized = True
+
 class UtilityKDChoosePoint(Node, MantisUINode):
     """Chooses the nearest point with a KD Tree."""
     bl_idname = "UtilityKDChoosePoint"

+ 141 - 23
utilities.py

@@ -724,36 +724,154 @@ def SetRibbonData(m, ribbon):
         #if this is a circle, the last v in vertData has a length, otherwise 0
     return ribbonData, totalLength
 
+def WireMeshEdgeLengths(m, wire):
+    circle = False
+    vIndex = wire.copy()
+    for e in m.edges:
+        if ((e.vertices[0] == vIndex[-1]) and (e.vertices[1] == vIndex[0])):
+            #this checks for an edge between the first and last vertex in the wire
+            circle = True
+            break
+    lengths = []
+    for i in range(len(vIndex)):
+        v = m.vertices[vIndex[i]]
+        if (circle == True):
+            vNextInd = vIndex[old_bad_wrap_that_should_be_refactored((i+1), len(vIndex) - 1)]
+        else:
+            vNextInd = vIndex[cap((i+1), len(vIndex) - 1 )]
+        vNext = m.vertices[vNextInd]
+        lengths.append(( v.co - vNext.co ).length)
+    #if this is a circular wire mesh, this should wrap instead of cap
+    return lengths
 
-def mesh_from_curve(crv, context,):
+def GetDataFromWire(m, wire):
+    vertData = []
+    vIndex = wire.copy()
+    lengths = WireMeshEdgeLengths(m, wire)
+    lengths.append(0)
+    totalLength = sum(lengths)
+    for i, vInd in enumerate(vIndex):
+        #-1 to avoid IndexError
+        vNext = vIndex[ (old_bad_wrap_that_should_be_refactored(i+1, len(vIndex) - 1)) ]
+        vertData.append((vInd, vNext, lengths[i]))
+    #if this is a circle, the last v in vertData has a length, otherwise 0
+    return vertData, totalLength
+
+def DetectWireEdges(mesh):
+    # Returns a list of vertex indices belonging to wire meshes
+    # NOTE: this assumes a mesh object with only wire meshes
+    ret = []
+    import bmesh
+    bm = bmesh.new()
+    try:
+        bm.from_mesh(mesh)
+        ends = []
+        for v in bm.verts:
+            if (len(v.link_edges) == 1):
+                ends.append(v.index)
+        for e in bm.edges:
+            assert (e.is_wire == True),"This function can only run on wire meshes"
+            if (e.verts[1].index - e.verts[0].index != 1):
+                ends.append(e.verts[1].index)
+                ends.append(e.verts[0].index)
+        for i in range(len(ends)//2): # // is floor division
+            beg = ends[i*2]
+            end = ends[(i*2)+1]
+            indices = [(j + beg) for j in range ((end - beg) + 1)]
+            ret.append(indices)
+    finally:
+        bm.free()
+        return ret
+
+def FindNearestPointOnWireMesh(m, pointsList):
+    from mathutils import Vector
+    from mathutils.geometry import intersect_point_line
+    from math import sqrt
+    wires = DetectWireEdges(m)
+    ret = []
+    # prevFactor = None
+    for wire, points in zip(wires, pointsList):
+        vertData, total_length = GetDataFromWire(m, wire)
+        factorsOut = []
+        for p in points:
+            prevDist = float('inf')
+            curDist  = float('inf')
+            v1 = None
+            v2 = None
+            for i in range(len(vertData) - 1):
+                #but it shouldn't check the last one
+                if (p == m.vertices[i].co):
+                    v1 = vertData[i]
+                    v2 = vertData[i+1]
+                    offset = 0
+                    break
+                else:
+                    curDist = ( (sqrt((m.vertices[vertData[i][0]].co - p).length)) +
+                                (sqrt((m.vertices[vertData[i][1]].co - p).length)) )/2
+                if (curDist < prevDist):
+                    v1 = vertData[i]
+                    v2 = vertData[i+1]
+                    prevDist = curDist
+                    offset = intersect_point_line(p, m.vertices[v1[0]].co, 
+                                                     m.vertices[v2[0]].co)[1]
+            if (offset < 0):
+                offset = 0
+            elif (offset > 1):
+                offset = 1
+            # Assume the vertices are in order
+            v1Length = 0
+            v2Length = v2[2]
+            for i in range(v1[0]):
+                v1Length += vertData[i][2]
+            factor = ((offset * (v2Length)) + v1Length )/total_length
+            factor = wrap(0, 1, factor) # doesn't hurt to wrap it if it's over 1 or less than 0
+            factorsOut.append(factor)
+        ret.append( factorsOut )
+    return ret
+
+
+def mesh_from_curve(crv, context, ribbon=True):
     """Utility function for converting a mesh to a curve
        which will return the correct mesh even with modifiers"""
     import bpy
+    m = None
     bevel = crv.data.bevel_depth
     extrude = crv.data.extrude
     offset = crv.data.offset
-    if (len(crv.modifiers) > 0):
-        do_unlink = False
-        if (not context.scene.collection.all_objects.get(crv.name)):
-            context.collection.objects.link(crv) # i guess this forces the dg to update it?
-            do_unlink = True
-        dg = context.view_layer.depsgraph
-        # just gonna modify it for now lol
-        EnsureCurveIsRibbon(crv)
-        # try:
-        dg.update()
-        mOb = crv.evaluated_get(dg)
-        m = bpy.data.meshes.new_from_object(mOb)
-        m.name=crv.data.name+'_mesh'
-        if (do_unlink):
-            context.collection.objects.unlink(crv)
-    else: # (ಥ﹏ಥ) why can't I just use this !
-        # for now I will just do it like this
-        EnsureCurveIsRibbon(crv)
-        m = bpy.data.meshes.new_from_object(crv)
-    crv.data.bevel_depth = bevel
-    crv.data.extrude = extrude
-    crv.data.offset = offset
+    try:
+        if (len(crv.modifiers) > 0):
+            do_unlink = False
+            if (not context.scene.collection.all_objects.get(crv.name)):
+                context.collection.objects.link(crv) # i guess this forces the dg to update it?
+                do_unlink = True
+            dg = context.view_layer.depsgraph
+            # just gonna modify it for now lol
+            if ribbon:
+                EnsureCurveIsRibbon(crv)
+            else:
+                crv.data.bevel_depth=0
+                crv.data.extrude=0
+                crv.data.offset=0
+            # try:
+            dg.update()
+            mOb = crv.evaluated_get(dg)
+            m = bpy.data.meshes.new_from_object(mOb)
+            m.name=crv.data.name+'_mesh'
+            if (do_unlink):
+                context.collection.objects.unlink(crv)
+        else: # (ಥ﹏ಥ) why can't I just use this !
+            # for now I will just do it like this
+            if ribbon:
+                EnsureCurveIsRibbon(crv)
+            else:
+                crv.data.bevel_depth=0
+                crv.data.extrude=0
+                crv.data.offset=0
+            m = bpy.data.meshes.new_from_object(crv)
+    finally:
+        crv.data.bevel_depth = bevel
+        crv.data.extrude = extrude
+        crv.data.offset = offset
     return m
 
 def DetectRibbon(f, bm, skipMe):