Demonstrates how to automate the creation of an additive FFF manufacturing setup and generate a toolpath.
To run the sample script, have a design with one or more components open in Fusion’s DESIGN workspace. This script will switch the UI from the DESIGN workspace to the MANUFACTURE workspace, create a new Manufacturing Model, and create an Additive setup using that manufacturing model as an input.
The setup will select an FFF 3D printer from Fusion’s machine library and a print setting from the print setting library. All components in the Manufacturing model will be automatically oriented and arranged within the build area of the selected FFF machine. This script will also create support structures, if required, based on the orientation of each component. Finally, the script generates the toolpath for the active setup and lets the user choose if they wish to post process the resulting toolpath or if they want to simulate it.
import adsk.core, adsk.fusion, adsk.cam, traceback, tempfile, time from pathlib import Path app = adsk.core.Application.get() ui = app.userInterface def run(context): try: # Make sure the TEXT COMMAND palette is visible. textPalette = ui.palettes.itemById('TextCommands') if not textPalette.isVisible: textPalette.isVisible = True adsk.doEvents() doc = app.activeDocument products = doc.products # Make camWS = ui.workspaces.itemById('CAMEnvironment') camWS.activate() cam = adsk.cam.CAM.cast(products.itemByProductType("CAMProductType")) # Design creation designWS = ui.workspaces.itemById('FusionSolidEnvironment') designWS.activate() design = adsk.fusion.Design.cast(products.itemByProductType("DesignProductType")) camWS.activate() showMessage('=============================================') showMessage('Creating Manufacturing Model...') manufacturingModels = cam.manufacturingModels mmInput = manufacturingModels.createInput() mmInput.name = "My Manufacturing Model - FFF" manufacturingModel = manufacturingModels.add(mmInput) showMessage('Getting occurrences...') occs = getValidOccurrences(manufacturingModel.occurrence) if len(occs) == 0: ui.messageBox('No component has been added to the scene.') return showMessage('Creating arrange operation...') setup = createAdditiveSetup(occs, cam) # Define and create the arrange operation. operationInput = setup.operations.createInput('additive_arrange') arrange = setup.operations.add(operationInput) parameter: adsk.cam.StringParameterValue = arrange.parameters.itemByName("arrange_arrangement_type").value parameter.value = 'Pack2D' # Specify the values to control the arrangement. All length units are centimeters. parameter: adsk.cam.FloatParameterValue = arrange.parameters.itemByName("arrange_sweetspot_option").value parameter.value = 'arrange_sweetspot_center' parameter: adsk.cam.FloatParameterValue = arrange.parameters.itemByName("arrange_platform_clearance").value parameter.value = 0 parameter: adsk.cam.FloatParameterValue = arrange.parameters.itemByName("arrange_frame_width").value parameter.value = 0.5 parameter: adsk.cam.FloatParameterValue = arrange.parameters.itemByName("arrange_ceiling_clearance").value parameter.value = 0.5 parameter: adsk.cam.FloatParameterValue = arrange.parameters.itemByName("arrange_object_spacing").value parameter.value = 1 future = cam.generateToolpath(arrange) while (future.isGenerationCompleted == False): time.sleep(0.5) # Create the automatic orientation operations for each occurrence. for occ in occs: showMessage(f'Defining orientation for occurrence "{occ.name}" ...') operationInput = setup.operations.createInput('automatic_orientation') operationInput.isAutoCalculating = False orientationTarget = operationInput.parameters.itemByName('optimizeOrientationTarget') orientationTarget.value.value = [occ] operationInput.displayName = 'Automatic Orientation: ' + occ.name #global orientation orientation = setup.operations.add(operationInput) parameter: adsk.cam.FloatParameterValue = orientation.parameters.itemByName("optimizeOrientationSmallestRotation").value parameter.value = 180 #angle units are always degrees parameter: adsk.cam.BooleanParameterValue = orientation.parameters.itemByName("optimizeOrientationUsePreciseCalculation").value parameter.value = True #capitilize parameter: adsk.cam.FloatParameterValue = orientation.parameters.itemByName("optimizeOrientationCriticalAngle").value parameter.value = 45 #angle units are always degrees parameter: adsk.cam.FloatParameterValue = orientation.parameters.itemByName("optimizeOrientationDistanceToPlatform").value parameter.value = 0 #units are always cm parameter: adsk.cam.BooleanParameterValue = orientation.parameters.itemByName("optimizeOrientationMoveToCenter").value parameter.value = True #capitilize parameter: adsk.cam.FloatParameterValue = orientation.parameters.itemByName("optimizeOrientationFrameWidth").value parameter.value = 0.5 #units are always cm parameter: adsk.cam.FloatParameterValue = orientation.parameters.itemByName("optimizeOrientationCeilingClearance").value parameter.value = 0.5 #units are always cm parameter: adsk.cam.ChoiceParameterValue = orientation.parameters.itemByName("optimizeOrientationRankingSupportVolume").value parameter.value = '10' #take the number from dialog with its quotes parameter: adsk.cam.ChoiceParameterValue = orientation.parameters.itemByName("optimizeOrientationRankingSupportArea").value parameter.value = '0' #take the number from dialog with its quotes parameter: adsk.cam.ChoiceParameterValue = orientation.parameters.itemByName("optimizeOrientationRankingBoundingBoxVolume").value parameter.value = '2' #take the number from dialog with its quotes parameter: adsk.cam.ChoiceParameterValue = orientation.parameters.itemByName("optimizeOrientationRankingPartHeight").value parameter.value = '6' #take the number from dialog with its quotes parameter: adsk.cam.ChoiceParameterValue = orientation.parameters.itemByName("optimizeOrientationRankingCOGHeight").value parameter.value = '6' #take the number from dialog with its quotes showMessage('Generating orientation...') future = cam.generateToolpath(orientation) while (future.isGenerationCompleted == False): time.sleep(0.5) generatedResults = orientation.generatedDataCollection castPref = None firstResult = None primary = generatedResults.itemByIdentifier(adsk.cam.GeneratedDataType.OptimizedOrientationGeneratedDataType) if isinstance(primary, adsk.cam.OptimizedOrientationResults): castPref: adsk.cam.OptimizedOrientationResults = primary firstResult = castPref.item(0) castPref.currentOrientationResult = firstResult showMessage('Generating arrange...') future = cam.generateToolpath(arrange) while (future.isGenerationCompleted == False): time.sleep(0.5) showMessage('Generating supports...') supportInput = setup.operations.createInput('solid_volume_support') volumeSupport = setup.operations.add(supportInput) supportParam = volumeSupport.parameters.itemByName('supportTarget') supportParam.value.value = occs future = cam.generateToolpath(volumeSupport) while (future.isGenerationCompleted == False): time.sleep(0.5) if volumeSupport.hasError: volumeSupport.deleteMe() showMessage('Generating toolpath...') toolpath = None i = 0 for i in range(setup.operations.count): op = setup.operations.item(i) if (op.strategy == 'additive_buildstyle'): toolpath = op break if (toolpath == None): return future = cam.generateToolpath(toolpath) while (future.isGenerationCompleted == False): time.sleep(1.0) app.activeViewport.fit() res = ui.messageBox('Do you want to post the toolpath? Choosing "No" will display the simulation instead.', '', adsk.core.MessageBoxButtonTypes.YesNoButtonType, adsk.core.MessageBoxIconTypes.QuestionIconType) if res == adsk.core.DialogResults.DialogNo: # Start the toolpath simulation command. ui.commandDefinitions.itemById('IronAdditiveSimulation').execute() else: # Create the NC program ncprograms = cam.ncPrograms input = ncprograms.createInput() input.name = 'Prusa NCProgram' input.operations = [toolpath] ncprogram = ncprograms.add(input) params = ncprogram.parameters params.itemByName("nc_program_useMachineConfig").value.value = True params.itemByName("nc_program_openInEditor").value.value = False ncprogram.postConfiguration = getPostConfiguration() # Decide where to save the NC Program to # The default is to save it to disk fileDlg = ui.createFileDialog() fileDlg.title = 'Post Toolpath' fileDlg.filter = 'G-Code (*.gcode)' # Show file open dialog dlgResult = fileDlg.showSave() if dlgResult == adsk.core.DialogResults.DialogOK: path: Path = Path(fileDlg.filename) folderPath = str(path.parent.as_posix()) fileName = path.name.replace('.gcode', '') else: ui.messageBox('Post cancelled, showing simulation instead.') ui.commandDefinitions.itemById('IronAdditiveSimulation').execute() return params.itemByName('nc_program_output_folder').expression = "\'"+folderPath+"\'" params.itemByName('nc_program_filename').expression = "\'"+fileName+"\'" # Post the NC program postProcessOptions = adsk.cam.NCProgramPostProcessOptions.create() #----------- Post to Fusion Hub Sample ------------ # Users can also choose to post the toolpath to FusionHub # In this case a new project and folder will be created in the user's active hub if they don't exist yet. #params.itemByName("nc_program_postToFusionTeam").value.value = True # This is the default, a dialog is raised to ask to save the project, cancelling the dialog will post the program, but without linking it to the project. #postProcessOptions.fusionHubExecutionBehavior = adsk.cam.FusionHubExecutionBehaviors.FusionHubExecutionBehavior_ExportWithRelationship # Same as above, except that cancelling the dialog will not post the program. #postProcessOptions.fusionHubExecutionBehavior = adsk.cam.FusionHubExecutionBehaviors.FusionHubExecutionBehavior_ForceExportWithRelationship # Saves the project if it has not been saved yet and links it to the posted program. #postProcessOptions.fusionHubExecutionBehavior = adsk.cam.FusionHubExecutionBehaviors.FusionHubExecutionBehavior_SilentForceExportWithRelationship # Only uploads the post, without linking the project. #postProcessOptions.fusionHubExecutionBehavior = adsk.cam.FusionHubExecutionBehaviors.FusionHubExecutionBehavior_SkipRelationship # Set the Fusion Hub folder to upload the post to #data = app.data #hub = data.activeHub #folder = createHubFolder(hub) #ncprogram.fusionHubFolder = folder #------------------------------------------------- ncprogram.postProcess(postProcessOptions) showMessage('Finished.') except: if ui: ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) adsk.terminate() # Create an additive setup. def createAdditiveSetup(models: list[adsk.fusion.Occurrence], cam: adsk.cam.CAM): setups = cam.setups input = setups.createInput(adsk.cam.OperationTypes.AdditiveOperation) input.models = models input.name = 'AdditiveSetup' camManager = adsk.cam.CAMManager.get() libraryManager = camManager.libraryManager printSettingLibrary = libraryManager.printSettingLibrary machineLibrary = libraryManager.machineLibrary printSetting = None machine = None if True: # URL-structure browsing printSettingUrl = printSettingLibrary.urlByLocation(adsk.cam.LibraryLocations.Fusion360LibraryLocation) ## .Fusion360LibraryLocation vs .LocalLibraryLocation etc. printSettings = printSettingLibrary.childPrintSettings(printSettingUrl) machineUrl = machineLibrary.urlByLocation(adsk.cam.LibraryLocations.Fusion360LibraryLocation) ## .Fusion360LibraryLocation vs .LocalLibraryLocation etc. machines = machineLibrary.childMachines(machineUrl) for ps in printSettings: if ps.name == "PLA (Direct Drive)": #print setting name from fusions library printSetting = ps break for machine in machines: #model name from fusions library -- Example: "Generic FFF Machine" if machine.model == "i3 MK3S+": machine = machine break input.machine = machine input.printSetting= printSetting setup = setups.add(input) return setup def getPostConfiguration() -> adsk.cam.PostConfiguration: camManager = adsk.cam.CAMManager.get() libraryManager = camManager.libraryManager postConfigLibrary = libraryManager.postLibrary postConfigUrl = postConfigLibrary.urlByLocation(adsk.cam.LibraryLocations.Fusion360LibraryLocation) postConfigs = postConfigLibrary.childPostConfigurations(postConfigUrl) for config in postConfigs: if config.description == "Prusa": # Example: "Prusa MK3S+" return config return None def createHubFolder(hub:adsk.core.DataHub) -> adsk.core.DataFolder: # Create hub folder folder = None projectFound = None for project in hub.dataProjects: if project.name == "AdditiveFFFAutomationSamples": projectFound = project break # If the project is not found, create it if not projectFound: hub.dataProjects.add("AdditiveFFFAutomationSamples", "Post samples created by the Additive FFF Automation Samples script.") # And then create the folder we want to use as the post destination folder = projectFound.rootFolder.dataFolders.itemByName("AutomationSamples") if not folder: folder = projectFound.rootFolder.dataFolders.add("AutomationSamples") return folder # Given an occurrence, this finds all child occurrences that contain either a # B-Rep or Mesh body. It is recursive, so it will find all occurrences at all levels. def getValidOccurrences(occurrence: adsk.fusion.Occurrence) -> list[adsk.fusion.Occurrence]: result = [] for childOcc in occurrence.childOccurrences: if (childOcc.bRepBodies.count + childOcc.component.meshBodies.count > 0): result.append(childOcc) result.extend(getValidOccurrences(childOcc)) return result def showMessage(message): app.log(message) # Give control back to Fusion, so it can update the UI. adsk.doEvents()