This script can be used to create joints between two parts that share coincident space.


1) Open a New Assembly workspace and insert two parts to be used for the Joint Creator script.  *Note these must be two separate parts and not two instances of the same part.

2) Use the Constraint tool to align the parts appropriately so that they occupy coincident space and are flush with one another in the area of the assembly where you want the joint creation to take place.  (See image above for reference)

3) Next, click on the Script tab on the Ribbon, and then click on Launch.

4) Paste the script found below into the New Script window.  Click "Save as" in the Script section of the Ribbon, and save the script into a folder location of your choice.

5) Next, save the attached "JointCreatorIcon.png" file into the same folder you saved the script into from step (4) above.

6) Click "Run" from the Script section of the Ribbon, and the Joint Creator dialog will appear.


7) To populate the Tab Part field of the Joint Creator, click on one of the part names in the Design Explorer.

8) Next, click on the Base Part filed of the Joint Creator dialog, and then click on the 2nd part in the Design Explorer to populate its field.

9) Lastly, click the Create Joint button to generate the joint.


# Joint Creator
# (c) Alibre, LLC 2019, All Rights Reserved
# Version 1.00

from __future__ import division
from math import *

# gets locaton of edge in a part coordinate system
# returns a list of two points defining the edge
def GetPartEdge(Prt, SharedEdge):
  Point1 = Prt.AssemblyPointtoPartPoint(SharedEdge[0])
  Point2 = Prt.AssemblyPointtoPartPoint(SharedEdge[1])
  return [Point1, Point2]

# compares two points [X1, Y1, Z1] and [X2, Y2, Z2]
# returns true if they are the same
def PointsAreEqual(P1, P2):
  if (round(P1[0], 6) == round(P2[0], 6) and
      round(P1[1], 6) == round(P2[1], 6) and
      round(P1[2], 6) == round(P2[2], 6)):
    return True
  else:
    return False

# gets part faces that use an edge
# returns a list of faces
def GetFacesFromEdge(Prt, SharedEdge):
  Faces = []
  PartEdge = GetPartEdge(Prt, SharedEdge)
  for Fce in Prt.Faces:
    for Edg in Fce.GetEdges():
      EdgeVertices = Edg.GetVertices()
      V1 = [EdgeVertices[0].X, EdgeVertices[0].Y, EdgeVertices[0].Z]
      V2 = [EdgeVertices[1].X, EdgeVertices[1].Y, EdgeVertices[1].Z]
      if ((PointsAreEqual(V1, PartEdge[0]) and PointsAreEqual(V2, PartEdge[1])) or
          (PointsAreEqual(V2, PartEdge[0]) and PointsAreEqual(V1, PartEdge[1]))):
         Faces.append(Fce)
  return Faces

# gets an edge that is shared between two parts
# returns list of edge vertices
def GetSharedEdge(Prt1, Prt2):  
  CornerVertices = []
  for TabVert in Prt1.GetAssemblyVertices():
    for BaseVert in Prt2.GetAssemblyVertices():
      if PointsAreEqual(TabVert, BaseVert):
        CornerVertices.append(TabVert)
  return CornerVertices
  
# gets the length of an edge
# returns edge length
def GetEdgeLength(Vert1, Vert2):
  a = abs(Vert2[0] - Vert1[0])
  b = abs(Vert2[1] - Vert1[1])
  c = abs(Vert2[2] - Vert1[2])
  return sqrt(a * a + b * b + c * c)

# gets the largest face from a set of faces
def GetLargestFace(Faces):
  if Faces[0].GetArea() > Faces[1].GetArea():
    return Faces[0]
  elif Faces[1].GetArea() > Faces[0].GetArea():
    return Faces[1]
  else:
    print "Unable to determine face of part."
    sys.exit()
    
# gets the smallest face from a set of faces
def GetSmallestFace(Faces):
  if Faces[0].GetArea() < Faces[1].GetArea():
    return Faces[0]
  elif Faces[1].GetArea() < Faces[0].GetArea():
    return Faces[1]
  else:
    print "Unable to determine face of part."
    sys.exit()

# generates a range of real values from start to stop
# incremented by step
def frange(start, stop, step):
  i = start
  if start < stop:
    while i < stop:
      yield i
      i += step
  else:
    while i > stop:
      yield i
      i += step

# gets the shortest edge of a face
# returns shortest edge
def GetShortestEdge(Fce):
  Shortest = Fce.GetEdges()[0]
  for E in Fce.GetEdges():
    if E.Length < Shortest.Length:
      Shortest = E
  return Shortest

# generates pin offsets
# NumPins = number of pins
# EdgeLength = length of edge for pins
# PinSense = True = slot at edge, False = pin at edge
# EdgeOffset = distance from ends of edges before pins
# Gap = distance between slot and pin
# returns: [ [Pin_1_Start, Pin_1_End], ..., [Pin_n_Start, Pin_n_End] ]
def GeneratePinOffsets(NumPins, EdgeLength, PinSense, EdgeOffset, Gap):
  Offsets = []
  
  # reduce length of edge by the edge offset at each end
  # giving a length that we generate pins and slots over
  PinEdgeLength = EdgeLength - (EdgeOffset * 2)
  
  # get length of each pin
  if PinSense == False:
    PinLength = PinEdgeLength / (NumPins + (NumPins - 1))
    PinState = True
  else:
    PinLength = PinEdgeLength / (NumPins + (NumPins + 1))
    PinState = False

  # generate start and end point of each pin
  CurrentPin = 0
  for Y in frange(EdgeOffset, EdgeLength - EdgeOffset, PinLength):
    if PinState:
      # if pins are never at the edges then always use gap on each
      # side of pin
      if PinSense == True:
        Offsets.append([Y - Gap, Y + PinLength + Gap])
      # pins could be at edges where we don't want the gap to be applied
      else:
        if CurrentPin == 0:
          # first pin, no gap at start
          Offsets.append([Y, Y + PinLength + Gap])
        elif CurrentPin == NumPins - 1:
          # last pin, no gap at end
          Offsets.append([Y - Gap, Y + PinLength])
        else:
          # middle pin, gap at start and end
          Offsets.append([Y - Gap, Y + PinLength + Gap])
      CurrentPin += 1
    PinState = not PinState

  return Offsets

# generates slot offsets
# NumPins = number of pins
# EdgeLength = length of edge for slots
# PinSense = True = slot at edge, False = pin at edge
# EdgeOffset = distance from ends of edges before pins
# Gap = distance between slot and pin
# returns: [ [Slot_1_Start, Slot_1_End], ..., [Slot_n_Start, Slot_n_End] ]
def GenerateSlotOffsets(NumPins, EdgeLength, PinSense, EdgeOffset, Gap):
  Offsets = []
  
  # reduce length of edge by the edge offset at each end
  # giving a length that we generate pins and slots over
  PinEdgeLength = EdgeLength - (EdgeOffset * 2)
  
  # get length of each pin
  if PinSense == False:
    PinLength = PinEdgeLength / (NumPins + (NumPins - 1))
    PinState = False
  else:
    PinLength = PinEdgeLength / (NumPins + (NumPins + 1))
    PinState = True

  if PinSense == True:
    NumSlots = NumPins + 1
  else:
    NumSlots = NumPins - 1

  # add initial slot for edge offset if pins are on outside of slots
  if EdgeOffset > 0 and PinSense == False:
    Offsets.append([0, EdgeOffset + (Gap * 2.0)])

  # generate start and end point of each slot
  CurrentSlot = 0
  for Y in frange(EdgeOffset, EdgeLength - EdgeOffset, PinLength):
    if PinState:
      # if slots are never at the edges then always use gap on each
      # side of slot
      if PinSense == False or (EdgeOffset > 0):
        Offsets.append([Y - Gap, Y + PinLength + Gap])
      # slots could be at edges where we don't want the gap to be applied
      else:
        if CurrentSlot == 0:
          # first slot, no gap at start
          Offsets.append([Y, Y + PinLength + Gap])
        elif CurrentSlot == NumSlots - 1:
          # last slot, no gap at end
          Offsets.append([Y - Gap, Y + PinLength])
        else:
          # middle pin, gap at start and end
          Offsets.append([Y - Gap, Y + PinLength + Gap])
      CurrentSlot += 1
    PinState = not PinState

  # add final slot for edge offset if pins are on outside of slots
  if EdgeOffset > 0 and PinSense == False:
    Offsets.append([EdgeLength - EdgeOffset - (Gap * 2.0), EdgeLength])

  if EdgeOffset > 0 and PinSense == True:
    # extend first slot to cover edge offset
    Offsets[0][0] = 0
    # extend last slot to cover edge offset
    Offsets[len(Offsets) - 1][1] = EdgeLength

  return Offsets

# generates the pins
# Prt = part to create pins on
# Fce = face on part to create pins
# PinOffsets = start and end values for pins
# Thickness = depth of pins
# SharedEdge = edge to generate pins along
def GeneratePins(Prt, Fce, PinOffsets, Thickness, SharedEdge):
  TabProfile = Prt.AddSketch('Pin Profile', Fce)

  TabEdge = GetPartEdge(Prt, SharedEdge)

  TabProfile.StartFaceMapping(TabEdge[0], TabEdge[1])
  
  for PinOffset in PinOffsets:
    TabProfile.AddRectangle(PinOffset[0], 0, PinOffset[1], Thickness, False)
  
  TabProfile.StopFaceMapping()
    
  # cut out rectangles (pins)
  Prt.AddExtrudeCut('Pins', TabProfile, 0, False, Part.EndCondition.ThroughAll, None, 0, Part.DirectionType.Normal, None, 0, False)

# generates the slots
# Prt = part to create slots on
# Fce = face on part to create slots
# SlotOffsets = start and end values for slots
# Thickness = depth of slots
# SharedEdge = edge to generate slots along
def GenerateSlots(Prt, Fce, SlotOffsets, Thickness, SharedEdge):
  BaseProfile = Prt.AddSketch('Slot Profile', Fce)

  BaseEdge = GetPartEdge(Prt, SharedEdge)
  
  BaseProfile.StartFaceMapping(BaseEdge[0], BaseEdge[1])
  
  for SlotOffset in SlotOffsets:
    BaseProfile.AddRectangle(SlotOffset[0], 0, SlotOffset[1], Thickness, False)
  
  BaseProfile.StopFaceMapping()
      
  # cut out rectangles (slots)
  Prt.AddExtrudeCut('Slots', BaseProfile, 0, False, Part.EndCondition.ThroughAll, None, 0, Part.DirectionType.Normal, None, 0, False)

# creates a joint based on user inputs
def CreateJoint(Values):
  TabPart      = Values[1]
  BasePart     = Values[2]
  NumberofPins = Values[3]
  PinSense     = Values[4]
  EdgeOffset   = Values[5]
  Gap          = Values[6]
  
  print "Gathering information..."

  # get edge shared by both parts
  SharedEdge = GetSharedEdge(TabPart, BasePart)
  # get the part faces for the shared edge
  TabFaces = GetFacesFromEdge(TabPart, SharedEdge)
  BaseFaces = GetFacesFromEdge(BasePart, SharedEdge)

  # get the largest faces on each part that use the shared edge
  TabFace = GetLargestFace(TabFaces)
  BaseFace = GetLargestFace(BaseFaces)

  # the smallest faces on each part that use the shared edge
  TabEndFace = GetSmallestFace(TabFaces)
  BaseEndFace = GetSmallestFace(BaseFaces)

  # get length of shared edge
  SharedEdgeLength = GetEdgeLength(SharedEdge[0], SharedEdge[1])

  # get thickness of each part
  TabThickness = GetShortestEdge(TabEndFace).Length
  BaseThickness = GetShortestEdge(BaseEndFace).Length

  print "Calculating..."

  # generate pin and slot offsets
  PinOffsets = GeneratePinOffsets(NumberofPins, SharedEdgeLength, PinSense, EdgeOffset, Gap / 2.0)
  SlotOffsets = GenerateSlotOffsets(NumberofPins, SharedEdgeLength, PinSense, EdgeOffset, Gap / 2.0)

  print "Generating..."

  # generate pins and slots
  GeneratePins(TabPart, TabFace, PinOffsets, BaseThickness, SharedEdge)
  GenerateSlots(BasePart, BaseFace, SlotOffsets, TabThickness, SharedEdge)

  print "Finished"

#################################################################################################

# check minimum requirements
if AlibreScriptVersion < 1110:
  sys.exit('Please upgrade your copy of Alibre Design to use this script')

ScriptName = 'Joint Creator'

Win = Windows()

# define options to show in dialog window
Options = []
Options.append([None, WindowsInputTypes.Image, 'JointCreatorIcon.png', 200])
Options.append(['Tab Part', WindowsInputTypes.Part, None])
Options.append(['Base Part', WindowsInputTypes.Part, None])
Options.append(['Number of Pins', WindowsInputTypes.Integer, 5])
Options.append(['Pins on Inside', WindowsInputTypes.Boolean, False])
Options.append(['Offset From Ends', WindowsInputTypes.Real, 0.0])
Options.append(['Gap Between Pins and Slots', WindowsInputTypes.Real, 0.0])

# show utility window
Win.UtilityDialog(ScriptName, 'Create Joint', CreateJoint, None, Options, 200)