Hole and Pocket Recognition API Sample

Description

This sample script demonstrates three different methods for feature recognition: one for holes and two for pockets.

The script starts by creating a simple component which is then used to demonstrate the three methods. After the features are recognised they are coloured and milling and drilling operations are created for each feature.

RecognizedHoleGroup returns a list of BRepFaces that can be used as selections for the drilling operation. RecognizedPocket and PocketRecognitionSelection do not return BRepFaces, and their output needs additional processing before the output can be used for creating machining operations.

The sample script demonstrates a couple of different methods, including finding the pocket BRepFaces and creating sketches from the recognized pockets.

This script works only if the Manufacturing Extension is active.

Code Samples

import adsk.core, adsk.fusion, adsk.cam, traceback
import math
from enum import Enum

#################### Some constants and enumerators used in the script ####################
# Milling & drilling tool libraries to get tools from
MILLING_TOOL_LIBRARY_URL  = adsk.core.URL.create('systemlibraryroot://Samples/Milling Tools (Metric).json')
DRILLING_TOOL_LIBRARY_URL = adsk.core.URL.create('systemlibraryroot://Samples/Hole Making Tools (Metric).json')

# Colors
POCKET_WALL_FACES_COLOR   = adsk.core.Color.create(0, 255, 255, 255)
POCKET_BOTTOM_FACES_COLOR = adsk.core.Color.create(0, 145, 230, 255)
HOLE_SIMPLE_COLOR         = adsk.core.Color.create(130, 225, 10, 255)
HOLE_COUNTERBORE_COLOR    = adsk.core.Color.create(180, 120, 255, 255)

# BRep Search
PROXIMITY_TOLERANCE = -1 # define BRep Search proximity tolerance

# Some tool types used in this script (enumerator)
class ToolType(Enum):
    BULL_NOSE_END_MILL = 'bull nose end mill'
    DRILL              = 'drill'
    FLAT_END_MILL      = 'flat end mill'
    SPOT_DRILL         = 'spot drill'

# Some variables
_app = adsk.core.Application.get()
_ui  = _app.userInterface


#################### ENTRY POINT #####################
def run(context):
    try:       
        # create a new empty document
        doc = _app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)

        # switch to manufacturing space
        camWS = _ui.workspaces.itemById('CAMEnvironment') 
        camWS.activate()

        # get the CAM product
        products = doc.products
        cam: adsk.cam.CAM = products.itemByProductType("CAMProductType")

        # get the CAD product
        design: adsk.fusion.Design = products.itemByProductType('DesignProductType')

        # Get the root component of the active design as we will need to create sketches later on...
        rootComponent = design.rootComponent
        sketches = rootComponent.sketches

        #################### CREATE SAMPLE PART ####################

        body = createSampleBody(rootComponent)

        #################### TOOL LIBRARIES ####################

        # get the tool libraries from the library manager
        camManager = adsk.cam.CAMManager.get()
        libraryManager = camManager.libraryManager
        toolLibraries = libraryManager.toolLibraries
        
        # load tool libraries
        millingToolLibrary = toolLibraries.toolLibraryAtURL(MILLING_TOOL_LIBRARY_URL)
        drillingToolLibrary = toolLibraries.toolLibraryAtURL(DRILLING_TOOL_LIBRARY_URL)

        ####################### HOLE & POCKETS RECOGNITION - SAMPLE SCRIPT MAIN LOGIC STARTS #######################

        # create setups
        holeRecognitionSetup = createSetup('Holes (using "RecognizedHoleGroup")', body) 
        pocketRecognitionSetup = createSetup('Pockets (using "RecognizedPocket")', body)
        pocketSelectionSetup = createSetup('Pockets (using "PocketRecognitionSelection")', body)

        # get body parent component
        pocketComponent = body.parentComponent

        # get a tool to machine the pockets
        tools = getToolsFromLibraryByTypeDiameterRangeAndMinFluteLength(millingToolLibrary, ToolType.FLAT_END_MILL.value, 1, 1, 2)
        roughingTool = tools[0] # use the first tool found

        ########## MAKE POCKETS USING POCKET RECOGNITION API ##########

        # pocket search vector: only look for pockets from top view: visible and machinable from Z+
        pocketSearchVector = adsk.core.Vector3D.create(0, 0, -1)

        # recognize pockets
        pockets = adsk.cam.RecognizedPocket.recognizePockets(body, pocketSearchVector)

        # get boundary key points to find bottom faces (if any) and create roughing operation
        for pocket in pockets:
            # check if the pocket is circular (if it's basically a simple hole) since we will deal with those using drilling
            if isCircularPocket(pocket):
                continue

            # pocket boundaries
            boundaries = pocket.boundaries

            # get pocket boundary key points (used later on to find and color faces)
            points: list[adsk.core.Point3D] = []
            for boundary in boundaries:
                boundaryPoints = getBoundaryKeyPoints(boundary)
                points.extend(boundaryPoints)

            # create pocket operations
            if pocket.isThrough:
                # THROUGH POCKETS: searching side faces for coloring. Bottom faces are missing so we will draw sketch to define pocket bottom boundary.
                
                # search the walls face breps using the points of the boundary
                pocketWallFaces = getPocketWallFaces(pocketComponent, points)
                
                # color pocket walls
                colorFaces(design, pocketWallFaces, 'pocketWallFacesColor', POCKET_WALL_FACES_COLOR)

                # Create a sketch on XY plane since there are no bottom face to use here
                sketch = sketches.add(rootComponent.xYConstructionPlane)
                sketch.name = 'Through pocket sketch'
                drawSketchCurves(sketch, boundary)

                # create operation using that sketch
                op = createClosedThroughPocketOperation(pocketRecognitionSetup, 'Closed through pocket', sketch, pocket, roughingTool)
                
            else:
                # BLIND POCKETS: searching the pocket bottom face to use in the operation and side faces for coloring.

                # search the bottom face breps using the points of the boundary
                pocketBottomFaces = getPocketBottomFaces(pocketComponent, points)

                # search the walls face breps using the points of the boundary
                pocketWallFaces = getPocketWallFaces(pocketComponent, points)

                # color pocket bottom faces
                colorFaces(design, pocketBottomFaces, 'pocketBottomFaces', POCKET_BOTTOM_FACES_COLOR)

                # color pocket walls
                colorFaces(design, pocketWallFaces, 'pocketWallFacesColor', POCKET_WALL_FACES_COLOR)

                # define pocket name
                if pocket.isClosed: 
                    name = 'Closed blind pocket'
                else: 
                    name = 'Open blind pocket'

                op = createBlindPocketOperation(pocketRecognitionSetup, name, pocketBottomFaces, pocket, roughingTool)

            # Give control back to Fusion so it can update the graphics.
            adsk.doEvents()


        ########## MAKE POCKETS USING POCKET RECOGNITION SELECTION (FROM UI) ##########

        # create basic roughing operations
        # the distinction between the pockets is done by filtering the pocket heights, within the functions below...
        op = createClosedThroughPocketSelectionOperation(pocketSelectionSetup, 'Closed through pocket (PocketSelection)', roughingTool)
        op = createClosedBlindPocketSelectionOperation(pocketSelectionSetup, 'Closed blind pocket (PocketSelection)', roughingTool)
        

        ########## MAKE HOLES USING HOLE GROUP RECOGNITION API ##########

        # hole groups recognition using "adsk.cam.RecognizedHoleGroup()" is grouping holes of the same type  
        # you could also use "adsk.cam.RecognizedHole()": this will give you all holes but ungrouped...
        recognizedHolesInput = adsk.cam.RecognizedHolesInput.create()
        holeGroups = adsk.cam.RecognizedHoleGroup.recognizeHoleGroupsWithInput([body], recognizedHolesInput)
        
        # loop through the hole groups found
        for holeGroup in holeGroups:
            
            # analyse a hole from the group to understand their geometry (hole from a group are all the same)
            holeToCheck = holeGroup.item(0)
            
            # check the number of segment and the geometry that make the hole to identify the hole type
            if holeToCheck.segmentCount == 1:
                firstSegment = holeToCheck.segment(0)
                if firstSegment.holeSegmentType == adsk.cam.HoleSegmentType.HoleSegmentTypeCylinder:
                    # this is a simple hole made of one cylinder so let's drill that hole group
                    
                    # color faces
                    for hole in holeGroup:
                        simpleHoleFaces :list[adsk.fusion.BRepFace] = []
                        simpleHoleFaces.extend(hole.segment(0).faces)
                        colorFaces(design, simpleHoleFaces, 'simpleHoleColor', HOLE_SIMPLE_COLOR)

                    # tool
                    drillDiameter = firstSegment.bottomDiameter # check hole diameter to select the right drill
                    drillDepth = firstSegment.height + 0.5 # check the hole length to make sure the drill is long enough... and adding a bit of clearance of 5mm
                    drillTools = getToolsFromLibraryByTypeDiameterRangeAndMinFluteLength(drillingToolLibrary, ToolType.DRILL.value, drillDiameter, drillDiameter, drillDepth)
                    drillTool = drillTools[0] # pick first tool found
                    
                    # operation
                    createSimpleDrillOperation('Simple drill', holeRecognitionSetup, drillTool, holeGroup)

            elif holeToCheck.segmentCount == 4:
                # a hole with 4 segments might be a counterbore through model with a top chamfer... so let's check the geometry...
                firstSegment = holeToCheck.segment(0)
                secondSegment = holeToCheck.segment(1)
                thirdSegment = holeToCheck.segment(2)
                fourthSegment = holeToCheck.segment(3)

                if firstSegment.holeSegmentType == adsk.cam.HoleSegmentType.HoleSegmentTypeCone:
                    if secondSegment.holeSegmentType == adsk.cam.HoleSegmentType.HoleSegmentTypeCylinder:
                        if thirdSegment.holeSegmentType == adsk.cam.HoleSegmentType.HoleSegmentTypeFlat:
                            if fourthSegment.holeSegmentType == adsk.cam.HoleSegmentType.HoleSegmentTypeCylinder:
                                # a hole made by a cone, a cylinder, a flat and finally a cylinder is our definition of a coulterbore through model with a top chamfer, in this example...
                                # we will ignore other types of holes made by 4 segments here if any...

                                # color faces
                                for hole in holeGroup:
                                    couterboreHoleFaces :list[adsk.fusion.BRepFace] = []
                                    couterboreHoleFaces.extend(hole.segment(0).faces)
                                    couterboreHoleFaces.extend(hole.segment(1).faces)
                                    couterboreHoleFaces.extend(hole.segment(2).faces)
                                    couterboreHoleFaces.extend(hole.segment(3).faces)
                                    colorFaces(design, couterboreHoleFaces, 'counterboreHoleColor', HOLE_COUNTERBORE_COLOR)

                                # pick tool
                                drillDiameter = fourthSegment.bottomDiameter # check hole diameter to select the right drill
                                # check the hole length to make sure the drill is long enough... and adding a bit of clearance of 5mm
                                drillDepth = firstSegment.height + secondSegment.height + fourthSegment.height + 0.5 # no need to using the third segment height since it's a flat face
                                drillTools = getToolsFromLibraryByTypeDiameterRangeAndMinFluteLength(drillingToolLibrary, ToolType.DRILL.value, drillDiameter, drillDiameter, drillDepth)
                                drillTool = drillTools[0] # pick first tool found
                                
                                # drill
                                createCounterboreDrillOperation('Counterbore drill', holeRecognitionSetup, drillTool, holeGroup)

                                # check counterbopre diameter to select the right flat end mill
                                # we will drill the hole first meaning we don't need to take this into account...
                                # pick a flat end mill for the counterbore
                                minCounterboreToolDiameter = secondSegment.bottomDiameter - fourthSegment.bottomDiameter
                                maxCounterboreToolDiameter = secondSegment.bottomDiameter * 0.75
                                counterboreToolMinFluteLength =  firstSegment.height + secondSegment.height + 0.5 # adding 5mm clearance
                                counterboreTools = getToolsFromLibraryByTypeDiameterRangeAndMinFluteLength(millingToolLibrary, ToolType.FLAT_END_MILL.value, minCounterboreToolDiameter, maxCounterboreToolDiameter, counterboreToolMinFluteLength)
                                counterboreTool = counterboreTools[0] # pick first tool found
                                
                                # counterbore
                                createCounterboreMillOperation('Counterbore mill', holeRecognitionSetup, counterboreTool, holeGroup)

                                # select tools
                                # pick a spot drill tool for the top chamfer (we will roll over the chamfer so no need for a very large tool)
                                minChamferToolDiameter = 1
                                maxChamferToolDiameter = 1.4
                                chamferTools = getToolsFromLibraryByTypeDiameterRangeAndMinFluteLength(drillingToolLibrary, ToolType.SPOT_DRILL.value, minChamferToolDiameter, maxChamferToolDiameter)
                                chamferTool = chamferTools[0] # pick first tool found
                                # chamfer
                                createCounterboreChamferOperation('Counterbore chamfer', holeRecognitionSetup, chamferTool, holeGroup)

            # Give control back to Fusion so it can update the graphics.
            adsk.doEvents()

        # compute all
        cam.generateAllToolpaths(True)

    except:
        if _ui:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

####################### SAMPLE SCRIPT MAIN LOGIC ENDS #######################
#############################################################################


####################### TOOLS #######################

def getToolsFromLibraryByTypeDiameterRangeAndMinFluteLength(toolLibrary: adsk.cam.ToolLibrary, tooltype: str, minDiameter: float, maxDiameter: float, minimumFluteLength: float = None):
    ''' Return a list of tools that fits the search '''
    # set the search critera
    query = toolLibrary.createQuery()
    query.criteria.add('tool_type', adsk.core.ValueInput.createByString(tooltype))
    query.criteria.add('tool_diameter.min', adsk.core.ValueInput.createByReal(minDiameter))
    query.criteria.add('tool_diameter.max', adsk.core.ValueInput.createByReal(maxDiameter))
    if minimumFluteLength:
        query.criteria.add('tool_fluteLength.min', adsk.core.ValueInput.createByReal(minimumFluteLength))

    # get query results
    results = query.execute()

    # get the tools from the query
    tools: list[adsk.cam.Tool] = []
    for result in results:
        # a result has a tool, url, toolLibrary and the index of the tool in that library: we just return the tool here
        tools.append(result.tool)
    return tools


####################### SETUPS #######################

def createSetup(name: str, body: adsk.fusion.BRepBody): 
    ''' Create a setup '''
    app = adsk.core.Application.get()
    doc = app.activeDocument
    products = doc.products
    cam = adsk.cam.CAM.cast(products.itemByProductType("CAMProductType"))
    setups = cam.setups

    # create setup input and set parameters
    input = setups.createInput(adsk.cam.OperationTypes.MillingOperation)
    input.models = [body] 
    input.name = name  
    input.stockMode = adsk.cam.SetupStockModes.RelativeBoxStock
    input.parameters.itemByName('job_stockOffsetMode').expression = "'keep'"

    # create the setup
    setup = setups.add(input) 
    return setup


####################### POCKETS (USING API) #######################

def createClosedThroughPocketOperation(setup: adsk.cam.Setup, name: str, sketch: adsk.fusion.Sketch, pocket: adsk.cam.RecognizedPocket, tool: adsk.cam.Tool) -> adsk.cam.Operation:
    ''' Produce the toolpath for the closed through pocket using API '''
    input = setup.operations.createInput('adaptive2d')
    input.displayName = name
    input.tool = tool
    input.parameters.itemByName('doMultipleDepths').expression = 'true'
    pocketHeightIncludingBottomOffsetInMM = round((pocket.depth  * 10) + 2, 3)  # convert cm to mm and add 2 mm from bottomHeight_offset
    input.parameters.itemByName('maximumStepdown').expression = str(pocketHeightIncludingBottomOffsetInMM / 2) + ' mm' # divide total height by 2 to get 2 passes
    input.parameters.itemByName('topHeight_mode').expression = "'from contour'"
    input.parameters.itemByName('topHeight_offset').expression = str(pocket.depth * 10) + ' mm'
    input.parameters.itemByName('bottomHeight_offset').expression = str(-2) + 'mm' # set bottom to be 2 mm below pocket bottom

    # apply the shetch boundary to the operation input
    pocketSelection: adsk.cam.CadContours2dParameterValue = input.parameters.itemByName('pockets').value
    chains = pocketSelection.getCurveSelections()
    chain = chains.createNewSketchSelection()
    chain.inputGeometry = [sketch]
    chain.loopType = adsk.cam.LoopTypes.OnlyOutsideLoops
    chain.sideType = adsk.cam.SideTypes.AlwaysInsideSideType
    pocketSelection.applyCurveSelections(chains)

    # add to the setup
    op = setup.operations.add(input)   
    return op


def createBlindPocketOperation(setup: adsk.cam.Setup, name: str, pocketBottomFaces: list[adsk.fusion.BRepFace], pocket: adsk.cam.RecognizedPocket, tool: adsk.cam.Tool) -> adsk.cam.Operation:
    ''' Produce the toolpath for the closed blind pocket using API '''
    input = setup.operations.createInput('adaptive2d')
    input.displayName = name
    input.tool = tool
    input.parameters.itemByName('doMultipleDepths').expression = 'true'
    input.parameters.itemByName('maximumStepdown').expression = str(round(pocket.depth / 2, 3) * 10) + ' mm' # divide total height by 2 to get 2 passes
    input.parameters.itemByName('topHeight_mode').expression = "'from contour'"
    input.parameters.itemByName('topHeight_offset').expression = str(pocket.depth * 10) + ' mm'

    # add to the setup
    op = setup.operations.add(input)

    # apply the limits edge to the operation
    pocketSelection: adsk.cam.CadContours2dParameterValue = op.parameters.itemByName('pockets').value
    chains = pocketSelection.getCurveSelections()
    chain = chains.createNewPocketSelection()
    chain.inputGeometry = pocketBottomFaces
    pocketSelection.applyCurveSelections(chains) 

    return op


####################### POCKETS (USING UI) #######################

def createClosedThroughPocketSelectionOperation(setup: adsk.cam.Setup, name: str, tool: adsk.cam.Tool):
    ''' Produce the toolpath for the closed through pocket using UI '''
    input = setup.operations.createInput('adaptive2d')
    input.displayName = name
    input.tool = tool
    input.parameters.itemByName('doMultipleDepths').expression = 'true'
    input.parameters.itemByName('maximumStepdown').expression = '12 mm' # just dividiung the pocket height by 2 to have 2 steps
    input.parameters.itemByName('bottomHeight_offset').expression = str(-2) + ' mm' # bottom height = 2 mm below pocket bottom

    # add to the setup
    op = setup.operations.add(input)

    # apply the shetch boundary to the operation
    pocketSelection: adsk.cam.CadContours2dParameterValue = op.parameters.itemByName('pockets').value
    chains = pocketSelection.getCurveSelections()
    chain = chains.createNewPocketRecognitionSelection()
    chain.maximumPocketDepth = 2.5 # (cm) define some pocket recognition settings to filter pockets by height (measured from UI)
    chain.minimumPocketDepth = 1.5 # (cm)
    pocketSelection.applyCurveSelections(chains)

    return op


def createClosedBlindPocketSelectionOperation(setup: adsk.cam.Setup, name: str, tool: adsk.cam.Tool):
    ''' Produce the toolpath for the closed blind pocket using UI '''
    input = setup.operations.createInput('adaptive2d')
    input.displayName = name
    input.tool = tool
    input.parameters.itemByName('doMultipleDepths').expression = 'true'
    input.parameters.itemByName('maximumStepdown').expression = '6 mm'

    # add to the setup
    op = setup.operations.add(input)

    # apply the shetch boundary to the operation
    pocketSelection: adsk.cam.CadContours2dParameterValue = op.parameters.itemByName('pockets').value
    chains = pocketSelection.getCurveSelections()
    chain = chains.createNewPocketRecognitionSelection()
    chain.maximumPocketDepth = 1.5 # (cm) define some pocket recognition settings to filter pockets by height (measured from UI)
    chain.minimumPocketDepth = 0   # (cm)
    pocketSelection.applyCurveSelections(chains)

    return op


####################### POCKET HELPER FUNCTION #######################

def isCircularPocket(pocket: adsk.cam.RecognizedPocket) -> bool:
    ''' Returns true if this is a circular pocket (= a hole) made of boundaries with circular segments only '''
    isCircleFound = False
    boundaries = pocket.boundaries
    for i in range(len(boundaries)):
        boundary = boundaries[i]
        for j in range(boundary.count):
            segment = boundary.item(j)
            if segment.classType() == adsk.core.Circle3D.classType(): 
                isCircleFound = True
            else:
                return False
            
    return isCircleFound


def getBoundaryKeyPoints(boundary: list[adsk.core.Curve3DPath]) -> list[adsk.core.Point3D]:
    ''' Get some key points on the pocket boundary '''
    points: list[adsk.core.Point3D] = []
    for segment in boundary:
        # cast to get the actual segment type (casting returns None if the wrong object is passed)
        line3D = adsk.core.Line3D.cast(segment)
        arc3D = adsk.core.Arc3D.cast(segment)

        if line3D:
            points.append(line3D.startPoint)
            points.append(line3D.endPoint)
        elif arc3D:
            points.append(arc3D.startPoint)
            points.append(arc3D.endPoint)
        else:
            classType = segment.classType()
            raise Exception('Unsupported pocket curve type for this sample script: ' + classType)
    
    return points


def getPocketBottomFaces(component: adsk.fusion.Component, points: list[adsk.core.Point3D]) -> list[adsk.fusion.BRepFace]:
    ''' Search the pocket bottom faces using the provided points '''
    # search the bottom face breps using the provided points of the boundary
    pocketBottomFaces: list[adsk.fusion.BRepFace] = []

    # define the Z value of the pocket bottom using a recognized boundary point
    pocketBottomZ = points[0].z
    for point in points:
        breps = component.findBRepUsingPoint(point, adsk.fusion.BRepEntityTypes.BRepFaceEntityType, PROXIMITY_TOLERANCE, True)
        for brep in breps:
            # cast so we have a nice typed variable
            brep = adsk.fusion.BRepFace.cast(brep)

            # filter to only find the flat faces (not the pocket walls)
            if brep.boundingBox.minPoint.z == pocketBottomZ and brep.boundingBox.maxPoint.z == pocketBottomZ:
                if not brep in pocketBottomFaces:
                    pocketBottomFaces.append(brep)

    return pocketBottomFaces


def getPocketWallFaces(component: adsk.fusion.Component, points: list[adsk.core.Point3D]) -> list[adsk.fusion.BRepFace]:
    ''' Search the pocket wall faces using the provided points '''
    # search the walls face breps using the points of the boundary
    pocketWallFaces: list[adsk.fusion.BRepFace] = []

    # define the Z value of the pocket bottom using a recognized boundary point
    pocketBottomZ = points[0].z
    for point in points:
        breps = component.findBRepUsingPoint(point, adsk.fusion.BRepEntityTypes.BRepFaceEntityType, PROXIMITY_TOLERANCE, True)
        for brep in breps:
            # cast so we have a nice typed variable
            brep = adsk.fusion.BRepFace.cast(brep)

            # filter to only find the wall faces (not the pocket bottom faces)
            if brep.boundingBox.minPoint.z == pocketBottomZ and brep.boundingBox.maxPoint.z > pocketBottomZ:
                if not brep in pocketWallFaces:
                    pocketWallFaces.append(brep)

    return pocketWallFaces


####################### DRILLING #######################

def createSimpleDrillOperation(name: str, setup: adsk.cam.Setup, tool: adsk.cam.Tool, holeGroup: adsk.cam.RecognizedHoleGroup):
    ''' Create simple drilling operation '''
    input = setup.operations.createInput('drill')
    input.displayName = name
    input.tool = tool
    input.parameters.itemByName('drillTipThroughBottom').expression = 'true'
    input.parameters.itemByName('breakThroughDepth').expression = '2 mm'

    # select the holes faces to drill
    faces: list[adsk.fusion.BRepFace] = []
    for i in range(holeGroup.count):
        hole = holeGroup.item(i)
        firstSegment = hole.segment(0)
        faces.extend(firstSegment.faces)
    holeSelection: adsk.cam.CadObjectParameterValue = input.parameters.itemByName('holeFaces').value
    holeSelection.value = faces

    # add to setup
    setup.operations.add(input)


def createCounterboreDrillOperation(name: str, setup: adsk.cam.Setup, tool: adsk.cam.Tool, holeGroup: adsk.cam.RecognizedHoleGroup):
    ''' Create drilling operation for the counterbore hole '''
    input = setup.operations.createInput('drill')
    input.displayName = name
    input.tool = tool
    input.parameters.itemByName('drillTipThroughBottom').expression = 'true'
    input.parameters.itemByName('breakThroughDepth').expression = '2 mm'

    # select the holes faces to drill
    faces: list[adsk.fusion.BRepFace] = []
    for i in range(holeGroup.count):
        hole = holeGroup.item(i)
        firstSegment = hole.segment(0)
        secondSegment = hole.segment(1)
        fourthSegment = hole.segment(3)
        faces.extend(firstSegment.faces)
        faces.extend(secondSegment.faces)
        faces.extend(fourthSegment.faces)
    holeSelection: adsk.cam.CadObjectParameterValue = input.parameters.itemByName('holeFaces').value
    holeSelection.value = faces

    # add to setup
    setup.operations.add(input)


def createCounterboreMillOperation(name: str, setup: adsk.cam.Setup, tool: adsk.cam.Tool, holeGroup: adsk.cam.RecognizedHoleGroup):
    ''' Create milling operation for the counterbore part of the hole '''
    input = setup.operations.createInput('contour2d')
    input.displayName = name
    input.tool = tool
    input.parameters.itemByName('doLeadIn').expression = 'false'
    input.parameters.itemByName('doRamp').expression = 'true'
    input.parameters.itemByName('rampAngle').expression = '2 deg'
    input.parameters.itemByName('exit_verticalRadius').expression = '0 mm'
    input.parameters.itemByName('exit_radius').expression = '0 mm'

    # select the counterbore bottom edge to mill
    edges: list[adsk.fusion.BRepEdge] = []
    for i in range(holeGroup.count):
        hole = holeGroup.item(i)
        secondSegment = hole.segment(1) # counterbore segment
        edge = getHoleSegmentBottomEdge(secondSegment)
        edges.append(edge)
    holeSelection: adsk.cam.CadContours2dParameterValue = input.parameters.itemByName('contours').value
    chains = holeSelection.getCurveSelections()

    # each edge is a separate chain selection since it's own by separate holes
    for edge in edges:
        chain = chains.createNewChainSelection()
        chain.isReverted = True
        chain.inputGeometry = [edge]
    holeSelection.applyCurveSelections(chains) 

    # add to setup
    setup.operations.add(input)


def createCounterboreChamferOperation(name: str, setup: adsk.cam.Setup, tool: adsk.cam.Tool, holeGroup: adsk.cam.RecognizedHoleGroup):
    ''' Create an operation for the top chamfer of the counterbore '''
    input = setup.operations.createInput('chamfer2d')
    input.displayName = name
    input.tool = tool
    input.parameters.itemByName('chamferClearance').expression = '0 mm'
    input.parameters.itemByName('entry_distance').expression = '5 mm'
    input.parameters.itemByName('chamferTipOffset').expression = '1 mm'

    # select the counterbore chamfers bottom edge to mill
    edges: list[adsk.fusion.BRepEdge] = []
    for i in range(holeGroup.count):
        hole = holeGroup.item(i)
        firstSegment = hole.segment(0) # chamfer segment
        edge = getHoleSegmentBottomEdge(firstSegment)
        edges.append(edge)
    holeSelection: adsk.cam.CadContours2dParameterValue = input.parameters.itemByName('contours').value
    chains = holeSelection.getCurveSelections()

    for edge in edges:
        # each edge is a separate chain selection since it's own by separate holes
        chain = chains.createNewChainSelection()
        chain.isReverted = True
        chain.inputGeometry = [edge] 
    holeSelection.applyCurveSelections(chains) 

    # add to setup
    setup.operations.add(input)


def getHoleSegmentBottomEdge(segment: adsk.cam.RecognizedHoleSegment) -> adsk.fusion.BRepEdge:
    ''' Get the bottom edge of a given hole segment (assuming the hole is algned on Z+) '''
    # we assume:
    #  - the segment is made by one face
    #  - the hole is aligned with Z+ (bounding box checking)
    if len(segment.faces) != 1:
        raise Exception('A hole segment with a single face is expected!')

    face = adsk.fusion.BRepFace.cast(segment.faces[0]) 
    faceEdges = face.edges
    if len(faceEdges) != 2:
        raise Exception('A hole segment with a single face made of 2 edges is expected!')
    if faceEdges[0].boundingBox.maxPoint.z < faceEdges[1].boundingBox.maxPoint.z:
        edge = faceEdges[0]
    else:
        edge = faceEdges[1]

    return edge


####################### SKETCHING #######################

def drawSketchCurves(sketch: adsk.fusion.Sketch, boundary: list[adsk.core.Curve3D]):
    ''' Create a sketch from given pocket boundary or island '''
    for segment in boundary:
        # cast to get the actual segment type (casting returns None if the wrong object is passed)
        line3D = adsk.core.Line3D.cast(segment)
        arc3D = adsk.core.Arc3D.cast(segment)
        circle3D = adsk.core.Circle3D.cast(segment)

        if line3D:
            startPoint = line3D.startPoint
            endPoint = line3D.endPoint
            sketchLine(sketch, startPoint, endPoint)
        elif arc3D:
            startPoint = arc3D.startPoint
            centerPoint = arc3D.center
            sweepAngle = arc3D.endAngle
            normal = arc3D.normal
            sketchTwoPointArc(sketch, centerPoint, startPoint, sweepAngle, normal)           
        elif circle3D:
            centerPoint = circle3D.center
            radius = circle3D.radius
            sketchCircles(sketch, centerPoint, radius)
        else:
            classType = segment.classType()
            raise Exception('Unsupported pocket curve type for this sample script: ' + classType)

    mergeCoincidentPoints(sketch)

    # Give control back to Fusion so it can update the graphics.
    adsk.doEvents()


def mergeCoincidentPoints(sketch: adsk.fusion.Sketch):
    ''' Merge sketchpoints that are coincident. '''
    endPoints: list[adsk.fusion.SketchPoint] = []

    # Get the end points of all lines and arcs.
    for skLine in sketch.sketchCurves.sketchLines:
        endPoints.append(skLine.startSketchPoint)
        endPoints.append(skLine.endSketchPoint)

    for skArc in sketch.sketchCurves.sketchArcs:
        endPoints.append(skArc.startSketchPoint)
        endPoints.append(skArc.endSketchPoint)

    # Check if the points are at the same location and add a constraint.
    for i in range(len(endPoints)):
        point1 = endPoints[i]
        if not point1 is None:
            for j in range(i+ 1, len(endPoints)):
                point2 = endPoints[j]
                if not point2 is None:
                    if point1.geometry.isEqualTo(point2.geometry):
                        point1.merge(point2)
                        endPoints[i] = None
                        endPoints[j] = None


def sketchCircles(sketch: adsk.fusion.Sketch, centerPoint: adsk.core.Point3D, radius: float) -> adsk.fusion.SketchCircle:
    ''' Create a circle based on the points  '''
    circles = sketch.sketchCurves.sketchCircles
    circle = circles.addByCenterRadius(centerPoint, radius)
    return circle


def sketchTwoPointArc(sketch: adsk.fusion.Sketch, centerPoint: adsk.core.Point3D, startPoint: adsk.core.Point3D, sweepAngle: float, normal: adsk.core.Vector3D) -> adsk.fusion.SketchArc:
    ''' Sketch a arc based on center, radius and sweepangle '''
    arcs = sketch.sketchCurves.sketchArcs
    arc = arcs.addByCenterStartSweep(centerPoint, startPoint, sweepAngle)
    arcNormal = arc.geometry.normal
    # check whether the arc is drawn in the right direction
    if not arcNormal.z - normal.z < 0.000001 and arcNormal.y - normal.y < 0.000001 and arcNormal.x - normal.x < 0.000001:  
        arc.deleteMe()
        arc = arcs.addByCenterStartSweep(centerPoint, startPoint, -sweepAngle)
    return arc
        

def sketchLine(sketch: adsk.fusion.Sketch, startPoint: adsk.core.Point3D, endPoint: adsk.core.Point3D) -> adsk.fusion.SketchLine:
    ''' Sketch a straight line based on the starting and ending points '''
    lines = sketch.sketchCurves.sketchLines
    line = lines.addByTwoPoints(startPoint, endPoint)
    return line


####################### COLORING #######################

def colorFaces(design: adsk.fusion.Design, faces: list[adsk.fusion.BRepFace], colorName: str, color: adsk.core.Color):
    ''' Color given BRepFaces '''
    app = adsk.core.Application.get()

    # look for the color
    fusionMaterials = app.materialLibraries.itemByName('Fusion Appearance Library')
    newColor = design.appearances.itemByName(colorName)
    if not newColor:
        # Get the existing Red appearance.            
        redColor = fusionMaterials.appearances.itemByName('Paint - Enamel Glossy (Red)')
        # Copy it to the design, giving it a new name.
        newColor = design.appearances.addByCopy(redColor, colorName)                    
        # Change the color of the default appearance to the provided one.
        theColor: adsk.core.ColorProperty = newColor.appearanceProperties.itemByName('Color')
        theColor.value = color

    # color given faces
    for face in faces:
        face.appearance = newColor

    # Give control back to Fusion so it can update the graphics.
    adsk.doEvents()


####################### CREATE SAMPLE PART #######################

def createSampleBody(component: adsk.fusion.Component) -> adsk.fusion.BRepBody:
    ''' Create a sample part for the script '''
    # Get reference to the sketchs
    sketches = component.sketches

    # Get the extrude features Collection for the component
    extrudes = component.features.extrudeFeatures
    chamfers = component.features.chamferFeatures
    
    # create a cuiboid
    rectangle = sketches.add(component.xYConstructionPlane)
    rectangle.sketchCurves.sketchLines.addTwoPointRectangle(adsk.core.Point3D.create(0, 0, 0), adsk.core.Point3D.create(22.0, 15.0, 0))
    blockExtrude = createExtrudeFeature(extrudes, rectangle, 2, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)

    # create a simple hole
    holeOne = sketches.add(component.xYConstructionPlane)
    circleOne = [adsk.core.Circle3D.createByCenter(adsk.core.Point3D.create(3, 11.5, 2), adsk.core.Vector3D.create(0, 0, 1), 0.5)]
    drawSketchCurves(holeOne, circleOne)
    createExtrudeFeature(extrudes, holeOne, -2, adsk.fusion.FeatureOperations.CutFeatureOperation)

    # create another simple hole
    holeTwo = sketches.add(component.xYConstructionPlane)
    circleTwo = [adsk.core.Circle3D.createByCenter(adsk.core.Point3D.create(5, 11.5, 2), adsk.core.Vector3D.create(0, 0, 1), 0.5)]
    drawSketchCurves(holeTwo, circleTwo)
    createExtrudeFeature(extrudes, holeTwo, -2, adsk.fusion.FeatureOperations.CutFeatureOperation)

    # Give control back to Fusion so it can update the graphics.
    adsk.doEvents()

    # Create six counterbore holes
    sk = sketches.add(blockExtrude.endFaces[0])
    skPoints = adsk.core.ObjectCollection.create()
    for i in range(1, 3):
        for j in range(1, 4):
            pstn = adsk.core.Point3D.create(3 * j, 3 * i, 0)
            skPoint = sk.sketchPoints.add(pstn)
            skPoints.add(skPoint)

    holes = component.features.holeFeatures
    counterBoreDiam = adsk.core.ValueInput.createByReal(2)
    counterBoreDepth = adsk.core.ValueInput.createByReal(1)
    holeDiam = adsk.core.ValueInput.createByReal(1)
    holeInput = holes.createCounterboreInput(holeDiam, counterBoreDiam, counterBoreDepth)
    holeInput.setAllExtent(adsk.fusion.ExtentDirections.PositiveExtentDirection)
    holeInput.setPositionBySketchPoints(skPoints)
    holeFeature = holes.add(holeInput)

    # Give control back to Fusion so it can update the graphics.
    adsk.doEvents()

    # Find the top edges of the holes to add a chamfer.
    topFace = blockExtrude.endFaces[0]
    chamferEdges = adsk.core.ObjectCollection.create()
    for cylinderFace in holeFeature.faces:
        if isinstance(cylinderFace.geometry, adsk.core.Cylinder):
            commonEdge = findCommonEdge(cylinderFace, topFace)
            if commonEdge:
                chamferEdges.add(commonEdge)

    # create the chamfer
    chamferInput = chamfers.createInput2()
    offset = adsk.core.ValueInput.createByReal(0.3)
    chamferInput.chamferEdgeSets.addEqualDistanceChamferEdgeSet(chamferEdges, offset, False)
    chamfers.add(chamferInput)

    # Give control back to Fusion so it can update the graphics.
    adsk.doEvents()

    # create a closed pocket
    pocketOne =  sketches.add(component.xYConstructionPlane)
    geometries: list[adsk.core.Curve3D] = []
    geometries.append(adsk.core.Line3D.create(adsk.core.Point3D.create(12, 1.5, 2),adsk.core.Point3D.create(13, 1.5, 2)))
    geometries.append(adsk.core.Line3D.create(adsk.core.Point3D.create(14, 2.5, 2),adsk.core.Point3D.create(14, 4.5, 2)))
    geometries.append(adsk.core.Line3D.create(adsk.core.Point3D.create(13, 5.5, 2),adsk.core.Point3D.create(12, 5.5, 2)))
    geometries.append(adsk.core.Line3D.create(adsk.core.Point3D.create(11, 4.5, 2),adsk.core.Point3D.create(11, 2.5, 2)))
    geometries.append(adsk.core.Arc3D.createByCenter(adsk.core.Point3D.create(12, 2.5, 2), adsk.core.Vector3D.create(0, 0, 1), adsk.core.Vector3D.create(-1, 0, 0), 1, 0, math.pi / 2))
    geometries.append(adsk.core.Arc3D.createByCenter(adsk.core.Point3D.create(13, 2.5, 2), adsk.core.Vector3D.create(0, 0, 1), adsk.core.Vector3D.create(0, -1, 0), 1, 0, math.pi / 2))
    geometries.append(adsk.core.Arc3D.createByCenter(adsk.core.Point3D.create(13, 4.5, 2), adsk.core.Vector3D.create(0, 0, 1), adsk.core.Vector3D.create(1, 0, 0), 1, 0, math.pi / 2))
    geometries.append(adsk.core.Arc3D.createByCenter(adsk.core.Point3D.create(12, 4.5, 2), adsk.core.Vector3D.create(0, 0, 1), adsk.core.Vector3D.create(0, 1, 0), 1, 0, math.pi / 2))
    drawSketchCurves(pocketOne, geometries)
    createExtrudeFeature(extrudes, pocketOne, -1, adsk.fusion.FeatureOperations.CutFeatureOperation)

    # create a open pocket
    pocketTwo = sketches.add(component.xYConstructionPlane)
    pocketTwoOutline: list[adsk.core.Curve3D] = []
    pocketTwoOutline.append(adsk.core.Line3D.create(adsk.core.Point3D.create(12, 7.5, 2),adsk.core.Point3D.create(18, 7.5, 2)))
    pocketTwoOutline.append(adsk.core.Line3D.create(adsk.core.Point3D.create(19, 8.5, 2),adsk.core.Point3D.create(19, 11.5, 2)))
    pocketTwoOutline.append(adsk.core.Line3D.create(adsk.core.Point3D.create(18, 12.5, 2),adsk.core.Point3D.create(12, 12.5, 2)))
    pocketTwoOutline.append(adsk.core.Line3D.create(adsk.core.Point3D.create(11, 11.5, 2),adsk.core.Point3D.create(11, 8.5, 2)))
    pocketTwoOutline.append(adsk.core.Arc3D.createByCenter(adsk.core.Point3D.create(12, 8.5, 2), adsk.core.Vector3D.create(0, 0, 1), adsk.core.Vector3D.create(-1,0,0), 1, 0, math.pi / 2))
    pocketTwoOutline.append(adsk.core.Arc3D.createByCenter(adsk.core.Point3D.create(18, 8.5, 2), adsk.core.Vector3D.create(0, 0, 1), adsk.core.Vector3D.create(0,-1,0), 1, 0, math.pi / 2))
    pocketTwoOutline.append(adsk.core.Arc3D.createByCenter(adsk.core.Point3D.create(18, 11.5, 2), adsk.core.Vector3D.create(0, 0, 1), adsk.core.Vector3D.create(1,0,0), 1, 0, math.pi / 2))
    pocketTwoOutline.append(adsk.core.Arc3D.createByCenter(adsk.core.Point3D.create(12, 11.5, 2), adsk.core.Vector3D.create(0, 0, 1), adsk.core.Vector3D.create(0,1,0), 1, 0, math.pi / 2))
    drawSketchCurves(pocketTwo, pocketTwoOutline)
    createExtrudeFeature(extrudes, pocketTwo, -2, adsk.fusion.FeatureOperations.CutFeatureOperation)

    # create a pocket on the side
    pocketThree =  sketches.add(component.xYConstructionPlane)
    pocketThreeOutline: list[adsk.core.Curve3D] = []
    pocketThreeOutline.append(adsk.core.Line3D.create(adsk.core.Point3D.create(18, 0, 2),adsk.core.Point3D.create(22, 0, 2)))
    pocketThreeOutline.append(adsk.core.Line3D.create(adsk.core.Point3D.create(22, 0, 2),adsk.core.Point3D.create(22, 4.5, 2)))
    pocketThreeOutline.append(adsk.core.Line3D.create(adsk.core.Point3D.create(22, 4.5, 2),adsk.core.Point3D.create(19, 4.5, 2)))
    pocketThreeOutline.append(adsk.core.Line3D.create(adsk.core.Point3D.create(18, 3.5, 2),adsk.core.Point3D.create(18, 0, 2)))
    pocketThreeOutline.append(adsk.core.Arc3D.createByCenter(adsk.core.Point3D.create(19, 3.5, 2), adsk.core.Vector3D.create(0,0,1), adsk.core.Vector3D.create(0, 1, 0), 1, 0, math.pi / 2))
    drawSketchCurves(pocketThree, pocketThreeOutline)
    createExtrudeFeature(extrudes, pocketThree, -1.5, adsk.fusion.FeatureOperations.CutFeatureOperation)

    # Give control back to Fusion so it can update the graphics.
    adsk.doEvents()

    # return the created body
    part = component.bRepBodies.item(0)
    return part


def createExtrudeFeature(extrudeFeatures: adsk.fusion.ExtrudeFeatures, sketch: adsk.fusion.Sketch, height: float, operation: adsk.fusion.FeatureOperations) -> adsk.fusion.ExtrudeFeature:
    ''' Create an extrude feature '''
    # Get the profile defined by the circle
    shape = sketch.profiles.item(0)

    # Define that the extent is a distance extent of 1 cm
    distance = adsk.core.ValueInput.createByReal(height)

    # Create the extrusion
    return extrudeFeatures.addSimple(shape, distance, operation)


# Find the edge that connects to the input faces.
def findCommonEdge(face1: adsk.fusion.BRepFace, face2: adsk.fusion.BRepFace) -> adsk.fusion.BRepEdge:
    # Checks to see if any of the edges of face1 connect to face2.
    edge: adsk.fusion.BRepEdge = None
    for edge in face1.edges:
        for face in edge.faces:
            if face == face2:
                return edge
            
    return None