Create Node Merge Object at a Symmetry Plane

Goal: Create a node merge object on a model with a symmetry plane.

Code:

# Purpose of the script: create a node merge object on models with a (plane) symmetry
# How to use: select a coordinate that defines a symmetry plane (origin axisX, axisY) 
#             then the script will create a node merge object with faces automatically selected

def GetSignedDistanceFromPointToPlane(point, planeOrigin, planeNormal):
    OP = [ planeOrigin[0]-point[0], planeOrigin[1]-point[1], planeOrigin[2]-point[2] ]
    cosOpNormal = OP[0]*planeNormal[0]+OP[1]*planeNormal[1]+OP[2]*planeNormal[2]
    normalNorm = abs(planeNormal[0]*planeNormal[0]+planeNormal[1]*planeNormal[1]+planeNormal[2]*planeNormal[2])
    dist = cosOpNormal / normalNorm
    return dist

def GetSelectedCoordinateSystem():
    if Tree.ActiveObjects.Count != 1
        return None        
    obj = Tree.FirstActiveObject
    if not obj.Path.StartsWith('/Project/Model/Coordinate Systems'):
        return None    
    return obj

def FindFacesClosestToPlane(planeOrigin, planeNormal, maximumDistanceToPlane):
    assembly = DataModel.GeoData.Assemblies[0]
    parts = assembly.Parts
    
    facesPlus = []
    facesMinus = []
    for part in parts:
        for body in part.Bodies:
            distBody = GetSignedDistanceFromPointToPlane(body.Centroid, planeOrigin, planeNormal)
            for face in body.Faces:
                distFace = GetSignedDistanceFromPointToPlane(face.Centroid, planeOrigin, planeNormal)
                if abs(distFace) <= maximumDistanceToPlane:
                    if distBody >= 0:
                        facesPlus.append(face)
                    else:
                        facesMinus.append(face)
    return (facesPlus, facesMinus)

def AddNodeMergeObject():
    meshEdits = DataModel.GetObjectsByType(DataModelObjectCategory.MeshEdit)
    if meshEdits.Count > 0:
        meshEdit = meshEdits[0]
    else:
        meshEdit = Model.AddMeshEdit()
        
    meshEdit.AddNodeMerge()
    nodeMergeObj = DataModel.GetObjectsByType(DataModelObjectCategory.NodeMerge)[0]
    return nodeMergeObj

def GetGeometryBoundingBoxLength():
    geom = Model.Geometry
    lengthQuantity = geom.LengthX*geom.LengthX + geom.LengthY*geom.LengthY + geom.LengthZ*geom.LengthZ
    return lengthQuantity.Value
    
def ShowError(errString):
    ExtAPI.Application.ScriptByName("jscript").ExecuteCommand("WBScript.Out('" + errString + "', 1)")
    
def CreateNodeMergeAtPlane():
    try:
        csObj = GetSelectedCoordinateSystem()
        if csObj is None:
            raise Exception("Select a coordinate system that defines the symmetry plane as (origin, AxiX, AxisY)")
        
        planeOrigin = csObj.Origin
        planeNormal = csObj.ZAxis
        maximumDistanceToPlane = GetGeometryBoundingBoxLength() * 1e-5
        facesColls = FindFacesClosestToPlane(planeOrigin, planeNormal, maximumDistanceToPlane)
        
        nodeMergeObj = AddNodeMergeObject()
        
        primarySel = ExtAPI.SelectionManager.CreateSelectionInfo(SelectionTypeEnum.GeometryEntities)
        primarySel.Entities = facesColls[0]
        secondarySel = ExtAPI.SelectionManager.CreateSelectionInfo(SelectionTypeEnum.GeometryEntities)
        secondarySel.Entities = facesColls[1]
        
        nodeMergeObj.MasterLocation = primarySel
        nodeMergeObj.SlaveLocation = secondarySel
    except Exception as ex:
        ShowError("Error: {0}".format(ex))