def __group_coaxial_cylinders(cylinders,
                              tol_ang=0.5,
                              tol_lin=0.1,
                              roundDigit=6):
    """According to cylinders' axis, categorize a list of cylinders into a dictionary by using its axis as a key.

    Arguments:
        cylinders {[TopoDS_Face,...]} -- list of TopoDS_Face
        tol_ang {float} -- [Unit - degree] the angle between these two axis below this value will be recognize as two parallel axis
        tol_lin

    Returns:
        {'string': [TopoDS_Face,...]} -- returns a dictionary, key is string of axis and location vector, value is list of TopoDS_Shape
    """
    logging.debug('Entering group_coaxial')
    tol_rad = radians(tol_ang)
    skipList = []
    cyl_ax_grp = {}
    for i in range(0, len(cylinders)):
        if i in skipList:
            continue

        cyl1 = BRepAdaptor_Surface(cylinders[i], True).Cylinder()
        axis1 = cyl1.Axis()
        location1 = cyl1.Location()
        axisDir1 = (round(axis1.Direction().X(), roundDigit),
                    round(axis1.Direction().Y(),
                          roundDigit), round(axis1.Direction().Z(),
                                             roundDigit))
        cylLoc1 = (round(location1.X(),
                         roundDigit), round(location1.Y(), roundDigit),
                   round(location1.Z(), roundDigit))
        key = (axisDir1, cylLoc1)

        if key not in cyl_ax_grp.keys():
            cyl_ax_grp[key] = [cylinders[i]]
        else:
            logging.warning('Error !!! Please check the logic again !')

        for j in range(i + 1, len(cylinders)):
            # logging.debug('i = %d, j = %d' % (i, j))

            if j in skipList:
                # logging.debug('skip !!')
                continue
            cyl2 = BRepAdaptor_Surface(cylinders[j]).Cylinder()
            axis2 = cyl2.Axis()
            if axis1.IsCoaxial(axis2, tol_rad, tol_lin) or axis1.IsCoaxial(
                    axis2.Reversed(), tol_rad, tol_lin):
                # logging.debug('Coaxial !!')
                cyl_ax_grp[key].append(cylinders[j])
                skipList.append(j)
    return cyl_ax_grp
def group_planes_by_axis(shape):
    """[summary]
    Extract all planes from shape, and group them according to their normal
    vector.

    Arguments:
        shape {TopoDS_Shape} -- [description]

    Returns:
        {dict} -- key: normal vector as string)
                  value: list of TopoDS_Shape(Plane)
    """
    if shape is None:
        logging.warn("Input shape is None")
        pln_dict = None
    else:
        planeList = RecognizeTopo(shape).planes()
        pln_dict = {}
        for pln in planeList:
            gp_pln = BRepAdaptor_Surface(pln).Plane()
            normal = gp_pln.Axis().Direction()
            key = '%.6f,%.6f,%.6f' % (round(normal.X(), 6), round(
                normal.Y(), 6), round(normal.Z(), 6))
            key = tuple([float(i) for i in key.split(',')])
            if key not in pln_dict.keys():
                pln_dict[key] = [pln]
            else:
                pln_dict[key].append(pln)
    return pln_dict
def group_cylinders_byAxisDir(cylinders,
                              anglTolDeg=5,
                              roundDigit=6,
                              groupParallelAx=True):
    logging.debug('Entering group_cylinders_byAxisDir')
    cyl_ax = {}
    for cylinder in cylinders:
        cyl = BRepAdaptor_Surface(cylinder, True).Cylinder()
        axis = cyl.Axis()
        key = (round(axis.Direction().X(),
                     roundDigit), round(axis.Direction().Y(), roundDigit),
               round(axis.Direction().Z(), roundDigit))
        if key in cyl_ax.keys():
            cyl_ax[key].append(cylinder)
        else:
            cyl_ax[key] = [cylinder]
    for key in cyl_ax.keys():
        listOflist = list(find_full_cylinder(cyl_ax[key]).values())
        cyl_ax[key] = [k for i in listOflist for k in i]

    if groupParallelAx:
        axisKeyList = list(cyl_ax.keys())
        combineList = []
        j = 0
        while len(axisKeyList) >= 1:
            ax1 = axisKeyList.pop(0)
            grp = [ax1]
            dir1 = gp_Dir(ax1[0], ax1[1], ax1[2])
            while j < len(axisKeyList):
                ax2 = axisKeyList[j]
                dir2 = gp_Dir(ax2[0], ax2[1], ax2[2])
                j += 1
                if dir1.IsParallel(dir2, radians(anglTolDeg)):
                    grp.append(ax2)
                    j -= 1
                    axisKeyList.pop(j)
            combineList.append(grp)
            j = 0
        cyl_grpByAx = {}
        for i in combineList:
            cyl_grpByAx[i[0]] = []
            for j in i:
                cyl_grpByAx[i[0]] += cyl_ax[j]
    else:
        cyl_grpByAx = cyl_ax

    return cyl_grpByAx
def group_cyl_byPln(solid, distTol=0.5, angTolDeg=5.0):
    cylinders = RecognizeTopo(solid).cylinders()
    cyl_dirGrp = group_cylinders_byAxisDir(cylinders,
                                           anglTolDeg=angTolDeg,
                                           groupParallelAx=True)
    cyl_dirGrpKeys = list(cyl_dirGrp.keys())

    # group cylinders by plane
    CylinderGrpsFromDiffPln = {}
    j = 0
    for dirKey in cyl_dirGrpKeys:
        CylinderGrpsFromDiffPln[dirKey] = []
        parallel_cyl_grps = cyl_dirGrp[dirKey].copy()
        grp_cylinderOnSamePln = {}
        while len(parallel_cyl_grps) >= 1:
            # take out the first element for creating plane, and search the other cylinders on the same plane
            firstCylinder = parallel_cyl_grps.pop(0)
            # fitting a geomety surface to TopoDS_Surface
            brepCylinder = BRepAdaptor_Surface(firstCylinder).Cylinder()
            dir1 = brepCylinder.Axis().Direction()
            # location of cylinder is usually the location of local coordinate system, which is on the axis of cylinder
            loc1 = brepCylinder.Location()
            # create gp_Pln
            pln = gp_Pln(loc1, dir1)
            grp_cylinderOnSamePln[pln] = [firstCylinder]
            # Search the cylinders on the same pln, if yes, extract from parallel_cyl_grps
            # loop all elements in parallel_cyl_grps
            while j < len(parallel_cyl_grps):
                brepCylinder2 = BRepAdaptor_Surface(
                    parallel_cyl_grps[j]).Cylinder()
                loc2 = brepCylinder2.Location()
                if pln.Distance(gp_Pnt(loc2.X(), loc2.Y(),
                                       loc2.Z())) < distTol:
                    grp_cylinderOnSamePln[pln].append(parallel_cyl_grps[j])
                    parallel_cyl_grps.pop(j)
                else:
                    j += 1
            j = 0
        CylinderGrpsFromDiffPln[dirKey] = grp_cylinderOnSamePln
    return CylinderGrpsFromDiffPln