# coding: utf-8
#
# Copyright © 2020 MSC.Software Corporation
#  
# Permission to use Scripts
#  
# Permission is granted to MSC.Software customers with valid licenses of the MSC Apex™ software 
# to use script files posted on this page (collectively “Scripts”) solely for customers’ own authorized 
# internal use in conjunction with licensed use of the MSC Apex software.  
# No other use is permitted.  No license to MSC Apex or other software is granted under this Permission.
#  
# SCRIPTS ARE PROVIDED ON AN "AS-IS" BASIS, WITH NO WARRANTIES OF ANY KIND.  USE OF THE SCRIPTS IS AT CUSTOMER'S SOLE RISK.
#  
# IN NO EVENT SHALL MSC.SOFTWARE CORPORATION, ITS AFFILIATES, SUBSIDIARIES, SUPPLIERS, AGENTS, REPRESENTATIVES OR EMPLOYEES BE LIABLE FOR ANY 
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, TORT, 
# STRICT LIABILITY, OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION USE OF THE SCRIPTS, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  
#  
# Customer shall indemnify, defend and hold MSC.Software Corporation, its affiliates, subsidiaries, suppliers, agents, 
# employees or representatives (collectively, “Indemnified Parties”) harmless from and against any third-party claims 
# brought against Indemnified Parties rising out of or in connection with Customer’s use of the Scripts.
#

# Author: Larry Pearce
# Revised by: Larry Pearce
# Revision History
# Date         Change                                 By
# 2020-10-13   Original code (3.0)                    Larry Pearce

import apex
import math
import os


def bushingProps(s1, s2, s3, s4, s5, s6):
    # Input:   variable        datatype         description
    #          s1              float            translational stiffness u1
    #          s2              float            translational stiffness u2
    #          s3              float            translational stiffness u3
    #          s4              float            rotational stiffness r1
    #          s5              float            rotational stiffness r2
    #          s6              float            rotational stiffness r3
    # Output:  variable        datatype         description
    #          connProps       connector property object
    #
    # Create an Apex connector property for Bushing connector

    connProps = apex.attribute.createConnectorProperties(
          stiffness               = 0.0,
          damping                 = 0.0,
          translationalStiffnessX = s1,
          translationalStiffnessY = s2,
          translationalStiffnessZ = s3,
          rotationalStiffnessX    = s4,
          rotationalStiffnessY    = s5,
          rotationalStiffnessZ    = s6,
          translationalDampingX   = 0.0,
          translationalDampingY   = 0.0,
          translationalDampingZ   = 0.0,
          rotationalDampingX      = 0.0,
          rotationalDampingY      = 0.0,
          rotationalDampingZ      = 0.0,
          diameter                = 0.0,
          linkMaterial            = None)
    return connProps


def flexibleLinkProps(dia, matl):
    # Input:   variable        datatype         description
    #          dia             float            fastener diameter
    #          matl            material object
    # Output:  variable        datatype         description
    #          connProps       connector property object
    #
    # Create an Apex connector property for Flexible connector

    connProps = apex.attribute.createConnectorProperties(
          stiffness               = 0.0,
          damping                 = 0.0,
          translationalStiffnessX = 0.0,
          translationalStiffnessY = 0.0,
          translationalStiffnessZ = 0.0,
          rotationalStiffnessX    = 0.0,
          rotationalStiffnessY    = 0.0,
          rotationalStiffnessZ    = 0.0,
          translationalDampingX   = 0.0,
          translationalDampingY   = 0.0,
          translationalDampingZ   = 0.0,
          rotationalDampingX      = 0.0,
          rotationalDampingY      = 0.0,
          rotationalDampingZ      = 0.0,
          diameter                = dia,
          linkMaterial            = matl)
    return connProps


def rigidLinkProps():
    # Input:   variable        datatype         description
    #          <none>
    # Output:  variable        datatype         description
    #          connProps       connector property object
    #
    # Create an Apex connector property for Rigid connector

    connProps = apex.attribute.createConnectorProperties(
          stiffness               = 0.0,
          damping                 = 0.0,
          translationalStiffnessX = 0.0,
          translationalStiffnessY = 0.0,
          translationalStiffnessZ = 0.0,
          rotationalStiffnessX    = 0.0,
          rotationalStiffnessY    = 0.0,
          rotationalStiffnessZ    = 0.0,
          translationalDampingX   = 0.0,
          translationalDampingY   = 0.0,
          translationalDampingZ   = 0.0,
          rotationalDampingX      = 0.0,
          rotationalDampingY      = 0.0,
          rotationalDampingZ      = 0.0,
          diameter                = 0.0,
          linkMaterial            = None)
    return connProps


def calcDistance(n1, n2):
    # Input:   variable        datatype         description
    #          n1              object           node
    #          n2              object           node
    # Output:  variable        datatype         description
    #          dist            float            distance between n1 and n2
    #
    # Calculate distance between 2 locations (nodes)

    loc1 = n1.asNode().getLocation()
    loc2 = n2.asNode().getLocation()
    dx = loc2.getX() - loc1.getX()
    dy = loc2.getY() - loc1.getY()
    dz = loc2.getZ() - loc1.getZ()

    dist = dx**2 + dy**2 + dz**2

    if dist > 0.0: dist = math.sqrt(dist)

    return dist


def normalizeVector(vec):
    # Input:   variable        datatype         description
    #          vec             Vector3D
    # Output:  variable        datatype         description
    #          <return>        Vector3D
    #
    # Calculate distance between 2 locations (nodes)

    # In this script, this vector will always have a length
    dx = vec.getX()/vec.getLength()
    dy = vec.getY()/vec.getLength()
    dz = vec.getZ()/vec.getLength()

    return apex.construct.Vector3D(dx, dy, dz)


def areVectorsAligned(vec1, vec2):
    # Input:   variable        datatype         description
    #          vec1            Vector3D
    #          vec2            Vector3D
    # Output:  variable        datatype         description
    #          isAligned       logical          True if aligned
    #
    # Calculate distance between 2 locations (nodes)

    # The vectors are assumed to be unit vectors
    dot  = vec1.getX()*vec2.getX()
    dot += vec1.getY()*vec2.getY()
    dot += vec1.getZ()*vec2.getZ()
    dot = abs(dot)

    upperRange = 1.0 + .01
    lowerRange = 1.0 - .01
    if upperRange >= dot >= lowerRange:
        isAligned = True
    else:
        isAligned = False

    return isAligned


def getNearestNodeForSurface(point, surf):
    # Input:   variable        datatype         description
    #          point           point object
    #          surf            surface object
    # Output:  variable        datatype         description
    #          nearNode        node object
    #
    # Returns the nearest node to the point on the surface

    nearNode = None

    nodes = surf.getNodes()
    if nodes.len() == 0: return nearNode

    iNodes = apex.IPhysicalCollection()
    for node in nodes:
        iNodes.append(node.asEntity())

    mySearch = apex.utility.ProximitySearch()
    mySearch.insertCollection(iNodes)

    loc = point.getLocation()
    try:
        nearestObject = mySearch.findNearestObject(loc)
        nearNode = nearestObject.getNearestObject()
    except:
        nearNode = None

    return nearNode


def calcBushingStiffness(dia, length, matl):
    # Input:   variable        datatype         description
    #          dia             float            fastener diameter
    #          length          float            fastener length
    #          matl            object           fastener material
    # Output:  variable        datatype         description
    #          stiff           Python List      6 stiffness values (3 translational, 3 rotational)
    #
    # Calculates stiffness of CBUSH using the formulas for PFAST documented in the Nastran QRG

    E  = matl.getElasticModulus()
    NU = matl.getPoissonRatio()
    G = E/2.0/(1.0+NU)

    # if length is zero, I am going to set it to unity
    if length <= 0.0: length = 1.0

    PI = 3.14159

    A = PI*dia**2/4.0
    I = PI*dia**4/64.0
    J = 2*I

    AS = 0.75*A

    s1 = E*A/length
    s2 = G*AS/length
    s3 = s2
    s4 = G*J/length
    s5 = E*I/length + G*AS*length/3.0
    s6 = s5

    stiff = [s1, s2, s3, s4, s5, s6]

    return stiff


def createMaterial():
    matl = apex.catalog.createMaterial(name = "Aluminum", description = "created by script", color = [40, 40, 40])
    matl.update(elasticModulus = 10.0E6)
    matl.update(poissonRatio   = .3)
    matl.update(density        = .101/386.)


def main(dict={}):
    myModel = apex.currentModel()

    # Tolerance represents the max separation distance of the nodes to be connected.

    # An automatic tolerance could be calculated based on shell thicknesses found in the model.  This would
    # likely need to incorporate shell offsets as well.

    connName        = dict["name"]
    matlName        = dict["material"]
    fastenerType    = dict["fastenerType"]
    fastenerDia     = float(dict["diameter"])
    tol             = float(dict["tolerance"])
    stiffnessMethod = dict["stiffnessMethod"]

    print("Name:           "+connName)
    print("Material:       "+matlName)
    print("Fastener type:  "+fastenerType)
    print("Diameter:       "+str(fastenerDia))
    print("Tolerance:      "+str(tol))

    if fastenerType == "":
        print("Fastener Type is BLANK!")
        return

    if fastenerDia <= 0.0:
        print("Invalid fastener diameter!")
        return

    if matlName == "None" and fastenerType != "Rigid Link":
        print("No material selected!")
        return

    # Get the material that goes with the selected material name
    fastenerMatl = None
    if matlName != "None":
        matls = apex.catalog.getMaterials([{"path":myModel.getName()+"/Materials"}])
        for matl in matls:
            if matl.getName() == matlName:
                fastenerMatl = matl
                break
        if fastenerMatl is None:
            print("Unknown material?")
            return

    if tol <= 0.0:
        tol = 0.3
        print("Updated Tolerance: "+str(tol))

    selPoints = apex.entityCollection()
    _selected = apex.selection.getCurrentSelection()  # these are the selected points
    for sel in _selected: selPoints.append(sel)

    if selPoints.len() == 0:
        allParts = myModel.getParts(True)
        for part in allParts: selPoints += part.getPoints()

    if selPoints.len() == 0:
        print("There are no points in the model!")
        return

    # Look for parts that contain mesh bodies on surfaces
    allParts = myModel.getParts(True)

    shellMeshParts = apex.entityCollection()
    for part in allParts:
        meshes = part.getMeshes()
        if meshes.len() == 0: continue
        surfs = part.getSurfaces()
        surfs += part.getFacetedSurfaces()
        for surf in surfs:
            meshes = surf.getMeshes()
            if meshes.len() > 0:
                shellMeshParts.append(part)
                break

    print("Total Parts: " + str(allParts.len()))
    print("Parts with surface meshes: " + str(shellMeshParts.len()))

    if shellMeshParts.len() == 0:
        print("No meshed surfaces in any Part?")
        return

    # This will facilitate a very simple method to detect existing connections.  It is not efficient nor perfect.
    # Get all connections.  Get end point nodes.  Add end point nodes to a list of strings for comparison
    existConnNodeIds = []  # node IDs used in existing connections
    existConns = apex.entityCollection()
    assys = myModel.getAssemblies()
    for assy in assys:
        existConns += apex.attribute.getConnectors([{"path":assy.getPath()}])
    for conn in existConns:
        interfacePoints = []
        interfacePoints.append(conn.getEnd1InterfacePoint())
        interfacePoints.append(conn.getEnd2InterfacePoint())
        associatedNodes = []
        for pt in interfacePoints:
            if pt is None: continue
            ent = pt.getAssociatedEntity()
            if ent is None: continue
            # Because I am only supporting point-to-point connections the associated entity must be a node
            if ent.getEntityType() == apex.EntityType.Node:
                associatedNodes.append(ent.asNode().getId())
        if len(associatedNodes) == 2:
            existConnNodeIds.append(str(associatedNodes[0]) + "-" + str(associatedNodes[1]))
            existConnNodeIds.append(str(associatedNodes[1]) + "-" + str(associatedNodes[0]))

    print("Existings node-to-node connections: " + str(len(existConnNodeIds)))

    # See if selected Points have an associated seed and if so add them to the proximity search list
    iPoints = apex.IPhysicalCollection()
    points  = apex.entityCollection()
    for point in selPoints:
        associatedSeed = point.getAssociatedSeedPoint()
        if associatedSeed is None: continue
        points.append(point)
        iPoints.append(point.asEntity())

    print("Selected points with Seeds: " + str(points.len()))

    if points.len() == 0:
        print("None of the selected points have associated Seeds!")
        return

    mySearch = apex.utility.ProximitySearch()
    mySearch.insertCollection(iPoints)

    # Loop through selected seed points to find closest seed point to connect to
    endPoints = [] # List of lists.  The sublists are the end point nodes of the connection
    maxLength = 0.0         # I want to echo the maximum fastener length
    minLength = 999999999.0 # I want to echo the minimum fastener length
    for point in points:
        pointParent = point.getParent()
        pointName   = point.getName()
        pointLoc    = point.getLocation()

        # Get seed associated to the Point and surface associated to the seed
        seed = point.getAssociatedSeedPoint()
        surf1 = seed.getTarget().getBody().asSurface()

        # Calculate normal to surface at the point.  This will be compared later to see if connection is "aligned"
        normVec1 = surf1.evaluateSurfaceNormal(point)
        normVec1 = normalizeVector(normVec1)

        nearbyObjects = mySearch.findNearestObjects(pointLoc, 36) # this is high due to potential sideways finds!
        foundObjects  = nearbyObjects.foundObjects()
        foundObjDists = nearbyObjects.getDistances()

        n1 = getNearestNodeForSurface(point, surf1)

        for data in enumerate(foundObjects):
            obj = data[1]
            k   = data[0]

            point2       = obj.asPoint()
            point2Parent = point2.getParent()
            # A point cannot find itself
            if point2Parent.getName()+point2.getName() == pointParent.getName()+pointName: continue

            # To be potentially connected, the point must be within the entered tolerance
            if foundObjDists[k] > tol: continue

            seed = point2.getAssociatedSeedPoint()
            surf2 = seed.getTarget().getBody().asSurface()

            # Cannot find a point on the same surface
            if pointParent.getName()+surf.getName() == point2Parent.getName()+surf2.getName(): continue

            normVec2 = surf2.evaluateSurfaceNormal(point2)
            normVec2 = normalizeVector(normVec2)
            if normVec2 is None:
                isAligned = True
            else:
                isAligned = areVectorsAligned(normVec1, normVec2)

            if not isAligned: continue  # surface vectors must be aligned

            n2 = getNearestNodeForSurface(point2, surf2)

            # This is not very efficient but will suffice for now
            nodeStr = str(n1.asNode().getId()) + "-" + str(n2.asNode().getId())
            if nodeStr in existConnNodeIds: continue  # cannot connect fastener where one already exists

            fastenerLength = calcDistance(n1, n2)
            if fastenerLength > maxLength: maxLength = fastenerLength
            if fastenerLength < minLength: minLength = fastenerLength
            endPoints.append([n1, n2, point, point2, fastenerLength])

            existConnNodeIds.append(str(n1.asNode().getId()) + "-" + str(n2.asNode().getId()))
            existConnNodeIds.append(str(n2.asNode().getId()) + "-" + str(n1.asNode().getId()))

            break # If I am here, I have found a match and can continue with the next point

    if fastenerType == "Flexible Link":
        _connectorType = apex.attribute.ConnectorType.FlexibleLink
        connectorProps = flexibleLinkProps(fastenerDia, fastenerMatl)
    if fastenerType == "Rigid Link":
        _connectorType = apex.attribute.ConnectorType.RigidLink
        connectorProps = rigidLinkProps()

    _description = "created by auAutoConnect"
    numCreated = 0
    for data in endPoints:
        n1 = data[0]
        n2 = data[1]
        p1 = data[2]
        p2 = data[3]

        if fastenerType == "Bushing":
            _connectorType = apex.attribute.ConnectorType.Bushing
            fastenerLength = data[4]
            stiff = calcBushingStiffness(fastenerDia, fastenerLength, fastenerMatl)
            connectorProps = bushingProps(stiff[0], stiff[1], stiff[2], stiff[3], stiff[4], stiff[5])
            _description = "created by auAutoConnect, dia = %-6.3f, matl = %s, length = %-6.3f" % (fastenerDia, fastenerMatl.getName(), fastenerLength)

        _endPoint1 = apex.construct.createLocationByEntity(entity = n1)
        _endPoint2 = apex.construct.createLocationByEntity(entity = n2)
        _appMethod = apex.attribute.ApplicationMethod.Direct
        if connName == "":
            _name = p1.getParent().getName() + " - " + p2.getParent().getName()
        else:
            _name = connName
        c1 = apex.attribute.createConnector(
               name                = _name,
               connectorType       = _connectorType,
               connectorProperties = connectorProps,
               applicationMethod1  = _appMethod,
               applicationMethod2  = _appMethod,
               end1InterfacePoint  = _endPoint1,
               end2InterfacePoint  = _endPoint2,
               description         = _description)

        # Add user attributes to the connector to be used to fix a broken connection
        # A sister script uses this information to re-connect a broken connector
        c1.addUserAttribute(userAttributeName = "au_P2P", stringValue = "auAutoConnect")

        c1.addUserAttribute(userAttributeName = "end1_X", floatValue = p1.getX())
        c1.addUserAttribute(userAttributeName = "end1_Y", floatValue = p1.getY())
        c1.addUserAttribute(userAttributeName = "end1_Z", floatValue = p1.getZ())

        c1.addUserAttribute(userAttributeName = "end2_X", floatValue = p2.getX())
        c1.addUserAttribute(userAttributeName = "end2_Y", floatValue = p2.getY())
        c1.addUserAttribute(userAttributeName = "end2_Z", floatValue = p2.getZ())

        # Add this connector to the existing connector list
        existConnNodeIds.append(str(n1.asNode().getId()) + "-" + str(n2.asNode().getId()))
        existConnNodeIds.append(str(n2.asNode().getId()) + "-" + str(n1.asNode().getId()))

        numCreated += 1

    print("Connectors created: "+str(numCreated))
    print("Min fastener length = %6.3g" % minLength)
    print("Max fastener length = %6.3g" % maxLength)

    print("Normal exit ...")



