This sample script starts by creating a simple component which is then used to describe a milling workflow. It creates a setup, a few operations, pick some tools from a Fusion sample tool library using loops and queries and ends up post-processing the operations out using an NC Program.
#include <Core/CoreAll.h> #include <Fusion/FusionAll.h> #include <Cam/CamAll.h> #include <algorithm> using namespace adsk::core; using namespace adsk::fusion; using namespace adsk::cam; // Constants used in the script // We assume we are cutting Aluminum here... // Milling tool library to get tools from const std::string MILLING_TOOL_LIBRARY = "Milling Tools (Metric)"; // Material properties for feed and speed calculation const int ALUMINUM_CUTTING_SPEED = 300; // mm/min const double ALUMINUM_FEED_PER_TOOTH = 0.1; // mm/tooth // Tool preset names (which we know exists for the selected tools) const std::string ALUMINUM_PRESET_ROUGHING = "alu* rou*"; const std::string ALUMINUM_PRESET_FINISHING = "Aluminum - Finishing"; // Tool types used in this script const std::string BULL_NOSE_END_MILL = "bull nose end mill"; const std::string BALL_END_MILL = "ball end mill"; const std::string FACE_MILL = "face mill"; // Setup work coordinate system (WCS) location const std::string TOP_CENTER = "top center"; const std::string TOP_XMIN_YMIN = "top 1"; const std::string TOP_XMAX_YMIN = "top 2"; const std::string TOP_XMIN_YMAX = "top 3"; const std::string TOP_XMAX_YMAX = "top 4"; const std::string TOP_SIDE_YMIN = "top side 1"; const std::string TOP_SIDE_XMAX = "top side 2"; const std::string TOP_SIDE_YMAX = "top side 3"; const std::string TOP_SIDE_XMIN = "top side 4"; const std::string CENTER = "center"; const std::string MIDDLE_XMIN_YMIN = "middle 1"; const std::string MIDDLE_XMAX_YMIN = "middle 2"; const std::string MIDDLE_XMIN_YMAX = "middle 3"; const std::string MIDDLE_XMAX_YMAX = "middle 4"; const std::string MIDDLE_SIDE_YMIN = "middle side 1"; const std::string MIDDLE_SIDE_XMAX = "middle side 2"; const std::string MIDDLE_SIDE_YMAX = "middle side 3"; const std::string MIDDLE_SIDE_XMIN = "middle side 4"; const std::string BOTTOM_CENTER = "bottom center"; const std::string BOTTOM_XMIN_YMIN = "bottom 1"; const std::string BOTTOM_XMAX_YMIN = "bottom 2"; const std::string BOTTOM_XMIN_YMAX = "bottom 3"; const std::string BOTTOM_XMAX_YMAX = "bottom 4"; const std::string BOTTOM_SIDE_YMIN = "bottom side 1"; const std::string BOTTOM_SIDE_XMAX = "bottom side 2"; const std::string BOTTOM_SIDE_YMAX = "bottom side 3"; const std::string BOTTOM_SIDE_XMIN = "bottom side 4"; const double PI = 3.14159265358979323846; std::vector<std::string> getLibrariesURLs(Ptr<ToolLibraries> libraries, Ptr<URL> url); std::vector<Ptr<Tool>> getToolsFromLibraryByTypeDiameterRangeAndMinFluteLength( Ptr<ToolLibrary> toolLibrary, std::string tooltype, double minDiameter, double maxDiameter, double minimumFluteLength = 0.0); Ptr<BRepBody> createSamplePart(Ptr<Design> design); extern "C" XI_EXPORT bool run(const char* context) { // Initialisation Ptr<Application> app = Application::get(); Ptr<UserInterface> ui = app->userInterface(); // Create a new empty document Ptr<Document> doc = app->documents()->add(DocumentTypes::FusionDesignDocumentType); // Get the design document used to create the sample part Ptr<Design> design = app->activeProduct(); // Switch to manufacturing space Ptr<Workspace> camWS = ui->workspaces()->itemById("CAMEnvironment"); camWS->activate(); // Get the CAM product Ptr<Products> products = doc->products(); // Create sample part Ptr<BRepBody> part = createSamplePart(design); // Select cutting tools // Get the tool libraries from the library manager Ptr<CAMManager> camManager = CAMManager::get(); Ptr<CAMLibraryManager> libraryManager = camManager->libraryManager(); Ptr<ToolLibraries> toolLibraries = libraryManager->toolLibraries(); Ptr<URL> url; const bool useHardCodedUrl = false; if (useHardCodedUrl) { // We can use a library URl directly if we know its address std::string libUrl = "systemlibraryroot://Samples/Milling Tools (Metric).json"; url = URL::create(libUrl); } else { // Or we can use the tool library objects fusion folder in the tool library Ptr<URL> fusionFolder = toolLibraries->urlByLocation(LibraryLocations::Fusion360LibraryLocation); std::vector<std::string> fusionLibs = getLibrariesURLs(toolLibraries, fusionFolder); // Search the required library url in the libraries for (std::string& libUrl : fusionLibs) { if (libUrl.find(MILLING_TOOL_LIBRARY) != std::string::npos) { url = URL::create(libUrl); break; } } } // Load the tool library Ptr<ToolLibrary> toolLibrary = toolLibraries->toolLibraryAtURL(url); if (!toolLibrary) { ui->messageBox("Failed to load tool library"); return false; } // Create variables to host the milling tools to be used in the operations Ptr<Tool> faceTool; Ptr<Tool> adaptiveTool; Ptr<Tool> finishingTool; // Search for the face mill and the bull nose using a loop for the roughing operations for (Ptr<Tool> tool : toolLibrary) { // Read the tool type Ptr<ChoiceParameterValue> toolTypeParameter = tool->parameters()->itemByName("tool_type")->value(); std::string toolType = toolTypeParameter->value(); if (toolType == FACE_MILL && !faceTool) { // Select the first face tool found faceTool = tool; } else if (toolType == BULL_NOSE_END_MILL && !adaptiveTool) { // Search the roughing tool // We look for a bull nose end mill tool larger or equal to 12mm but less than 14mm Ptr<FloatParameterValue> diameterParameter = tool->parameters()->itemByName("tool_diameter")->value(); double diameter = diameterParameter->value(); if (diameter >= 1.2 && diameter < 1.4) { adaptiveTool = tool; } } // Exit when the 2 tools are found if (faceTool && adaptiveTool) { break; } } if (!faceTool) { ui->messageBox("No face mill tool found"); return false; } if (!adaptiveTool) { ui->messageBox("No bull nose end mill tool found"); return false; } // Using a query, search for a ball end mill tool having diameter between 6 mm and 10 mm and a minimum flute length // of 20.001mm std::vector<Ptr<Tool>> finishingTools = getToolsFromLibraryByTypeDiameterRangeAndMinFluteLength(toolLibrary, BALL_END_MILL, 0.6, 1, 2.0001); // For this example, we select the first tool found as our finishing tool if (finishingTools.size() == 0) { ui->messageBox("No ball end mill tool found"); return false; } finishingTool = finishingTools[0]; // Create a setup Ptr<CAM> cam = products->itemByProductType("CAMProductType"); Ptr<Setups> setups = cam->setups(); Ptr<SetupInput> setupInput = setups->createInput(OperationTypes::MillingOperation); // Create a list for the models to add to the setup Input std::vector<Ptr<Base>> models; // Add the part to the model list models.push_back(part); // Pass the model list to the setup input setupInput->models(models); // Create the setup and set some properties Ptr<Setup> setup = setups->add(setupInput); setup->name("CAM Automation Script Sample"); setup->stockMode(SetupStockModes::RelativeBoxStock); // Set the offset mode setup->parameters()->itemByName("job_stockOffsetMode")->expression("\"simple\""); // Set the offset stock side setup->parameters()->itemByName("job_stockOffsetSides")->expression("0 mm"); // Set the offset stock top setup->parameters()->itemByName("job_stockOffsetTop")->expression("1 mm"); // Set the setup origin Ptr<ChoiceParameterValue> wcs_origin = setup->parameters()->itemByName("wcs_origin_boxPoint")->value(); wcs_origin->value(TOP_XMIN_YMIN); // Face operations // Calculate the feed and speed for the face operation Ptr<FloatParameterValue> toolDiameterParameter = faceTool->parameters()->itemByName("tool_diameter")->value(); double toolDiameter = toolDiameterParameter->value(); // cm Ptr<IntegerParameterValue> numberOfFlutesParameter = faceTool->parameters()->itemByName("tool_numberOfFlutes")->value(); int numberOfFlutes = numberOfFlutesParameter->value(); // int double spindleSpeed = ALUMINUM_CUTTING_SPEED / PI / (toolDiameter * 10) * 1000; // rpm double cuttingFeedrate = spindleSpeed * ALUMINUM_FEED_PER_TOOTH * numberOfFlutes; // mm/min // Create a preset with those calculated feeds Ptr<ToolPreset> facePreset = faceTool->presets()->add(); facePreset->name("Aluminum (set by script)"); Ptr<FloatParameterValue> tool_spindleSpeed = facePreset->parameters()->itemByName("tool_spindleSpeed")->value(); tool_spindleSpeed->value(int(spindleSpeed)); facePreset->parameters() ->itemByName("tool_feedCutting") ->expression(std::to_string(int(cuttingFeedrate)) + " mm/min"); // Create a face operation input Ptr<OperationInput> input = setup->operations()->createInput("face"); input->tool(faceTool); input->toolPreset(facePreset); // Assign created preset input->displayName("Face Operation"); input->parameters()->itemByName("tolerance")->expression("0.01 mm"); input->parameters()->itemByName("stepover")->expression("0.75 * tool_diameter"); input->parameters()->itemByName("direction")->expression("\"climb\""); // Determine the pass angle along the largest part dimension // Get the stock box dimensions in cm Ptr<FloatParameterValue> stockXParameter = setup->parameters()->itemByName("job_stockInfoDimensionX")->value(); double stockX = stockXParameter->value(); Ptr<FloatParameterValue> stockYParameter = setup->parameters()->itemByName("job_stockInfoDimensionY")->value(); double stockY = stockYParameter->value(); // Determine the pass angle to be along the largest length (X or Y) of the block if (stockX >= stockY) { input->parameters()->itemByName("passAngle")->expression("0 deg"); } else { input->parameters()->itemByName("passAngle")->expression("90 deg"); } // Add the operation to the setup Ptr<OperationBase> faceOp = setup->operations()->add(input); // Adaptive operations input = setup->operations()->createInput("adaptive"); input->tool(adaptiveTool); input->displayName("Adaptive Roughing"); input->parameters()->itemByName("tolerance")->expression("0.1 mm"); input->parameters()->itemByName("maximumStepdown")->expression("5 mm"); input->parameters()->itemByName("fineStepdown")->expression("0.25 * maximumStepdown"); input->parameters()->itemByName("flatAreaMachining")->expression("false"); // Look for a tool preset related to aluminum roughing std::vector<Ptr<ToolPreset>> presets = adaptiveTool->presets()->itemsByName(ALUMINUM_PRESET_ROUGHING); if (presets.size() > 0) { // We select and use the first preset found Ptr<ToolPreset> adaptivePreset = presets[0]; input->toolPreset(adaptivePreset); } // Add the operation to the setup Ptr<OperationBase> adaptiveOp = setup->operations()->add(input); // Finishing tool preset // Get a tool preset from the finishing tool Ptr<ToolPreset> finishingPreset; presets = finishingTool->presets()->itemsByName(ALUMINUM_PRESET_FINISHING); if (presets.size() > 0) { // Use the first aluminum finishing preset found finishingPreset = presets[0]; } // Parallel operations input = setup->operations()->createInput("parallel"); input->tool(finishingTool); input->displayName("Parallel Finishing"); input->parameters()->itemByName("tolerance")->expression("0.01 mm"); input->parameters()->itemByName("cuspHeightStepover")->expression("0.005 mm"); input->parameters()->itemByName("boundaryMode")->expression("\"selection\""); if (finishingPreset) { // Assign the finishig tool preset input->toolPreset(finishingPreset); } // Add the operation to the setup Ptr<OperationBase> parallelOp = setup->operations()->add(input); // Use a contour for the sake of demonstration Ptr<BRepEdge> limitEdge; for (Ptr<BRepEdge> e : part->edges()) { // This is the inner one: intersection of a plane and a sphere making up a circle if (e->geometry()->curveType() == Curve3DTypes::Circle3DCurveType) { limitEdge = e; break; } } if (limitEdge) { // Apply the limits edge to the operation Ptr<CadContours2dParameterValue> cadcontours2dParam = parallelOp->parameters()->itemByName("machiningBoundarySel")->value(); Ptr<CurveSelections> chains = cadcontours2dParam->getCurveSelections(); Ptr<ChainSelection> chain = chains->createNewChainSelection(); chain->inputGeometry({limitEdge}); cadcontours2dParam->applyCurveSelections(chains); } // Steep and shallow operations // Create a folder for finishing operations that require Machining Extension Ptr<OperationInput> operationInput = setup->operations()->createInput("folder"); operationInput->displayName("Machining Extension Required"); Ptr<CAMFolder> folder = setup->operations()->add(operationInput); // Create a steep and shallow operation in the folder input = setup->operations()->createInput("steep_and_shallow"); input->tool(finishingTool); input->displayName("Steep and Shallow Finishing"); input->parameters()->itemByName("tolerance")->expression("0.01 mm"); input->parameters()->itemByName("useAvoidFlats")->expression("true"); input->parameters()->itemByName("cuspHeightStepdown")->expression("0.005 mm"); input->parameters()->itemByName("cuspHeightStepover")->expression("cuspHeightStepdown"); input->parameters()->itemByName("spiral")->expression("true"); input->parameters()->itemByName("shallowSpiral")->expression("true"); input->parameters()->itemByName("offsetSmoothing")->expression("true"); if (finishingPreset) { // Assign the finishig tool preset input->toolPreset(finishingPreset); } // Add the operation to the folder Ptr<OperationBase> steepAndShallowOp = folder->operations()->add(input); // Check whether this toolpath is generatable ("steep_and_shallow" required the manufacturing extension) bool isSteepAndShallowGeneratable = false; for (Ptr<OperationStrategy> op : setup->operations()->compatibleStrategies()) { if (op->name() == "steep_and_shallow") { if (op->isGenerationAllowed()) { // isGenerationAllowed will be false if the extension is not active thus preventing the // steep_and_shallow operation isSteepAndShallowGeneratable = true; } break; } } // Generate operations // List the valid operations to generate Ptr<ObjectCollection> operations = ObjectCollection::create(); operations->add(faceOp); operations->add(adaptiveOp); operations->add(parallelOp); if (isSteepAndShallowGeneratable) { operations->add(steepAndShallowOp); } // Create progress bar Ptr<ProgressDialog> progressDialog = ui->createProgressDialog(); progressDialog->isCancelButtonShown(false); progressDialog->show("Generating operations->()..", "%p%", 0, 100); adsk::doEvents(); // Allow Fusion to update so that the progressDialog updates nicely // Generate the valid operations Ptr<GenerateToolpathFuture> gtf = cam->generateToolpath(operations); // Wait for the generation to be finished and update the progress bar while (!gtf->isGenerationCompleted()) { // Calculate progress and update the progress bar int total = gtf->numberOfOperations(); int completed = gtf->numberOfCompleted(); int progress = int(completed * 100 / total); progressDialog->progressValue(progress); adsk::doEvents(); // Allow Fusion to update so that the screen does not freeze } // Generation done progressDialog->progressValue(100); progressDialog->hide(); // NC Program and post-processing // Get the post library from the library manager Ptr<PostLibrary> postLibrary = libraryManager->postLibrary(); // Query the post library to get the postprocessor list Ptr<PostConfigurationQuery> postQuery = postLibrary->createQuery(LibraryLocations::Fusion360LibraryLocation); postQuery->vendor("Autodesk"); postQuery->capability(PostCapabilities::Milling); std::vector<Ptr<PostConfiguration>> postConfigs = postQuery->execute(); // Find the "XYZ" post in the post library and import it to the local library Ptr<URL> importedURL; for (Ptr<PostConfiguration> config : postConfigs) { if (config->description() == "XYZ") { Ptr<URL> _url = URL::create("user://"); importedURL = postLibrary->importPostConfiguration(config, _url, "NCProgramSamplePost.cps"); } } // Get the imported local post config Ptr<PostConfiguration> postConfig = postLibrary->postConfigurationAtURL(importedURL); // Create the NCProgramInput object Ptr<NCProgramInput> ncInput = cam->ncPrograms()->createInput(); ncInput->displayName("NC Program Sample"); // Change some nc program parameters... Ptr<CAMParameters> ncParameters = ncInput->parameters(); std::string programName = "NCProgramSample"; Ptr<StringParameterValue> nc_program_filename = ncParameters->itemByName("nc_program_filename")->value(); nc_program_filename->value(programName); Ptr<BooleanParameterValue> nc_program_openInEditor = ncParameters->itemByName("nc_program_openInEditor")->value(); nc_program_openInEditor->value(true); // Set the temporary directory as the output directory std::string outputFolder = cam->temporaryFolder(); Ptr<StringParameterValue> nc_program_output_folder = ncParameters->itemByName("nc_program_output_folder")->value(); nc_program_output_folder->value(outputFolder); // Select the operations to generate (we skip steep_and_shallow here) ncInput->operations({faceOp, adaptiveOp, parallelOp}); // Add a new ncprogram from the ncprogram input Ptr<NCProgram> newProgram = cam->ncPrograms()->add(ncInput); // Set the post processor newProgram->postConfiguration(postConfig); // Change some of the post parameters Ptr<CAMParameters> postParameters = newProgram->postParameters(); Ptr<FloatParameterValue> builtin_tolerance = postParameters->itemByName("builtin_tolerance")->value(); builtin_tolerance->value(0.02); // NcProgram parameter is passed unchanged to the postprocessor (it has no units) Ptr<FloatParameterValue> builtin_minimumChordLength = postParameters->itemByName("builtin_minimumChordLength")->value(); builtin_minimumChordLength->value(0.33); // NcProgram parameter is passed unchanged (it has no units) // Update/apply post parameters newProgram->updatePostParameters(postParameters); // Set post options, by default post process only valid operations containing toolpath data Ptr<NCProgramPostProcessOptions> postOptions = NCProgramPostProcessOptions::create(); // postOptions.PostProcessExecutionBehaviors = // adsk.cam.PostProcessExecutionBehaviors.PostProcessExecutionBehavior_PostAll // Post-process newProgram->postProcess(postOptions); // Show the output file ui->messageBox( "Post processing is complete. The results have been written to:\n" + outputFolder + "/" + programName + ".csv"); return true; } // Return the list of libraries' URLs for the specified libraries std::vector<std::string> getLibrariesURLs(Ptr<ToolLibraries> libraries, Ptr<URL> url) { std::vector<std::string> urls; std::vector<Ptr<URL>> libs = libraries->childAssetURLs(url); for (auto& elem : libs) { urls.push_back(elem->toString()); } for (Ptr<URL> folder : libraries->childFolderURLs(url)) { std::vector<std::string> folderUrls = getLibrariesURLs(libraries, folder); urls.insert(urls.end(), folderUrls.begin(), folderUrls.end()); } return urls; } // Return a list of tools that fits the search std::vector<Ptr<Tool>> getToolsFromLibraryByTypeDiameterRangeAndMinFluteLength( Ptr<ToolLibrary> toolLibrary, std::string tooltype, double minDiameter, double maxDiameter, double minimumFluteLength) { Ptr<ToolQuery> query = toolLibrary->createQuery(); // Set the search critera query->criteria()->add("tool_type", ValueInput::createByString(tooltype)); query->criteria()->add("tool_diameter.min", ValueInput::createByReal(minDiameter)); query->criteria()->add("tool_diameter.max", ValueInput::createByReal(maxDiameter)); if (minimumFluteLength) { query->criteria()->add("tool_fluteLength.min", ValueInput::createByReal(minimumFluteLength)); } // Get query results std::vector<Ptr<ToolQueryResult>> results = query->execute(); // Get the tools from the query std::vector<Ptr<Tool>> tools; // Results have a tool, url, toollibrary and the index of the tool in that library: we just return the tool here for (auto& elem : results) { tools.push_back(elem->tool()); } return tools; } // CAD creation functions Ptr<BRepBody> createBox(Ptr<Design> design, double sizeX, double sizeY, double sizeZ); Ptr<BRepBody> createSphere(Ptr<Design> design, double radius); Ptr<BRepBody> getBodyFromBooleanOperation(Ptr<Design> design, Ptr<BRepBody> body1, Ptr<BRepBody> body2); // Create the sample part for this script Ptr<BRepBody> createSamplePart(Ptr<Design> design) { // The sample part is a box containing a concave side generated by subtracting the intersection from a sphere. // The lower face of the box is at Z = 0 and we position the sphere above but intersecting with the box upper face. // Fusion's default behaviour is now to ground the first component of an assembly to the parent. // This can be overriden, see Preferences->General->Design->Assemblies->First component grounded to parent. // However if we make the first component the box, we can move the sphere without changing anything. Ptr<BRepBody> box = createBox(design, 22, 15, 5); Ptr<BRepBody> sphere = createSphere(design, 7.5); // Get the root component Ptr<Component> rootComp = design->rootComponent(); Ptr<Occurrences> occs = rootComp->occurrences(); // Get the second Occurrence(sphere) as the first occurrence(box) may be grounded to the parent Ptr<Occurrence> occ = occs->item(1); Ptr<Matrix3D> mat = occ->transform(); // Matrix translation, moving the sphere up by 10 units along Z axis Ptr<Vector3D> origin = Vector3D::create(0, 0, 10); mat->translation(origin); // Set the transform occ->transform(mat); // Snapshot - Determining the position is important!!! design->snapshots()->add(); // Cut the sphere / box intersection from the box to leave a concave face Ptr<BRepBody> part = getBodyFromBooleanOperation(design, box, sphere); return part; } // Create a sample box Ptr<BRepBody> createBox(Ptr<Design> design, double sizeX, double sizeY, double sizeZ) { Ptr<Component> component = design->rootComponent(); // Create a sketch Ptr<Sketches> sketches = component->sketches(); Ptr<Sketch> sketch = sketches->add(component->xYConstructionPlane()); Ptr<SketchLines> lines = sketch->sketchCurves()->sketchLines(); Ptr<SketchLineList> recLines = lines->addTwoPointRectangle( Point3D::create(-sizeX / 2, -sizeY / 2, 0), Point3D::create(sizeX / 2, sizeY / 2, 0)); Ptr<Profile> prof = sketch->profiles()->item(0); Ptr<ExtrudeFeatures> extrudes = component->features()->extrudeFeatures(); Ptr<ValueInput> distance = ValueInput::createByReal(sizeZ); Ptr<ExtrudeFeature> ext = extrudes->addSimple(prof, distance, FeatureOperations::NewComponentFeatureOperation); return ext->bodies()->item(0); } // Create a sample sphere Ptr<BRepBody> createSphere(Ptr<Design> design, double radius) { Ptr<Component> component = design->rootComponent(); // Create a new sketch on the xy plane Ptr<Sketches> sketches = component->sketches(); Ptr<ConstructionPlane> xyPlane = component->xYConstructionPlane(); Ptr<Sketch> sketch = sketches->add(component->xYConstructionPlane()); // Draw a circle Ptr<SketchCircles> circles = sketch->sketchCurves()->sketchCircles(); Ptr<SketchCircle> circle = circles->addByCenterRadius(Point3D::create(0, 0, 0), radius); // Draw a line to use as the axis of revolution Ptr<SketchLines> lines = sketch->sketchCurves()->sketchLines(); Ptr<SketchLine> axisLine = lines->addByTwoPoints(Point3D::create(-radius, 0, 0), Point3D::create(radius, 0, 0)); // Get the profile defined by half of the circle Ptr<Profile> prof = sketch->profiles()->item(0); // Create an revolution input to be able to define the input needed for a revolution // while specifying the profile and that a new component is to be created Ptr<RevolveFeatures> revolves = component->features()->revolveFeatures(); Ptr<RevolveFeatureInput> revInput = revolves->createInput(prof, axisLine, FeatureOperations::NewComponentFeatureOperation); // Define that the extent is an angle of 2*pi to get a sphere Ptr<ValueInput> angle = ValueInput::createByReal(2 * PI); revInput->setAngleExtent(false, angle); // Create the extrusion Ptr<RevolveFeature> ext = revolves->add(revInput); return ext->bodies()->item(0); } // Creates a boolean operation between two bodies Ptr<BRepBody> getBodyFromBooleanOperation(Ptr<Design> design, Ptr<BRepBody> body1, Ptr<BRepBody> body2) { Ptr<Component> model = design->activeComponent(); Ptr<Features> features = model->features(); Ptr<ObjectCollection> bodyCollection = ObjectCollection::create(); bodyCollection->add(body2); Ptr<CombineFeatures> combineFeatures = features->combineFeatures(); Ptr<CombineFeatureInput> combineFeatureInput = combineFeatures->createInput(body1, bodyCollection); combineFeatureInput->operation(FeatureOperations(1)); combineFeatureInput->isKeepToolBodies(false); combineFeatureInput->isNewComponent(false); Ptr<CombineFeature> returnValue = combineFeatures->add(combineFeatureInput); Ptr<BRepBody> part = returnValue->bodies()->item(0); return part; }
import math, os, traceback from enum import Enum import adsk.fusion, adsk.core, adsk.cam #################### Script Constants #################### # We assume we are cutting Aluminum here... # Milling tool library to get tools from MILLING_TOOL_LIBRARY = 'Milling Tools (Metric)' # Some material properties for feed and speed calculation ALUMINUM_CUTTING_SPEED = 300 # mm/min ALUMINUM_FEED_PER_TOOTH = 0.1 # mm/tooth # Some tool preset names, known to exist for the selected tools ALUMINUM_PRESET_ROUGHING = 'alu* rou*' ALUMINUM_PRESET_FINISHING = 'Aluminum - Finishing' #################### Useful Enumerators #################### # Some tool types used in this script (enumerator) class ToolType(Enum): BULL_NOSE_END_MILL = 'bull nose end mill' BALL_END_MILL = 'ball end mill' FACE_MILL = 'face mill' # Setup work coordinate system (WCS) location (enumerator) class SetupWCSPoint(Enum): TOP_CENTER = 'top center' TOP_XMIN_YMIN = 'top 1' TOP_XMAX_YMIN = 'top 2' TOP_XMIN_YMAX = 'top 3' TOP_XMAX_YMAX = 'top 4' TOP_SIDE_YMIN = 'top side 1' TOP_SIDE_XMAX = 'top side 2' TOP_SIDE_YMAX = 'top side 3' TOP_SIDE_XMIN = 'top side 4' CENTER = 'center' MIDDLE_XMIN_YMIN = 'middle 1' MIDDLE_XMAX_YMIN = 'middle 2' MIDDLE_XMIN_YMAX = 'middle 3' MIDDLE_XMAX_YMAX = 'middle 4' MIDDLE_SIDE_YMIN = 'middle side 1' MIDDLE_SIDE_XMAX = 'middle side 2' MIDDLE_SIDE_YMAX = 'middle side 3' MIDDLE_SIDE_XMIN = 'middle side 4' BOTTOM_CENTER = 'bottom center' BOTTOM_XMIN_YMIN = 'bottom 1' BOTTOM_XMAX_YMIN = 'bottom 2' BOTTOM_XMIN_YMAX = 'bottom 3' BOTTOM_XMAX_YMAX = 'bottom 4' BOTTOM_SIDE_YMIN = 'bottom side 1' BOTTOM_SIDE_XMAX = 'bottom side 2' BOTTOM_SIDE_YMAX = 'bottom side 3' BOTTOM_SIDE_XMIN = 'bottom side 4' #################### Script Entry Point #################### def run(context) -> None: ui = None try: #################### Initialisation ##################### app = adsk.core.Application.get() ui: adsk.core.UserInterface = app.userInterface # Create a new empty document doc: adsk.core.Document = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType) # Get the design document used to create the sample part design: adsk.core.Product = app.activeProduct # Switch to manufacturing space camWS: adsk.core.Workspace = ui.workspaces.itemById('CAMEnvironment') camWS.activate() # Get the CAM product products: adsk.core.Products = doc.products #################### Create Sample Part #################### part: adsk.fusion.BRepBody = createSamplePart(design) #################### Select Cutting Tools #################### # Get the tool libraries from the library manager camManager = adsk.cam.CAMManager.get() libraryManager: adsk.cam.CAMLibraryManager = camManager.libraryManager toolLibraries: adsk.cam.ToolLibraries = libraryManager.toolLibraries url: adsk.core.URL = None libUrl = None useHardCodedUrl: bool = False if useHardCodedUrl: # We could use a library URl directly if we know its address ... libUrl = 'systemlibraryroot://Samples/Milling Tools (Metric).json' url = adsk.core.URL.create(libUrl) else: # ... or we can use the tool library objects # Fusion folder in the tool library fusionFolder: adsk.core.URL = toolLibraries.urlByLocation(adsk.cam.LibraryLocations.Fusion360LibraryLocation) fusionLibs: list[str] = getLibrariesURLs(toolLibraries, fusionFolder) # Search the required library url in the libraries for libUrl in fusionLibs: if MILLING_TOOL_LIBRARY in libUrl: url = adsk.core.URL.create(libUrl) break # Load the tool library toolLibrary: adsk.cam.ToolLibrary = toolLibraries.toolLibraryAtURL(url) # Create some variables to host the milling tools which will be used in the operations faceTool: adsk.cam.Tool = None adaptiveTool: adsk.cam.Tool = None finishingTool: adsk.cam.Tool = None # For the roughing operations, search for the face mill and the bull nose using a loop for tool in toolLibrary: # Read the tool type toolType: adsk.cam.ToolType = tool.parameters.itemByName('tool_type').value.value # Select the first face tool found if toolType == ToolType.FACE_MILL.value and not faceTool: faceTool = tool # Search for the roughing tool elif toolType == ToolType.BULL_NOSE_END_MILL.value and not adaptiveTool: # Look for a bull nose end mill tool larger or equal to 12mm but less than 14mm diameter: float = tool.parameters.itemByName('tool_diameter').value.value if diameter >= 1.2 and diameter < 1.4: adaptiveTool = tool # Exit when the 2 tools are found if faceTool and adaptiveTool: break # Using a query, search for a ball end mill tool with diameter between 6 mm and 10 mm with a minimum flute length of 20.001mm finishingTools: list[adsk.cam.Tool] = getToolsFromLibraryByTypeDiameterRangeAndMinFluteLength(toolLibrary, ToolType.BALL_END_MILL.value, 0.6, 1, 2.0001) # For this example, we select the first tool found as our finishing tool finishingTool = finishingTools[0] #################### Create Setup #################### cam = adsk.cam.CAM.cast(products.itemByProductType("CAMProductType")) setups: adsk.cam.Setups = cam.setups setupInput: adsk.cam.SetupInput = setups.createInput(adsk.cam.OperationTypes.MillingOperation) # Create a list for the models to add to the setup Input models:list = [] # Add the part to the model list models.append(part) # Pass the model list to the setup input setupInput.models = models # Create the setup and set some properties setup: adsk.cam.Setup = setups.add(setupInput) setup.name = 'CAM Automation Script Sample' setup.stockMode = adsk.cam.SetupStockModes.RelativeBoxStock # Set the offset mode setup.parameters.itemByName('job_stockOffsetMode').expression = "'simple'" # Set offset stock side setup.parameters.itemByName('job_stockOffsetSides').expression = '0 mm' # Set offset stock top setup.parameters.itemByName('job_stockOffsetTop').expression = '1 mm' # Set setup origin setup.parameters.itemByName('wcs_origin_boxPoint').value.value = SetupWCSPoint.TOP_XMIN_YMIN.value #################### Face Operations #################### # Calculate feed and speed for the face operation toolDiameter: float = faceTool.parameters.itemByName('tool_diameter').value.value # cm numberOfFlutes = faceTool.parameters.itemByName('tool_numberOfFlutes').value.value # int spindleSpeed: float = ALUMINUM_CUTTING_SPEED / math.pi / (toolDiameter * 10) * 1000 # rpm cuttingFeedrate: float = spindleSpeed * ALUMINUM_FEED_PER_TOOTH * numberOfFlutes # mm/min # Create a preset with those calculated feeds facePreset: adsk.cam.ToolPreset = faceTool.presets.add() facePreset.name = 'Aluminum (set by script)' facePreset.parameters.itemByName('tool_spindleSpeed').value.value = int(spindleSpeed) facePreset.parameters.itemByName('tool_feedCutting').expression = str(int(cuttingFeedrate)) + ' mm/min' # Create a face operation input input: adsk.cam.OperationInput = setup.operations.createInput('face') input.tool = faceTool input.toolPreset = facePreset # Assign created preset input.displayName = 'Face Operation' input.parameters.itemByName('tolerance').expression = '0.01 mm' input.parameters.itemByName('stepover').expression = '0.75 * tool_diameter' input.parameters.itemByName('direction').expression = "'climb'" # Determine pass angle along largest part dimension # Get stock box dimensions in cm stockX: float = setup.parameters.itemByName('job_stockInfoDimensionX').value.value stockY: float = setup.parameters.itemByName('job_stockInfoDimensionY').value.value # Determine pass angle to be along largest length (X or Y) of the block if stockX >= stockY: input.parameters.itemByName('passAngle').expression = '0 deg' else: input.parameters.itemByName('passAngle').expression = '90 deg' # Add the operation to the setup faceOp: adsk.cam.OperationBase = setup.operations.add(input) #################### Adaptive Operations #################### input = setup.operations.createInput('adaptive') input.tool = adaptiveTool input.displayName = 'Adaptive Roughing' input.parameters.itemByName('tolerance').expression = '0.1 mm' input.parameters.itemByName('maximumStepdown').expression = '5 mm' input.parameters.itemByName('fineStepdown').expression = '0.25 * maximumStepdown' input.parameters.itemByName('flatAreaMachining').expression = 'false' # Check if there is a tool preset related to aluminum roughing presets: adsk.cam.ToolPresets = adaptiveTool.presets.itemsByName(ALUMINUM_PRESET_ROUGHING) if len(presets) > 0: # Select the first preset found adaptivePreset: adsk.cam.ToolPreset = presets[0] input.toolPreset = adaptivePreset # Add the operation to the setup adaptiveOp: adsk.cam.OperationBase = setup.operations.add(input) #################### Finishing Tool Preset #################### # Select a tool preset from the finishing tool finishingPreset: adsk.cam.ToolPreset = None presets = finishingTool.presets.itemsByName(ALUMINUM_PRESET_FINISHING) if len(presets) > 0: # Use the first aluminum finishing preset found finishingPreset = presets[0] #################### Parallel Operations #################### input = setup.operations.createInput('parallel') input.tool = finishingTool input.displayName = 'Parallel Finishing' input.parameters.itemByName('tolerance').expression = '0.01 mm' input.parameters.itemByName('cuspHeightStepover').expression = '0.005 mm' input.parameters.itemByName('boundaryMode').expression = "'selection'" if finishingPreset: # Assign the finishing tool preset input.toolPreset = finishingPreset # Add the operation to the setup parallelOp: adsk.cam.OperationBase = setup.operations.add(input) # Lets use a contour for the sake of demonstration limitEdge: adsk.fusion.BRepEdge = None for e in part.edges: # This is the inner one: intersection of a plane and a sphere making up a circle if e.geometry.curveType == adsk.core.Curve3DTypes.Circle3DCurveType: limitEdge = e break if limitEdge: # Apply the limiting edge to the operation cadcontours2dParam: adsk.cam.CadContours2dParameterValue = parallelOp.parameters.itemByName('machiningBoundarySel').value chains: adsk.cam.CurveSelections = cadcontours2dParam.getCurveSelections() chain: adsk.cam.ChainSelection = chains.createNewChainSelection() chain.inputGeometry = [limitEdge] cadcontours2dParam.applyCurveSelections(chains) #################### Steep And Shallow Operations #################### # Create folder for finishing operations that require Machining Extension operationInput: adsk.cam.OperationInput = setup.operations.createInput('folder') operationInput.displayName = 'Machining Extension Required' folder: adsk.cam.CAMFolder = setup.operations.add(operationInput) # Create steep and shallow operation in the folder input = setup.operations.createInput('steep_and_shallow') input.tool = finishingTool input.displayName = 'Steep and Shallow Finishing' input.parameters.itemByName('tolerance').expression = '0.01 mm' input.parameters.itemByName('useAvoidFlats').expression = 'true' input.parameters.itemByName('cuspHeightStepdown').expression = '0.005 mm' input.parameters.itemByName('cuspHeightStepover').expression = 'cuspHeightStepdown' input.parameters.itemByName('spiral').expression = 'true' input.parameters.itemByName('shallowSpiral').expression = 'true' input.parameters.itemByName('offsetSmoothing').expression = 'true' # Assign the finishing tool preset if finishingPreset: input.toolPreset = finishingPreset # Add the operation to the folder steepAndShallowOp: adsk.cam.OperationBase = folder.operations.add(input) # Check if this toolpath is generatable as "steep_and_shallow" requires the manufacturing extension isSteepAndShallowGeneratable: bool = False for op in setup.operations.compatibleStrategies: if op.name == 'steep_and_shallow': if op.isGenerationAllowed: # If isGenerationAllowed is False, the extension isn't active, preventing generation of the steep_and_shallow operation isSteepAndShallowGeneratable = True break #################### Generate Operations #################### # Include the valid operations to generate operations = adsk.core.ObjectCollection.create() operations.add(faceOp) operations.add(adaptiveOp) operations.add(parallelOp) if isSteepAndShallowGeneratable: operations.add(steepAndShallowOp) # create progress bar progressDialog: adsk.core.ProgressDialog = ui.createProgressDialog() progressDialog.isCancelButtonShown = False progressDialog.show('Generating operations...', '%p%', 0, 100) adsk.doEvents() # Allow Fusion to update so the progressDialog shows up nicely # Generate the valid operations gtf: adsk.cam.GenerateToolpathFuture = cam.generateToolpath(operations) # Wait for the generation to be finished and update progress bar while not gtf.isGenerationCompleted: # Calculate progress and update progress bar total = gtf.numberOfOperations completed = gtf.numberOfCompleted progress = int(completed * 100 / total) progressDialog.progressValue = progress adsk.doEvents() # Allow Fusion to update so the screen doesn't freeze # Generation done progressDialog.progressValue = 100 progressDialog.hide() #################### NC Program And Post-processing #################### # Get the post library from library manager postLibrary: adsk.cam.PostLibrary = libraryManager.postLibrary # Query post library to get postprocessor list postQuery: adsk.cam.PostConfigurationQuery = postLibrary.createQuery(adsk.cam.LibraryLocations.Fusion360LibraryLocation) postQuery.vendor = "Autodesk" postQuery.capability = adsk.cam.PostCapabilities.Milling postConfigs: list[adsk.cam.PostConfiguration] = postQuery.execute() # Find the "XYZ" post in the post library and import it to local library for config in postConfigs: if config.description == 'XYZ': url = adsk.core.URL.create("user://") importedURL: adsk.core.URL = postLibrary.importPostConfiguration(config, url, "NCProgramSamplePost.cps") # Get the imported local post config postConfig: adsk.cam.PostConfiguration = postLibrary.postConfigurationAtURL(importedURL) # Create NCProgramInput object ncInput: adsk.cam.NCProgramInput = cam.ncPrograms.createInput() ncInput.displayName = 'NC Program Sample' # Change some nc program parameters... ncParameters: adsk.cam.CAMParameters = ncInput.parameters ncParameters.itemByName('nc_program_filename').value.value = 'NCProgramSample' ncParameters.itemByName('nc_program_openInEditor').value.value = True # Set user desktop as output directory (Windows and Mac) # Make the path valid for Fusion by replacing \\ to / in the path desktopDirectory = os.path.expanduser("~/Desktop").replace('\\', '/') ncParameters.itemByName('nc_program_output_folder').value.value = desktopDirectory # Select the operations to generate (we skip steep_and_shallow here) ncInput.operations = [faceOp, adaptiveOp, parallelOp] # Add a new ncprogram from the ncprogram input newProgram: adsk.cam.NCProgram = cam.ncPrograms.add(ncInput) # Set post processor newProgram.postConfiguration = postConfig # Change some post parameter postParameters: adsk.cam.CAMParameters = newProgram.postParameters # NcProgram parameters are passed as theyare to the postprocessor (they have no units) postParameters.itemByName('builtin_tolerance').value.value = 0.01 postParameters.itemByName('builtin_minimumChordLength').value.value = 0.33 # Update/apply post parameters newProgram.updatePostParameters(postParameters) # Set post options, by default post process only valid operations containing toolpath data postOptions = adsk.cam.NCProgramPostProcessOptions.create() # Post-process newProgram.postProcess(postOptions) except: if ui: ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) #################### Functions To Make Our Life Easier #################### def getLibrariesURLs(libraries: adsk.cam.ToolLibraries, url: adsk.core.URL) -> list[str]: ''' Return the list of libraries' URLs for the specified libraries ''' urls: list[str] = [] libs: list[adsk.core.URL] = libraries.childAssetURLs(url) for lib in libs: urls.append(lib.toString()) for folder in libraries.childFolderURLs(url): urls = urls + getLibrariesURLs(libraries, folder) return urls def getToolsFromLibraryByTypeDiameterRangeAndMinFluteLength(toolLibrary: adsk.cam.ToolLibrary, tooltype: str, minDiameter: float, maxDiameter: float, minimumFluteLength: float = None) -> list[adsk.cam.Tool]: ''' Return a list of tools that satisfy the search ''' query: adsk.cam.ToolQuery = toolLibrary.createQuery() # Set the search critera 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: list[adsk.cam.ToolQueryResult] = query.execute() # Get the tools from the query tools: list[adsk.cam.Tool] = [] for result in results: # Results have 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 #################### CAD Creation #################### def createSamplePart(design: adsk.fusion.Design) -> adsk.fusion.BRepBody: """ Creates the sample part for this script """ # The sample part is a box containing a concave side generated by subtracting the intersection from a sphere. # The lower face of the box is at Z=0 and we position the sphere above but intersecting with the box upper face. # Fusion's default behaviour is now to ground the first component of an assembly to the parent. # This can be overriden, see Preferences ->General ->Design ->Assemblies ->First component grounded to parent # However if we make the first component the box, we can move the sphere without changing anything box: adsk.fusion.BRepBody = createBox(design, 22, 15, 5) # 1st component occurrence sphere: adsk.fusion.BRepBody = createSphere(design, 7.5) # 2nd component occurrence # Get the root component rootComp: adsk.fusion.Component = design.rootComponent occs: adsk.fusion.Occurrences = rootComp.occurrences # Get the second Occurrence (sphere) as the first occurrence (box) may be grounded to the parent occ: adsk.fusion.Occurrence = occs.item(1) mat: adsk.core.Matrix3D = occ.transform # Matrix translation, moving the sphere up by 10 units along Z axis mat.translation = adsk.core.Vector3D.create(0, 0, 10) # Set transform occ.transform = mat # Snapshot - Determining the position is important!!! design.snapshots.add() # Cut the sphere/box intersection from the box to leave a concave face part: adsk.fusion.BRepBody = getBodyFromBooleanOperation(design, box, sphere) return part def createBox(design: adsk.fusion.Design, sizeX: float, sizeY: float, sizeZ: float) -> adsk.fusion.BRepBody: ''' Creates a sample box''' component: adsk.fusion.Component = design.rootComponent # Create sketch sketches: adsk.fusion.Sketches = component.sketches sketch: adsk.fusion.Sketch = sketches.add(component.xYConstructionPlane) lines: adsk.fusion.SketchLines = sketch.sketchCurves.sketchLines lines.addTwoPointRectangle(adsk.core.Point3D.create(-sizeX / 2, -sizeY / 2, 0), adsk.core.Point3D.create(sizeX / 2, sizeY / 2, 0)) prof: adsk.fusion.Profile = sketch.profiles.item(0) extrudes: adsk.fusion.ExtrudeFeatures = component.features.extrudeFeatures distance = adsk.core.ValueInput.createByReal(sizeZ) ext: adsk.fusion.ExtrudeFeature = extrudes.addSimple(prof, distance, adsk.fusion.FeatureOperations.NewComponentFeatureOperation) return ext.bodies.item(0) def createSphere(design: adsk.fusion.Design, radius: float) -> adsk.fusion.BRepBody: ''' Creates a sample sphere ''' component: adsk.fusion.Component = design.rootComponent # Create a new sketch on the xy plane. sketches: adsk.fusion.Sketches = component.sketches xyPlane: adsk.fusion.ConstructionPlane = component.xYConstructionPlane sketch: adsk.fusion.Sketch = sketches.add(xyPlane) # Draw a circle. circles: adsk.fusion.SketchCircles = sketch.sketchCurves.sketchCircles circles.addByCenterRadius(adsk.core.Point3D.create(0, 0, 0), radius) # Draw a line to use as the axis of revolution. lines: adsk.fusion.SketchLines = sketch.sketchCurves.sketchLines axisLine: adsk.fusion.SketchLine = lines.addByTwoPoints(adsk.core.Point3D.create(-radius, 0, 0), adsk.core.Point3D.create(radius, 0, 0)) # Get the profile defined by half of the circle. prof: adsk.fusion.Profile = sketch.profiles.item(0) # Create an revolution input for a revolution while specifying the profile and that a new component is to be created revolves: adsk.fusion.RevolveFeatures = component.features.revolveFeatures revInput: adsk.fusion.RevolveFeatureInput = revolves.createInput(prof, axisLine, adsk.fusion.FeatureOperations.NewComponentFeatureOperation) # Define that the extent is an angle of 2*pi to get a sphere angle = adsk.core.ValueInput.createByReal(2*math.pi) revInput.setAngleExtent(False, angle) # Create the extrusion. ext: adsk.fusion.RevolveFeature = revolves.add(revInput) return ext.bodies.item(0) def getBodyFromBooleanOperation(design: adsk.fusion.Design, body1: adsk.fusion.BRepBody, body2: adsk.fusion.BRepBody) -> adsk.fusion.BRepBody: """ Creates a boolean operation between two bodies """ model: adsk.fusion.Component = design.activeComponent features: adsk.fusion.Features = model.features # Create a collection and add our sphere bodyCollection = adsk.core.ObjectCollection.create() bodyCollection.add(body2) # Create a CombineFeatureInput using our box (body1), sphere (bodyCollection) specifying a 'cut' operation combineFeatures: adsk.fusion.CombineFeatures = features.combineFeatures combineFeatureInput: adsk.fusion.CombineFeatureInput = combineFeatures.createInput(body1, bodyCollection) combineFeatureInput.operation = adsk.fusion.FeatureOperations.CutFeatureOperation combineFeatureInput.isKeepToolBodies = False combineFeatureInput.isNewComponent = False # Generate our combined feature returnValue: adsk.fusion.CombineFeature = combineFeatures.add(combineFeatureInput) part: adsk.fusion.BRepBody = returnValue.bodies[0] return part