2224 lines
81 KiB
Python
2224 lines
81 KiB
Python
"""
|
|
Utility functions for the CAM function calling tool implementations.
|
|
"""
|
|
|
|
import re
|
|
import time
|
|
import unicodedata
|
|
|
|
from collections.abc import Callable
|
|
from enum import Flag, StrEnum, auto
|
|
from typing import Any
|
|
|
|
import adsk.cam
|
|
import adsk.core
|
|
import adsk.fusion
|
|
from CAMFunctionContext import CAMFunctionContext
|
|
from CAMFunctionTasks import CAMFunctionTaskContext
|
|
from ToolDefinition import (
|
|
TOOL_LIBRARY_SEARCH_PRIORITY,
|
|
ToolDefinition,
|
|
ToolLibraryDefinition,
|
|
ToolLibraryType,
|
|
)
|
|
|
|
|
|
class MissingObjectReason(StrEnum):
|
|
NONE = auto()
|
|
NO_MATCHING_NAME = auto()
|
|
NO_MATCHING_REGEX = auto()
|
|
NO_OBJECTS = auto()
|
|
|
|
|
|
class HoleAttributes(Flag):
|
|
SIMPLE = auto()
|
|
COUNTERBORE = auto()
|
|
THROUGH = auto()
|
|
BLIND = auto()
|
|
CHAMFER = auto()
|
|
|
|
|
|
# This enum should be in ModelInput.py, but is needed in multiple files
|
|
class ModelInputMode(StrEnum):
|
|
"""
|
|
Enum representing different modes for model input.
|
|
"""
|
|
|
|
NONE = "none"
|
|
ALL = "all"
|
|
NAMED = "named"
|
|
MANUFACTURING_MODEL = "manufacturing_model"
|
|
ACTIVE_SELECTION = "active_selection"
|
|
|
|
|
|
class CAMObjectType(StrEnum):
|
|
"""
|
|
Enum representing different types of manufacturing objects.
|
|
"""
|
|
|
|
SETUP = "setup"
|
|
OPERATION = "operation"
|
|
FOLDER = "folder"
|
|
PATTERN = "pattern"
|
|
MANUFACTURING_MODEL = "manufacturingModel"
|
|
ANY = "any"
|
|
|
|
def includesType(self, objectType) -> bool:
|
|
"""
|
|
Check if this object type includes the given object type.
|
|
ANY will also be included in the check.
|
|
"""
|
|
return self == objectType or self == CAMObjectType.ANY
|
|
|
|
def canBeUnderSetup(self) -> bool:
|
|
"""
|
|
Check if this object type is a type that can be located under a setup.
|
|
"""
|
|
match self:
|
|
case (
|
|
CAMObjectType.OPERATION
|
|
| CAMObjectType.FOLDER
|
|
| CAMObjectType.PATTERN
|
|
| CAMObjectType.ANY
|
|
):
|
|
return True
|
|
case CAMObjectType.SETUP | CAMObjectType.MANUFACTURING_MODEL:
|
|
return False
|
|
case _:
|
|
return False
|
|
|
|
|
|
# Boilerplate initialization
|
|
def init():
|
|
"""
|
|
Boilerplate code for CAM functions to retrieve the application, document,
|
|
and CAM workspace.
|
|
"""
|
|
|
|
app = adsk.core.Application.get()
|
|
ui = app.userInterface
|
|
|
|
# Use active document
|
|
doc = app.activeDocument
|
|
|
|
# Get the CAM product
|
|
products = doc.products
|
|
cam = adsk.cam.CAM.cast(products.itemByProductType("CAMProductType"))
|
|
|
|
return app, ui, doc, products, cam
|
|
|
|
|
|
def designProduct() -> adsk.fusion.Design:
|
|
"""Return the Design Product (workspace) of the active document."""
|
|
_, _, _, products, _ = init()
|
|
design = products.itemByProductType("DesignProductType")
|
|
return adsk.fusion.Design.cast(design)
|
|
|
|
|
|
# String helpers
|
|
def isNonEmptyString(string: str | None) -> bool:
|
|
"""Check if the given string is a valid string (not None and not empty)."""
|
|
return string is not None and string != ""
|
|
|
|
|
|
def getActionStringFromMode(mode: str) -> str:
|
|
"""
|
|
Get the string of the operation mode ("x", "u" or "d").
|
|
"""
|
|
function_type_map = {"x": "creating", "u": "updating", "d": "deleting"}
|
|
return function_type_map.get(mode, "unknown")
|
|
|
|
|
|
def getOperationTypeString(operationType: adsk.cam.OperationTypes) -> str:
|
|
"""
|
|
Get the string of the operation type.
|
|
"""
|
|
operation_type_map = {
|
|
adsk.cam.OperationTypes.MillingOperation: "milling",
|
|
adsk.cam.OperationTypes.TurningOperation: "turning",
|
|
adsk.cam.OperationTypes.JetOperation: "jet",
|
|
adsk.cam.OperationTypes.AdditiveOperation: "additive",
|
|
}
|
|
return operation_type_map.get(operationType, "Unknown")
|
|
|
|
|
|
def getListAsString(list: list | None) -> str:
|
|
"""
|
|
Get the string of a list of names from a list of strings.
|
|
e.g. ["op1", "op2", "op3"] -> "'op1', 'op2', 'op3'"
|
|
"""
|
|
if list is None or len(list) == 0:
|
|
return ""
|
|
return ", ".join([f"'{item}'" for item in list])
|
|
|
|
|
|
def extractNameAndRegexEnabled(nameArg: dict | str | None) -> tuple[str, bool]:
|
|
"""
|
|
Extract the name string and regex_enabled flag from the given argument.
|
|
If the argument is None, return an empty string and False.
|
|
If the argument is a string, return it and False.
|
|
If the argument is a dictionary, return the "name" value and the "regex"
|
|
value (defaulting to False if not present).
|
|
"""
|
|
if nameArg is None:
|
|
return "", False
|
|
elif isinstance(nameArg, str):
|
|
return nameArg, False
|
|
elif isinstance(nameArg, dict):
|
|
return nameArg.get("name", ""), nameArg.get("regex_enabled", False)
|
|
else:
|
|
raise ValueError("Invalid argument type for nameArg.")
|
|
|
|
|
|
# Model/occurrence helpers
|
|
def containsBodies(occurrence: adsk.fusion.Occurrence) -> bool:
|
|
"""
|
|
Given an occurrence, returns true if there are any B-Rep or Mesh bodies in
|
|
it (without recursion to child occurrences).
|
|
"""
|
|
return occurrence.bRepBodies.count + occurrence.component.meshBodies.count > 0
|
|
|
|
|
|
def getValidOccurrences(
|
|
occurrence: adsk.fusion.Occurrence,
|
|
) -> list[adsk.fusion.Occurrence]:
|
|
"""
|
|
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.
|
|
"""
|
|
result = []
|
|
for childOcc in occurrence.childOccurrences:
|
|
if containsBodies(childOcc):
|
|
result.append(childOcc)
|
|
result.extend(getValidOccurrences(childOcc))
|
|
|
|
return result
|
|
|
|
|
|
def getManufacturingModelBRepBodies(
|
|
manufacturingModel: adsk.cam.ManufacturingModel,
|
|
) -> list[adsk.fusion.BRepBody]:
|
|
"""Get the bodies found in manufacturing model"""
|
|
occurrences = getValidOccurrences(manufacturingModel.occurrence)
|
|
result = []
|
|
if len(occurrences) > 0:
|
|
for occurrence in occurrences:
|
|
bRepBodies = occurrence.bRepBodies
|
|
if bRepBodies.count > 0:
|
|
result.extend([bRepBodies.item(i) for i in range(bRepBodies.count)])
|
|
|
|
return result
|
|
|
|
|
|
def getHoleSegmentBottomEdge(
|
|
holeSegment: adsk.cam.RecognizedHoleSegment,
|
|
) -> adsk.fusion.BRepEdge | None:
|
|
"""
|
|
Get the bottom edge of the given hole segment, if:
|
|
- the segment is made by one face
|
|
- the hole is aligned with Z+ (bounding box checking)
|
|
Returns None otherwise.
|
|
"""
|
|
|
|
if len(holeSegment.faces) != 1:
|
|
# Expected a hole segment with a single face
|
|
return None
|
|
|
|
face = adsk.fusion.BRepFace.cast(holeSegment.faces[0])
|
|
if not face:
|
|
# Expected a BRep face
|
|
return None
|
|
|
|
faceEdges: adsk.fusion.BRepEdges = face.edges
|
|
if faceEdges.count != 2:
|
|
# Expected a hole segment with a single face made of two edges
|
|
return None
|
|
|
|
# Return the lower edge in Z
|
|
if faceEdges[0].boundingBox.maxPoint.z < faceEdges[1].boundingBox.maxPoint.z:
|
|
return faceEdges[0]
|
|
else:
|
|
return faceEdges[1]
|
|
|
|
|
|
def boundarySelectionNeedsFlipping(edge: adsk.fusion.BRepEdge) -> bool:
|
|
"""
|
|
Check if the selected edge should be flipped for geometry selection (to
|
|
mill inside counterbore).
|
|
|
|
Returns true if it needs to be flipped.
|
|
"""
|
|
Z_POS = adsk.core.Vector3D.create(0, 0, 1)
|
|
|
|
geometry = edge.geometry
|
|
if geometry:
|
|
circle = adsk.core.Circle3D.cast(geometry)
|
|
if circle:
|
|
return not circle.normal.isEqualTo(Z_POS)
|
|
|
|
return False
|
|
|
|
|
|
# Item-by-name helpers
|
|
def getNormalizedLowerCaseName(name: str) -> str:
|
|
"""
|
|
Normalize the given name into consistent lower-case unicode characters.
|
|
"""
|
|
return unicodedata.normalize("NFKD", name).casefold()
|
|
|
|
|
|
def areMatchingCaseInsensitiveNames(name1: str, name2: str) -> bool:
|
|
"""
|
|
Check if two names match case-insensitively.
|
|
"""
|
|
return getNormalizedLowerCaseName(name1) == getNormalizedLowerCaseName(name2)
|
|
|
|
|
|
def hasMatchingCaseInsensitiveName(
|
|
item: adsk.cam.OperationBase,
|
|
name: str,
|
|
regexEnabled: bool = False,
|
|
) -> bool:
|
|
"""
|
|
Check if the given item has a name that matches the given name.
|
|
This does a case-insensitive comparison for both regular expressions and
|
|
exact matches.
|
|
If regexEnabled is True, `name` is treated as a regular expression.
|
|
"""
|
|
normalizedItemName = getNormalizedLowerCaseName(item.name)
|
|
normalizedName = getNormalizedLowerCaseName(name)
|
|
|
|
if regexEnabled:
|
|
return re.search(normalizedName, normalizedItemName) is not None
|
|
else:
|
|
return normalizedItemName == normalizedName
|
|
|
|
|
|
def findMatchingParentsFromList(
|
|
parents: list[adsk.cam.Base],
|
|
parentName: str | None,
|
|
regexEnabled: bool,
|
|
) -> tuple[MissingObjectReason, list[adsk.cam.Base], bool]:
|
|
"""
|
|
Find all parent objects (Setups, Folders, Patterns) in the list that match
|
|
the given name.
|
|
If parentName is None, all objects are returned.
|
|
If regexEnabled is True, the name is treated as a regular expression.
|
|
|
|
Returns a tuple of:
|
|
- A MissingObjectReason indicating the reason for missing objects
|
|
(NONE if objects were found)
|
|
- A list of adsk.cam.Base objects that match the name
|
|
- A boolean indicating if the search was for all objects
|
|
(True if parentName is None or empty)
|
|
"""
|
|
parentNameIsProvided: bool = isNonEmptyString(parentName)
|
|
|
|
# Get the scope of parents that we want to search
|
|
selectingAllObjects: bool = False
|
|
if not parentNameIsProvided:
|
|
selectingAllObjects = True
|
|
parentName = ".*"
|
|
regexEnabled = True
|
|
|
|
# Get the matching parents
|
|
matchingParents: list[adsk.cam.Base] = [
|
|
parent
|
|
for parent in parents
|
|
if hasMatchingCaseInsensitiveName(parent, parentName, regexEnabled)
|
|
]
|
|
|
|
# Check that we found at least one parent according to the search criteria
|
|
reason = MissingObjectReason.NONE
|
|
|
|
if not regexEnabled and parentNameIsProvided:
|
|
# If exact name is provided, check we found it
|
|
if len(matchingParents) == 0:
|
|
reason = MissingObjectReason.NO_MATCHING_NAME
|
|
elif not selectingAllObjects and regexEnabled and parentNameIsProvided:
|
|
# If regex is enabled, check we found at least one parent
|
|
if len(matchingParents) == 0:
|
|
reason = MissingObjectReason.NO_MATCHING_REGEX
|
|
elif len(matchingParents) == 0:
|
|
# If no search criteria is provided, there were no parents to find
|
|
reason = MissingObjectReason.NO_OBJECTS
|
|
|
|
return reason, matchingParents, selectingAllObjects
|
|
|
|
|
|
def findMatchingSetups(
|
|
setupName: str | None,
|
|
regexEnabled: bool,
|
|
cam: adsk.cam.CAM,
|
|
errorMessage: str,
|
|
) -> tuple[bool, list[adsk.cam.Setup], bool]:
|
|
"""
|
|
Find all setups in the CAM document that match the given name.
|
|
If setupName is None, all setups are returned.
|
|
If regexEnabled is True, the name is treated as a regular expression.
|
|
|
|
Returns a tuple of:
|
|
- A boolean indicating if setups were found that match the name
|
|
- A list of adsk.cam.Setup objects that match the name
|
|
- A boolean indicating if the search was for all setups
|
|
(True if setupName is None or empty)
|
|
|
|
If no setups are found according to the search criteria, the function will
|
|
add an error message to the CAMFunctionContext.
|
|
"""
|
|
|
|
reason, matchingObjects, selectingAllObjects = findMatchingParentsFromList(
|
|
parents=cam.setups,
|
|
parentName=setupName,
|
|
regexEnabled=regexEnabled,
|
|
)
|
|
|
|
if reason == MissingObjectReason.NO_MATCHING_NAME:
|
|
CAMFunctionContext.fail(
|
|
message=errorMessage,
|
|
error=f"Setup '{setupName}' not found.",
|
|
)
|
|
elif reason == MissingObjectReason.NO_MATCHING_REGEX:
|
|
CAMFunctionContext.fail(
|
|
message=errorMessage,
|
|
error=f"No matching setups for the name: '{setupName}'.",
|
|
)
|
|
elif reason == MissingObjectReason.NO_OBJECTS:
|
|
CAMFunctionContext.fail(
|
|
message=errorMessage,
|
|
error="No setups found.",
|
|
)
|
|
|
|
return reason == MissingObjectReason.NONE, matchingObjects, selectingAllObjects
|
|
|
|
|
|
def findMatchingParents(
|
|
parentName: str | None,
|
|
regexEnabled: bool,
|
|
cam: adsk.cam.CAM,
|
|
errorMessage: str,
|
|
) -> tuple[bool, list[adsk.cam.Base], bool]:
|
|
"""
|
|
Find all objects in the CAM document that can contain operations
|
|
(Setups, Folders, Patterns), that match the given name.
|
|
If parentName is None, all parents are returned.
|
|
If regexEnabled is True, the name is treated as a regular expression.
|
|
|
|
Returns a tuple of:
|
|
- A boolean indicating if parents were found that match the name
|
|
- A list of adsk.cam.Base objects that match the name
|
|
- A boolean indicating if the search was for all parents
|
|
(True if parentName is None or empty)
|
|
|
|
If no parents are found according to the search criteria, the function will
|
|
add an error message to the CAMFunctionContext.
|
|
"""
|
|
|
|
# Get all objects that could contain an operation
|
|
objects: list[adsk.cam.Base] = []
|
|
|
|
def addNestedParentObjects(
|
|
parent: adsk.cam.CAMFolder | adsk.cam.CAMPattern | adsk.cam.Setup,
|
|
):
|
|
objects.append(parent)
|
|
for child in parent.children:
|
|
if (
|
|
child.objectType == adsk.cam.CAMFolder.classType()
|
|
or child.objectType == adsk.cam.CAMPattern.classType()
|
|
):
|
|
addNestedParentObjects(child)
|
|
|
|
for setup in cam.setups:
|
|
addNestedParentObjects(setup)
|
|
|
|
# Find the matching parents
|
|
reason, matchingParents, selectingAllParents = findMatchingParentsFromList(
|
|
parents=objects,
|
|
parentName=parentName,
|
|
regexEnabled=regexEnabled,
|
|
)
|
|
|
|
if reason == MissingObjectReason.NO_MATCHING_NAME:
|
|
CAMFunctionContext.fail(
|
|
message=errorMessage,
|
|
error=f"Parent '{parentName}' not found.",
|
|
)
|
|
elif reason == MissingObjectReason.NO_MATCHING_REGEX:
|
|
CAMFunctionContext.fail(
|
|
message=errorMessage,
|
|
error=f"No matching parents for the name: '{parentName}'.",
|
|
)
|
|
elif reason == MissingObjectReason.NO_OBJECTS:
|
|
CAMFunctionContext.fail(
|
|
message=errorMessage,
|
|
error="No parents found.",
|
|
)
|
|
|
|
return reason == MissingObjectReason.NONE, matchingParents, selectingAllParents
|
|
|
|
|
|
def findMatchingSetupsByType(
|
|
setupName: str | None,
|
|
regexEnabled: bool,
|
|
cam: adsk.cam.CAM,
|
|
operationType: adsk.cam.OperationTypes,
|
|
errorMessage: str,
|
|
) -> tuple[bool, list[adsk.cam.Setup], bool]:
|
|
"""
|
|
Find all setups in the CAM document that match the given name and operation
|
|
type.
|
|
If setupName is None, all setups are returned.
|
|
If regexEnabled is True, the name is treated as a regular expression.
|
|
|
|
Returns a tuple of:
|
|
- A boolean indicating if setups were found that match the name
|
|
- A list of adsk.cam.Setup objects that match the name
|
|
- A boolean indicating if the search was for all setups
|
|
(True if setupName is None or empty)
|
|
|
|
If no setups are found according to the search criteria, the function will
|
|
add an error message to the CAMFunctionContext.
|
|
"""
|
|
|
|
# Find all setups that match the name
|
|
foundSetups, setups, selectingAllSetups = findMatchingSetups(
|
|
setupName=setupName,
|
|
regexEnabled=regexEnabled,
|
|
cam=cam,
|
|
errorMessage=errorMessage,
|
|
)
|
|
if not foundSetups:
|
|
# If no setups were found, return False
|
|
return False, [], selectingAllSetups
|
|
|
|
# Check if the setups are the right type
|
|
def isCorrectOperationType(setup):
|
|
return setup.operationType == operationType
|
|
|
|
matchingSetups = [setup for setup in setups if isCorrectOperationType(setup)]
|
|
differentSetups = [setup for setup in setups if not isCorrectOperationType(setup)]
|
|
if (len(differentSetups) > 0) and not (regexEnabled or selectingAllSetups):
|
|
CAMFunctionContext.fail(
|
|
message=errorMessage,
|
|
error=f"Setup '{setups[0].name}' is not the correct type '{getOperationTypeString(operationType)}'.",
|
|
)
|
|
foundSetups = False
|
|
elif len(matchingSetups) == 0:
|
|
if selectingAllSetups:
|
|
CAMFunctionContext.fail(
|
|
message=errorMessage,
|
|
error=f"No {getOperationTypeString(operationType)} setups found.",
|
|
)
|
|
else:
|
|
CAMFunctionContext.fail(
|
|
message=errorMessage,
|
|
error=f"No matching {getOperationTypeString(operationType)} setups for the name: '{setupName}'.",
|
|
)
|
|
foundSetups = False
|
|
|
|
return foundSetups, matchingSetups, selectingAllSetups
|
|
|
|
|
|
def findMatchingAdditiveSetups(
|
|
setupName: str | None,
|
|
regexEnabled: bool,
|
|
cam: adsk.cam.CAM,
|
|
errorMessage: str,
|
|
) -> tuple[bool, list[adsk.cam.Setup]]:
|
|
"""
|
|
Find all additive setups in the CAM document that match the given name.
|
|
If setupName is None, all setups are returned.
|
|
If regexEnabled is True, the name is treated as a regular expression.
|
|
|
|
Returns a tuple of:
|
|
- A boolean indicating if additive setups were found that match the name
|
|
- A list of adsk.cam.Setup objects that match the name and are additive
|
|
setups
|
|
"""
|
|
|
|
foundSetups, matchingSetups, _ = findMatchingSetupsByType(
|
|
setupName=setupName,
|
|
regexEnabled=regexEnabled,
|
|
cam=cam,
|
|
operationType=adsk.cam.OperationTypes.AdditiveOperation,
|
|
errorMessage=errorMessage,
|
|
)
|
|
return foundSetups, matchingSetups
|
|
|
|
|
|
def findAdditiveArrangeInSetup(setup: adsk.cam.Setup):
|
|
operations = setup.operations
|
|
for i in range(operations.count):
|
|
operation = operations.item(i)
|
|
if operation.strategy == "additive_arrange":
|
|
return operation
|
|
return None
|
|
|
|
|
|
def searchChildrenForName(
|
|
children: adsk.cam.ChildOperationList,
|
|
name: str,
|
|
objectType: CAMObjectType,
|
|
parentTask: CAMFunctionTaskContext,
|
|
) -> tuple[adsk.cam.OperationBase | None, str | None]:
|
|
"""
|
|
Iterate through all child objects in the given collection and search for
|
|
an object with the given name.
|
|
"""
|
|
|
|
# If the children collection is empty, return None
|
|
if children.count == 0:
|
|
return None, None
|
|
|
|
with parentTask.makeSubTask(numberOfTasks=children.count) as task:
|
|
for i in range(children.count):
|
|
with task.makeSubTaskByIndex(index=i) as childTask:
|
|
child = children.item(i)
|
|
if child.objectType == adsk.cam.CAMFolder.classType():
|
|
if objectType.includesType(
|
|
CAMObjectType.FOLDER
|
|
) and hasMatchingCaseInsensitiveName(child, name):
|
|
return child, CAMObjectType.FOLDER.value
|
|
# Recursively search within folder children
|
|
found, typeStr = searchChildrenForName(
|
|
child.children, name, objectType, childTask
|
|
)
|
|
if found:
|
|
return found, typeStr
|
|
elif child.objectType == adsk.cam.CAMPattern.classType():
|
|
if objectType.includesType(
|
|
CAMObjectType.PATTERN
|
|
) and hasMatchingCaseInsensitiveName(child, name):
|
|
return child, CAMObjectType.PATTERN.value
|
|
# Recursively search within pattern children
|
|
found, typeStr = searchChildrenForName(
|
|
child.children, name, objectType, childTask
|
|
)
|
|
if found:
|
|
return found, typeStr
|
|
elif (
|
|
objectType.includesType(CAMObjectType.OPERATION)
|
|
and child.objectType == adsk.cam.Operation.classType()
|
|
and hasMatchingCaseInsensitiveName(child, name)
|
|
):
|
|
return child, CAMObjectType.OPERATION.value
|
|
|
|
return None, None
|
|
|
|
|
|
def findObjectByName(
|
|
cam: adsk.cam.CAM,
|
|
name: str,
|
|
object_type: str,
|
|
parentTask: CAMFunctionTaskContext,
|
|
) -> tuple[adsk.cam.OperationBase | None, str | None]:
|
|
"""
|
|
Returns the first object found in the CAM document with the given name and
|
|
a string of its type.
|
|
"""
|
|
|
|
objectType = CAMObjectType.ANY
|
|
if object_type is not None:
|
|
try:
|
|
objectType = CAMObjectType(object_type)
|
|
except ValueError:
|
|
CAMFunctionContext.warn(
|
|
message="Manufacturing object type is not recognized.",
|
|
warning=f"Unrecognized manufacturing object type: '{object_type}'. All manufacturing objects will be used for searching.",
|
|
)
|
|
|
|
if objectType.includesType(CAMObjectType.MANUFACTURING_MODEL):
|
|
manufacturingModels = cam.manufacturingModels
|
|
if manufacturingModels.count > 0:
|
|
for i in range(manufacturingModels.count):
|
|
manufacturingModel = manufacturingModels.item(i)
|
|
if hasMatchingCaseInsensitiveName(manufacturingModel, name):
|
|
return manufacturingModel, "manufacturing model"
|
|
|
|
if objectType != CAMObjectType.MANUFACTURING_MODEL:
|
|
# Only when we have a manufacturing model we don't want to look through the setups
|
|
setups = cam.setups
|
|
if setups.count == 0:
|
|
# If there are no setups, return None
|
|
return None, None
|
|
|
|
with parentTask.makeSubTask(numberOfTasks=setups.count) as task:
|
|
for i, setup in enumerate(setups):
|
|
with task.makeSubTaskByIndex(index=i) as setupTask:
|
|
if objectType.includesType(
|
|
CAMObjectType.SETUP
|
|
) and hasMatchingCaseInsensitiveName(setup, name):
|
|
return setup, "setup"
|
|
|
|
# Search within setup children for the named object if the setup name does not match
|
|
if objectType.canBeUnderSetup():
|
|
found, typeStr = searchChildrenForName(
|
|
setup.children, name, objectType, setupTask
|
|
)
|
|
if found:
|
|
return found, typeStr
|
|
|
|
return None, None
|
|
|
|
|
|
# CAM operation helpers
|
|
def createManufacturingModel(
|
|
cam: adsk.cam.CAM,
|
|
task: CAMFunctionTaskContext,
|
|
name: str = None,
|
|
) -> adsk.cam.ManufacturingModel:
|
|
"""
|
|
Create a new manufacturing model in the CAM document.
|
|
"""
|
|
|
|
manufacturingModels = cam.manufacturingModels
|
|
mmInput = manufacturingModels.createInput()
|
|
|
|
# Change the name if it is defined
|
|
if isNonEmptyString(name):
|
|
mmInput.name = name
|
|
|
|
# Async worker to create the manufacturing model
|
|
#
|
|
# This can be a long operation, so we run it asynchronously and
|
|
# indeterminately increment the progress bar
|
|
async def worker() -> adsk.cam.ManufacturingModel:
|
|
return manufacturingModels.add(mmInput)
|
|
|
|
manufacturingModel = task.runAsyncWorker(worker=worker)
|
|
|
|
# Creating a new manufacturing model will schedule an idle task for invalidating
|
|
# any CAD reference cache. Since we need to use the manufacturing model right away,
|
|
# we need to ensure that the cache is valid before proceeding and not rely on
|
|
# adsk.doEvents() calls that might execute the idle task we need or not.
|
|
# Calling cam.checkValidity() will force the cache to be updated as necessary and
|
|
# the manufacturing model can be used as a CAD reference as normal.
|
|
cam.checkValidity()
|
|
|
|
# Check that the manufacturing model was created with the correct name
|
|
if isinstance(name, str) and name != manufacturingModel.name:
|
|
CAMFunctionContext.blockFutureExecution(
|
|
warning=f"Created manufacturing model named '{manufacturingModel.name}' (instead of '{name}')."
|
|
)
|
|
else:
|
|
CAMFunctionContext.succeed(
|
|
message=f"Created manufacturing model '{manufacturingModel.name}'."
|
|
)
|
|
return manufacturingModel
|
|
|
|
|
|
def getManufacturingModelByName(
|
|
cam: adsk.cam.CAM, name: str
|
|
) -> adsk.cam.ManufacturingModel | None:
|
|
"""
|
|
Returns the first manufacturing model matching the given name, or None.
|
|
"""
|
|
manufacturingModels = [
|
|
model
|
|
for model in cam.manufacturingModels
|
|
if hasMatchingCaseInsensitiveName(model, name)
|
|
]
|
|
return next(iter(manufacturingModels), None)
|
|
|
|
|
|
def setAdditiveSetup(
|
|
cam: adsk.cam.CAM,
|
|
additiveTechnology: str,
|
|
setupInput: adsk.cam.SetupInput,
|
|
task: CAMFunctionTaskContext,
|
|
manufacturingModelName: str = None,
|
|
) -> bool:
|
|
"""
|
|
Helper function to set machine, print setting, and model in the inputs for
|
|
a new additive setup.
|
|
"""
|
|
|
|
# Check the chosen technology, determine the machine and print setting
|
|
printSettingRegex = None
|
|
if additiveTechnology == "FFF":
|
|
machineModel = "Generic FFF Machine"
|
|
printSettingName = "PLA (Direct Drive)"
|
|
elif additiveTechnology == "DED":
|
|
machineModel = "Generic DED"
|
|
printSettingName = None
|
|
elif additiveTechnology == "MPBF":
|
|
machineModel = "Generic MPBF"
|
|
printSettingName = "MPBF 10 micron"
|
|
printSettingRegex = re.compile("MPBF 10 micron")
|
|
elif additiveTechnology == "MJF":
|
|
machineModel = "Jet Fusion 5000"
|
|
printSettingName = "HP - MJF"
|
|
elif additiveTechnology == "Binder Jetting":
|
|
machineModel = "Metal Jet S100"
|
|
printSettingName = "HP - BinderJet"
|
|
elif additiveTechnology == "SLA/DLP":
|
|
machineModel = "Generic SLA/DLP Printer"
|
|
printSettingName = "Autodesk Generic SLA"
|
|
elif additiveTechnology == "SLS":
|
|
machineModel = "Generic SLS Machine"
|
|
printSettingName = "Generic SLS"
|
|
else:
|
|
CAMFunctionContext.fail(
|
|
message="Error setting additive setup:",
|
|
error="Additive technology not specified or unknown.",
|
|
)
|
|
return False
|
|
|
|
# Find the machine model and print setting from Fusion library
|
|
camManager = adsk.cam.CAMManager.get()
|
|
libraryManager = camManager.libraryManager
|
|
|
|
machine = None
|
|
machineTask = task.makeSubTask(
|
|
message="Getting machine",
|
|
progressRange=(0, 70),
|
|
)
|
|
with machineTask:
|
|
machineLibrary = libraryManager.machineLibrary
|
|
machineUrl = machineLibrary.urlByLocation(
|
|
adsk.cam.LibraryLocations.Fusion360LibraryLocation
|
|
) ## .Fusion360LibraryLocation vs .LocalLibraryLocation etc.
|
|
|
|
# Get the machines from this location
|
|
#
|
|
# This can be a long operation, so we run it asynchronously and
|
|
# indeterminately increment the progress bar
|
|
async def worker() -> list[adsk.cam.Machine]:
|
|
return machineLibrary.childMachines(machineUrl)
|
|
|
|
machines = machineTask.runAsyncWorker(worker=worker)
|
|
|
|
for m in machines:
|
|
if m.model == machineModel:
|
|
machine = m
|
|
break
|
|
|
|
if not machine:
|
|
CAMFunctionContext.fail(
|
|
message="Error setting additive setup:",
|
|
error=f"Machine '{machineModel}' not found.",
|
|
)
|
|
return False
|
|
|
|
# Find the print setting, if the machine model requires one
|
|
printSettingTask = task.makeSubTask(
|
|
message="Getting print setting",
|
|
progressRange=(70, 90),
|
|
)
|
|
with printSettingTask:
|
|
if printSettingName is not None:
|
|
printSettingLibrary = libraryManager.printSettingLibrary
|
|
printSettingUrl = printSettingLibrary.urlByLocation(
|
|
adsk.cam.LibraryLocations.Fusion360LibraryLocation
|
|
) ## .Fusion360LibraryLocation vs .LocalLibraryLocation etc.
|
|
|
|
# Get the print settings from this location
|
|
#
|
|
# This can be a long operation, so we run it asynchronously and
|
|
# indeterminately increment the progress bar
|
|
async def worker() -> list[adsk.cam.PrintSetting]:
|
|
return printSettingLibrary.childPrintSettings(printSettingUrl)
|
|
|
|
printSettings = printSettingTask.runAsyncWorker(worker=worker)
|
|
|
|
printSetting = None
|
|
if printSettingRegex is None:
|
|
printSettingRegex = re.compile(f"^{re.escape(printSettingName)}$")
|
|
for ps in printSettings:
|
|
if printSettingRegex.search(ps.name):
|
|
printSetting = ps
|
|
break
|
|
if not printSetting:
|
|
CAMFunctionContext.fail(
|
|
message="Error setting additive setup:",
|
|
error=f"Print setting matching '{printSettingName}' not found.",
|
|
)
|
|
return False
|
|
else:
|
|
printSetting = None
|
|
|
|
# Determine the manufacturing model to use
|
|
# If name is given, use the first matching model with that name
|
|
# If not given, use the first available manufacturing model
|
|
# If we don't find a model, create one
|
|
manufacturingModels = cam.manufacturingModels
|
|
manufacturingModel = None
|
|
|
|
manufacturingModelTask = task.makeSubTask(
|
|
message="Getting manufacturing model",
|
|
progressRange=(90, 95),
|
|
)
|
|
with manufacturingModelTask:
|
|
if isNonEmptyString(manufacturingModelName):
|
|
manufacturingModel = getManufacturingModelByName(
|
|
cam=cam,
|
|
name=manufacturingModelName,
|
|
)
|
|
if not manufacturingModel:
|
|
CAMFunctionContext.fail(
|
|
message="Error setting additive setup:",
|
|
error=f"Manufacturing model '{manufacturingModelName}' not found.",
|
|
)
|
|
return False
|
|
elif manufacturingModels.count > 0:
|
|
manufacturingModel = manufacturingModels.item(0)
|
|
|
|
# Check that we have models to use for the models of the setup
|
|
if manufacturingModel is None:
|
|
validModel = (
|
|
containsBodies(cam.designRootOccurrence)
|
|
or len(getValidOccurrences(cam.designRootOccurrence)) > 0
|
|
)
|
|
else:
|
|
models = getValidOccurrences(manufacturingModel.occurrence)
|
|
validModel = len(models) > 0
|
|
|
|
manufacturingModelTask.updateUI()
|
|
|
|
if not validModel:
|
|
CAMFunctionContext.fail(
|
|
message="Error creating additive setup:",
|
|
error="No available components exist to select as the Models of the setup.",
|
|
)
|
|
return False
|
|
|
|
if not manufacturingModel:
|
|
manufacturingModel = createManufacturingModel(
|
|
cam=cam,
|
|
task=manufacturingModelTask,
|
|
)
|
|
models = getValidOccurrences(manufacturingModel.occurrence)
|
|
|
|
# Fill in the SetupInput with the inputs for an additive setup
|
|
with task.makeSubTask(progressRange=(95, 100)):
|
|
setupInput.machine = machine
|
|
setupInput.printSetting = printSetting
|
|
setupInput.models = models
|
|
|
|
# DED machines require the Stock solids to be set
|
|
if additiveTechnology == "DED":
|
|
setupInput.stockSolids = models
|
|
|
|
if printSetting is not None:
|
|
CAMFunctionContext.succeed(
|
|
message=f"Selected the '{machine.model}' machine and '{printSetting.name}' print setting for the new additive setup with {additiveTechnology} technology."
|
|
)
|
|
else:
|
|
CAMFunctionContext.succeed(
|
|
message=f"Selected the '{machine.model}' machine for the new additive setup with {additiveTechnology} technology."
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
def setTurningSetup(
|
|
cam: adsk.cam.CAM,
|
|
modelInputMode: ModelInputMode,
|
|
setupInput: adsk.cam.SetupInput,
|
|
) -> bool:
|
|
"""
|
|
Helper function to set the inputs for a new turning setup.
|
|
"""
|
|
|
|
# Turning setups require a valid model
|
|
#
|
|
# If the model input mode is "none", then the model inputs do not need to
|
|
# be set but we still need to check that there is a valid model in the
|
|
# scene to create the setup.
|
|
#
|
|
# Otherwise, the ModelInput object would have set up the models on the setup
|
|
# input already.
|
|
if modelInputMode == ModelInputMode.NONE:
|
|
# If the model input mode is "none", then the model inputs do not need to
|
|
# be set but we still need to check that there is a valid model in the
|
|
# scene to create the setup.
|
|
if not containsBodies(cam.designRootOccurrence):
|
|
CAMFunctionContext.fail(
|
|
message="Error setting turning setup:",
|
|
error="No component has been added to the scene",
|
|
)
|
|
return False
|
|
else:
|
|
if len(setupInput.models) == 0:
|
|
CAMFunctionContext.fail(
|
|
message="Error setting turning setup:",
|
|
error="No models have been set for the setup",
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def setCAMParameters(
|
|
inputParameters: dict[str, any],
|
|
camParameters: adsk.cam.CAMParameters,
|
|
task: CAMFunctionTaskContext,
|
|
) -> list[str]:
|
|
"""
|
|
Set the input parameters on the given CAM parameters collection.
|
|
Values in the input are expressions to be set.
|
|
Returns a list of parameters that raised an exception while setting.
|
|
"""
|
|
|
|
warningParams: list[str] = []
|
|
|
|
for i, (key, value) in enumerate(inputParameters.items()):
|
|
task.updateTaskProgress(i, len(inputParameters))
|
|
try:
|
|
camParameters.itemByName(key).expression = value
|
|
except Exception:
|
|
warningParams.append(key)
|
|
|
|
return warningParams
|
|
|
|
|
|
def createDrillOperation(
|
|
setup: adsk.cam.Setup,
|
|
tool: adsk.cam.Tool | None,
|
|
holeGroup: adsk.cam.RecognizedHoleGroup,
|
|
drillTipThroughBottom: bool,
|
|
) -> adsk.cam.Operation:
|
|
"""
|
|
Create a drilling operation for the given hole group using the specified tool.
|
|
If drillTipThroughBottom is True, the drill tip will go through the bottom of
|
|
the hole. Otherwise, it will stop at the bottom of the hole.
|
|
"""
|
|
|
|
# Select the hole faces to drill from the hole group, excluding flats
|
|
faces: list[adsk.fusion.BRepFace] = []
|
|
for hole in holeGroup:
|
|
for i in range(hole.segmentCount):
|
|
segment: adsk.cam.RecognizedHoleSegment = hole.segment(i)
|
|
if segment.holeSegmentType != adsk.cam.HoleSegmentType.HoleSegmentTypeFlat:
|
|
faces.extend(segment.faces)
|
|
|
|
# Create the operation input for drilling
|
|
input: adsk.cam.OperationInput = setup.operations.createInput("drill")
|
|
input.parameters.itemByName(
|
|
"drillTipThroughBottom"
|
|
).value.value = drillTipThroughBottom
|
|
input.parameters.itemByName("breakThroughDepth").expression = "2 mm"
|
|
input.parameters.itemByName("holeFaces").value.value = faces
|
|
if tool:
|
|
input.tool = tool
|
|
|
|
# Create and return the drilling operation
|
|
op: adsk.cam.Operation = setup.operations.add(input)
|
|
return op
|
|
|
|
|
|
def createCounterboreMillOperation(
|
|
setup: adsk.cam.Setup,
|
|
tool: adsk.cam.Tool | None,
|
|
holeGroup: adsk.cam.RecognizedHoleGroup,
|
|
) -> adsk.cam.Operation:
|
|
"""
|
|
Create a counterbore milling operation for the given hole group using the
|
|
specified tool.
|
|
"""
|
|
|
|
# Select the counterbore bottom edge to mill
|
|
edges: list[adsk.fusion.BRepEdge] = []
|
|
for hole in holeGroup:
|
|
# Find the first cylinder (skipping cone/chamfer)
|
|
seg0: adsk.cam.RecognizedHoleSegment = hole.segment(0)
|
|
seg1: adsk.cam.RecognizedHoleSegment = hole.segment(1)
|
|
if seg0.holeSegmentType == adsk.cam.HoleSegmentType.HoleSegmentTypeCylinder:
|
|
counterboreSegment: adsk.cam.RecognizedHoleSegment = seg0
|
|
elif seg1.holeSegmentType == adsk.cam.HoleSegmentType.HoleSegmentTypeCylinder:
|
|
counterboreSegment = seg1
|
|
else:
|
|
continue
|
|
|
|
edge: adsk.fusion.BRepEdge | None = getHoleSegmentBottomEdge(counterboreSegment)
|
|
if edge:
|
|
edges.append(edge)
|
|
|
|
# Create the operation input for counterbore milling
|
|
input: adsk.cam.OperationInput = setup.operations.createInput("contour2d")
|
|
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"
|
|
if tool:
|
|
input.tool = tool
|
|
|
|
# Add each edge to the contours chains as separate selections, since they are owned by different holes
|
|
holeSelection: adsk.cam.CadContours2dParameterValue = input.parameters.itemByName(
|
|
"contours"
|
|
).value
|
|
chains: adsk.cam.CurveSelections = holeSelection.getCurveSelections()
|
|
|
|
for edge in edges:
|
|
chain: adsk.cam.ChainSelection = chains.createNewChainSelection()
|
|
chain.isReverted = boundarySelectionNeedsFlipping(edge)
|
|
chain.inputGeometry = [edge]
|
|
holeSelection.applyCurveSelections(chains)
|
|
|
|
# Create and return the milling operation
|
|
op: adsk.cam.Operation = setup.operations.add(input)
|
|
return op
|
|
|
|
|
|
def createChamferOperation(
|
|
setup: adsk.cam.Setup,
|
|
tool: adsk.cam.Tool | None,
|
|
holeGroup: adsk.cam.RecognizedHoleGroup,
|
|
) -> adsk.cam.Operation:
|
|
"""
|
|
Create an operation for the top chamfer of the given hole group.
|
|
"""
|
|
|
|
# Select the counterbore chamfer's bottom edge to mill
|
|
edges: list[adsk.fusion.BRepEdge] = []
|
|
for hole in holeGroup:
|
|
# Index 0 is the chamfer segment
|
|
firstSegment: adsk.cam.RecognizedHoleSegment = hole.segment(0)
|
|
edge: adsk.fusion.BRepEdge | None = getHoleSegmentBottomEdge(firstSegment)
|
|
if edge:
|
|
edges.append(edge)
|
|
|
|
# Create the operation input for chamfer milling
|
|
input: adsk.cam.OperationInput = setup.operations.createInput("chamfer2d")
|
|
input.parameters.itemByName("chamferClearance").expression = "0 mm"
|
|
input.parameters.itemByName("chamferTipOffset").expression = "1 mm"
|
|
input.parameters.itemByName("entry_distance").expression = "5 mm"
|
|
if tool:
|
|
input.tool = tool
|
|
|
|
# Add each edge to the contours chains as separate selections, since they are owned by different holes
|
|
holeSelection: adsk.cam.CadContours2dParameterValue = input.parameters.itemByName(
|
|
"contours"
|
|
).value
|
|
chains: adsk.cam.CurveSelections = holeSelection.getCurveSelections()
|
|
|
|
for edge in edges:
|
|
chain: adsk.cam.ChainSelection = chains.createNewChainSelection()
|
|
chain.isReverted = True
|
|
chain.inputGeometry = [edge]
|
|
holeSelection.applyCurveSelections(chains)
|
|
|
|
# Add to the setup
|
|
op: adsk.cam.Operation = setup.operations.add(input)
|
|
return op
|
|
|
|
|
|
def regenerateToolpaths(
|
|
cam: adsk.cam.CAM,
|
|
task: CAMFunctionTaskContext,
|
|
operations: list[adsk.cam.Operation] | None = None,
|
|
skipValid: bool = True,
|
|
) -> list[adsk.cam.Operation]:
|
|
"""
|
|
Regenerate the toolpath(s) according to the provided arguments.
|
|
|
|
:param operations: If provided, only these operations will be regenerated.
|
|
:param skipValid: If True, only operations that are not valid will be regenerated.
|
|
If False, all operations will be regenerated.
|
|
Only applicable if operations is None.
|
|
|
|
Returns a list of operations that were regenerated.
|
|
"""
|
|
|
|
generatedOperations: list[adsk.cam.Operation] = []
|
|
|
|
if operations is None:
|
|
future: adsk.cam.GenerateToolpathFuture = cam.generateAllToolpaths(skipValid)
|
|
else:
|
|
future: adsk.cam.GenerateToolpathFuture = cam.generateToolpath(
|
|
adsk.core.ObjectCollection.createWithArray(operations),
|
|
)
|
|
|
|
try:
|
|
generatedOperations.extend(future.operations)
|
|
|
|
while not future.isGenerationCompleted:
|
|
task.updateTaskProgress(future.numberOfCompletedTasks, future.numberOfTasks)
|
|
time.sleep(0.1)
|
|
|
|
except Exception as e:
|
|
# Gracefully handle "Generation not started" errors, re-raise other exceptions
|
|
if "Generation not started" not in str(e):
|
|
raise e
|
|
|
|
# Return the generated operations
|
|
return generatedOperations
|
|
|
|
|
|
def selectOperations(
|
|
ui: adsk.core.UserInterface,
|
|
operations: list[adsk.cam.Operation],
|
|
task: CAMFunctionTaskContext,
|
|
):
|
|
"""
|
|
Select the given operations in the CAM workspace.
|
|
"""
|
|
|
|
ui.activeSelections.clear()
|
|
|
|
if len(operations) == 0:
|
|
return
|
|
|
|
with task.makeSubTask(numberOfTasks=len(operations)) as selectTask:
|
|
for i, operation in enumerate(operations):
|
|
selectTask.updateTaskProgress(i, len(operations))
|
|
ui.activeSelections.add(operation)
|
|
|
|
|
|
# Tool Library helpers
|
|
def getToolLibrary(
|
|
toolLibraryDefinition: ToolLibraryDefinition,
|
|
task: CAMFunctionTaskContext,
|
|
) -> adsk.cam.ToolLibrary | None:
|
|
"""
|
|
Return a tool library from a tool library definition.
|
|
"""
|
|
|
|
if not isNonEmptyString(toolLibraryDefinition.name):
|
|
return None
|
|
|
|
locationsToSearch: list[adsk.cam.LibraryLocations] = []
|
|
libraryLocation = adsk.cam.LibraryLocations.LocalLibraryLocation
|
|
match toolLibraryDefinition.type:
|
|
case ToolLibraryType.SAMPLES:
|
|
locationsToSearch.append(adsk.cam.LibraryLocations.Fusion360LibraryLocation)
|
|
case ToolLibraryType.LOCAL:
|
|
locationsToSearch.append(adsk.cam.LibraryLocations.LocalLibraryLocation)
|
|
case ToolLibraryType.CLOUD:
|
|
locationsToSearch.append(adsk.cam.LibraryLocations.CloudLibraryLocation)
|
|
case ToolLibraryType.ALL:
|
|
# Search both local and Sample libraries in that order
|
|
locationsToSearch.append(adsk.cam.LibraryLocations.LocalLibraryLocation)
|
|
locationsToSearch.append(adsk.cam.LibraryLocations.Fusion360LibraryLocation)
|
|
locationsToSearch.append(adsk.cam.LibraryLocations.CloudLibraryLocation)
|
|
case ToolLibraryType.DOCUMENT:
|
|
# We should have already have grabbed the document library and set it in the tool library definition
|
|
return toolLibraryDefinition.toolLibrary
|
|
case _:
|
|
return None
|
|
|
|
if len(locationsToSearch) == 0:
|
|
return None
|
|
|
|
camManager = adsk.cam.CAMManager.get()
|
|
libraryManager: adsk.cam.CAMLibraryManager = camManager.libraryManager
|
|
toolLibraries: adsk.cam.ToolLibraries = libraryManager.toolLibraries
|
|
|
|
with task.makeSubTask(numberOfTasks=len(locationsToSearch)) as searchTask:
|
|
for i, libraryLocation in enumerate(locationsToSearch):
|
|
with searchTask.makeSubTaskByIndex(index=i) as libraryTask:
|
|
try:
|
|
libraryFolder = toolLibraries.urlByLocation(libraryLocation)
|
|
url = libraryFolder.join(f"{toolLibraryDefinition.name}.json")
|
|
|
|
# This can be a long operation, so we run it asynchronously and
|
|
# indeterminately increment the progress bar
|
|
async def worker() -> adsk.cam.ToolLibrary:
|
|
return toolLibraries.toolLibraryAtURL(url)
|
|
|
|
toolLibrary: adsk.cam.ToolLibrary = libraryTask.runAsyncWorker(
|
|
worker=worker
|
|
)
|
|
if toolLibrary:
|
|
return toolLibrary
|
|
except Exception:
|
|
continue
|
|
|
|
return None
|
|
|
|
|
|
def getToolsByQueryForAllLibraries(
|
|
toolLibraryDefinition: ToolLibraryDefinition | None,
|
|
addCriteriaToQueryFunction: Callable[[adsk.cam.ToolQuery], None],
|
|
task: CAMFunctionTaskContext,
|
|
) -> list[adsk.cam.Tool]:
|
|
"""
|
|
Get the tools that fulfill the given criteria searching through all types of libraries.
|
|
The order of libraries is:
|
|
- Local Library
|
|
- Fusion Sample Tool Libraries
|
|
- Document Library
|
|
"""
|
|
for libraryType in TOOL_LIBRARY_SEARCH_PRIORITY:
|
|
possibleToolLibraryDefinition = ToolLibraryDefinition(
|
|
name=toolLibraryDefinition.name
|
|
if toolLibraryDefinition and isNonEmptyString(toolLibraryDefinition.name)
|
|
else None,
|
|
type=libraryType,
|
|
toolLibrary=toolLibraryDefinition.toolLibrary
|
|
if toolLibraryDefinition
|
|
else None,
|
|
)
|
|
tools = getToolsByQuery(
|
|
toolLibraryDefinition=possibleToolLibraryDefinition,
|
|
addCriteriaToQueryFunction=addCriteriaToQueryFunction,
|
|
task=task,
|
|
)
|
|
if len(tools) > 0:
|
|
return tools
|
|
|
|
return []
|
|
|
|
|
|
def getToolsByQuery(
|
|
toolLibraryDefinition: ToolLibraryDefinition | None,
|
|
addCriteriaToQueryFunction: Callable[[adsk.cam.ToolQuery], None],
|
|
task: CAMFunctionTaskContext,
|
|
) -> list[adsk.cam.Tool]:
|
|
"""
|
|
Return the tools that match the given criteria from the specified tool definition.
|
|
"""
|
|
|
|
# Define the query to find the tool by name
|
|
query: adsk.cam.ToolQuery | None = None
|
|
|
|
if toolLibraryDefinition:
|
|
if toolLibraryDefinition.toolLibrary:
|
|
query = toolLibraryDefinition.toolLibrary.createQuery()
|
|
elif isNonEmptyString(toolLibraryDefinition.name):
|
|
# If a tool library name is provided, try to get the library
|
|
toolLibrary = getToolLibrary(
|
|
toolLibraryDefinition=toolLibraryDefinition,
|
|
task=task,
|
|
)
|
|
if toolLibrary is None:
|
|
return []
|
|
|
|
query = toolLibrary.createQuery()
|
|
else:
|
|
camManager = adsk.cam.CAMManager.get()
|
|
libraryManager: adsk.cam.CAMLibraryManager = camManager.libraryManager
|
|
toolLibraries: adsk.cam.ToolLibraries = libraryManager.toolLibraries
|
|
match toolLibraryDefinition.type:
|
|
case ToolLibraryType.ALL:
|
|
return getToolsByQueryForAllLibraries(
|
|
toolLibraryDefinition=toolLibraryDefinition,
|
|
addCriteriaToQueryFunction=addCriteriaToQueryFunction,
|
|
task=task,
|
|
)
|
|
|
|
case ToolLibraryType.SAMPLES:
|
|
query = toolLibraries.createQuery(
|
|
adsk.cam.LibraryLocations.Fusion360LibraryLocation
|
|
)
|
|
case ToolLibraryType.LOCAL:
|
|
query = toolLibraries.createQuery(
|
|
adsk.cam.LibraryLocations.LocalLibraryLocation
|
|
)
|
|
case ToolLibraryType.CLOUD:
|
|
query = toolLibraries.createQuery(
|
|
adsk.cam.LibraryLocations.CloudLibraryLocation
|
|
)
|
|
case ToolLibraryType.DOCUMENT:
|
|
if toolLibraryDefinition.toolLibrary is not None:
|
|
# We should have already grabbed the document library and set it in the tool definition
|
|
query = toolLibraryDefinition.toolLibrary.createQuery()
|
|
|
|
else:
|
|
# If no tool library definition is provided, search all tool libraries
|
|
return getToolsByQueryForAllLibraries(
|
|
toolLibraryDefinition=toolLibraryDefinition,
|
|
addCriteriaToQueryFunction=addCriteriaToQueryFunction,
|
|
task=task,
|
|
)
|
|
|
|
if query is None:
|
|
return []
|
|
|
|
addCriteriaToQueryFunction(query)
|
|
|
|
# Get the query results
|
|
#
|
|
# This can be a long operation, so we run it asynchronously and
|
|
# indeterminately increment the progress bar
|
|
async def worker() -> list[adsk.cam.ToolQueryResult]:
|
|
try:
|
|
return query.execute()
|
|
except Exception:
|
|
return []
|
|
|
|
results = task.runAsyncWorker(worker=worker)
|
|
|
|
# Get the tools from the query
|
|
tools: list[adsk.cam.Tool] = [result.tool for result in results]
|
|
return tools
|
|
|
|
|
|
def getToolByName(
|
|
toolDefinition: ToolDefinition,
|
|
task: CAMFunctionTaskContext,
|
|
) -> adsk.cam.Tool | None:
|
|
"""
|
|
Return a tool from the given tool definition by name.
|
|
"""
|
|
if not isNonEmptyString(toolDefinition.name):
|
|
# If no name is specified, return None
|
|
return None
|
|
|
|
def addCriteriaToQueryFunction(query: adsk.cam.ToolQuery):
|
|
"""
|
|
Function to add criteria to the query.
|
|
This is passed as a parameter to getToolsByQuery.
|
|
"""
|
|
# Add criteria for tool description (name)
|
|
query.criteria.add(
|
|
"tool_description",
|
|
adsk.core.ValueInput.createByString(toolDefinition.name),
|
|
)
|
|
|
|
# Get the tools from the query
|
|
tools: list[adsk.cam.Tool] = getToolsByQuery(
|
|
toolLibraryDefinition=toolDefinition.toolLibraryDefinition,
|
|
addCriteriaToQueryFunction=addCriteriaToQueryFunction,
|
|
task=task,
|
|
)
|
|
|
|
# Return the first tool that matches the name
|
|
if len(tools) > 0:
|
|
return tools[0]
|
|
|
|
# If no tool was found, return None
|
|
return None
|
|
|
|
|
|
def getFilteredTools(
|
|
toolLibraryDefinition: ToolLibraryDefinition | None,
|
|
task: CAMFunctionTaskContext,
|
|
toolType: str | None = None,
|
|
minDiameter: float | None = None,
|
|
maxDiameter: float | None = None,
|
|
) -> list[adsk.cam.Tool]:
|
|
"""
|
|
Return a list of tools from the given tool definition that match the given
|
|
criteria. If no criteria are provided, all tools are returned.
|
|
"""
|
|
|
|
def addCriteriaToQueryFunction(query: adsk.cam.ToolQuery):
|
|
"""
|
|
Function to add criteria to the query.
|
|
This is passed as a parameter to getToolsByQuery.
|
|
"""
|
|
# Add criteria for tool type, if specified
|
|
if toolType is not None:
|
|
query.criteria.add(
|
|
"tool_type", adsk.core.ValueInput.createByString(toolType)
|
|
)
|
|
|
|
# Add criteria for diameter, if specified
|
|
if minDiameter is not None:
|
|
query.criteria.add(
|
|
"tool_diameter.min", adsk.core.ValueInput.createByReal(minDiameter)
|
|
)
|
|
if maxDiameter is not None:
|
|
query.criteria.add(
|
|
"tool_diameter.max", adsk.core.ValueInput.createByReal(maxDiameter)
|
|
)
|
|
|
|
return getToolsByQuery(
|
|
toolLibraryDefinition=toolLibraryDefinition,
|
|
addCriteriaToQueryFunction=addCriteriaToQueryFunction,
|
|
task=task,
|
|
)
|
|
|
|
|
|
def getToolsToMakeHole(
|
|
hole: adsk.cam.RecognizedHole,
|
|
holeAttributes: HoleAttributes,
|
|
drillingToolLibraryDefinition: ToolLibraryDefinition | None,
|
|
millingToolLibraryDefinition: ToolLibraryDefinition | None,
|
|
task: CAMFunctionTaskContext,
|
|
) -> tuple[
|
|
adsk.cam.Tool | None,
|
|
adsk.cam.Tool | None,
|
|
adsk.cam.Tool | None,
|
|
]:
|
|
"""
|
|
Return a set of tools to be used to make the given hole.
|
|
|
|
The tools are returned in the following order:
|
|
- Drill tool (for drilling the hole)
|
|
- Counterbore tool (for counterboring the hole, if applicable)
|
|
- Chamfer tool (for chamfering the hole, if applicable)
|
|
"""
|
|
|
|
# Ratios of the counterbore diameter to define minimum and maximum tools to cut the counterbore
|
|
COUNTERBORE_TOOL_DIAMETER_RATIO_MIN: float = 1.00
|
|
COUNTERBORE_TOOL_DIAMETER_RATIO_MAX: float = 1.25
|
|
|
|
drillTool: adsk.cam.Tool | None = None
|
|
counterboreTool: adsk.cam.Tool | None = None
|
|
chamferTool: adsk.cam.Tool | None = None
|
|
|
|
# Get features of the hole and what tools are needed
|
|
drillingDiameter: float = -1.0
|
|
counterboreRadius: float = -1.0
|
|
needChamfer: bool = False
|
|
|
|
if holeAttributes & HoleAttributes.SIMPLE:
|
|
if holeAttributes & HoleAttributes.CHAMFER:
|
|
# Simple hole with top chamfer
|
|
drillingDiameter = hole.segment(1).bottomDiameter
|
|
needChamfer = True
|
|
else:
|
|
# Simple hole
|
|
drillingDiameter = hole.segment(0).bottomDiameter
|
|
elif holeAttributes & HoleAttributes.COUNTERBORE:
|
|
if holeAttributes & HoleAttributes.CHAMFER:
|
|
# Counterbore hole with top chamfer
|
|
drillingDiameter = hole.segment(3).bottomDiameter
|
|
counterboreRadius = hole.segment(1).bottomDiameter / 2.0
|
|
needChamfer = True
|
|
else:
|
|
# Counterbore hole
|
|
drillingDiameter = hole.segment(2).bottomDiameter
|
|
counterboreRadius = hole.segment(0).bottomDiameter / 2.0
|
|
|
|
# Get drill tool matching the drilling diameter
|
|
with task.makeSubTask(progressRange=(0, 40)) as drillToolTask:
|
|
if drillingDiameter > 0:
|
|
drillingTools: list[adsk.cam.Tool] = getFilteredTools(
|
|
toolLibraryDefinition=drillingToolLibraryDefinition,
|
|
toolType="drill",
|
|
minDiameter=drillingDiameter,
|
|
maxDiameter=drillingDiameter,
|
|
task=drillToolTask,
|
|
)
|
|
drillTool = next(iter(drillingTools), None)
|
|
|
|
# Get counterbore tool matching the counterbore diameter
|
|
with task.makeSubTask(progressRange=(40, 80)) as counterboreToolTask:
|
|
if counterboreRadius > 0:
|
|
minToolDiameter: float = (
|
|
counterboreRadius * COUNTERBORE_TOOL_DIAMETER_RATIO_MIN
|
|
)
|
|
maxToolDiameter: float = (
|
|
counterboreRadius * COUNTERBORE_TOOL_DIAMETER_RATIO_MAX
|
|
)
|
|
|
|
counterboreTools: list[adsk.cam.Tool] = getFilteredTools(
|
|
toolLibraryDefinition=millingToolLibraryDefinition,
|
|
toolType="flat end mill",
|
|
minDiameter=minToolDiameter,
|
|
maxDiameter=maxToolDiameter,
|
|
task=counterboreToolTask,
|
|
)
|
|
counterboreTool = next(iter(counterboreTools), None)
|
|
|
|
# Get chamfer tool if required
|
|
with task.makeSubTask(progressRange=(80, 100)) as chamferToolTask:
|
|
if needChamfer:
|
|
chamferToolDefinition = ToolDefinition(
|
|
name="6mm x 45 Engraving Tool",
|
|
toolLibraryDefinition=ToolLibraryDefinition(
|
|
toolLibrary=millingToolLibraryDefinition.toolLibrary
|
|
if millingToolLibraryDefinition
|
|
else None,
|
|
name=millingToolLibraryDefinition.name
|
|
if millingToolLibraryDefinition
|
|
else None,
|
|
type=millingToolLibraryDefinition.type
|
|
if millingToolLibraryDefinition
|
|
else None,
|
|
),
|
|
)
|
|
chamferTool = getToolByName(
|
|
toolDefinition=chamferToolDefinition,
|
|
task=chamferToolTask,
|
|
)
|
|
|
|
# Return the tools in the order: drilling, counterbore, chamfer
|
|
return drillTool, counterboreTool, chamferTool
|
|
|
|
|
|
# Hole and Pocket Recognition helpers
|
|
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: bool = False
|
|
for boundary in pocket.boundaries:
|
|
for segment in boundary:
|
|
if segment.classType() == adsk.core.Circle3D.classType():
|
|
isCircleFound = True
|
|
else:
|
|
return False
|
|
|
|
return isCircleFound
|
|
|
|
|
|
def recognizePockets(
|
|
collections: list[adsk.fusion.BRepBody | adsk.fusion.Occurrence],
|
|
parentTask: CAMFunctionTaskContext,
|
|
) -> list[adsk.cam.RecognizedPocket]:
|
|
"""
|
|
Recognize pockets (from top Z view) in bodies.
|
|
Return a list of recognized pockets.
|
|
"""
|
|
|
|
POCKET_SEARCH_VECTOR = adsk.core.Vector3D.create(0, 0, -1)
|
|
|
|
bodies: list[adsk.fusion.BRepBody] = []
|
|
|
|
for collection in collections:
|
|
if isinstance(collection, adsk.fusion.BRepBody):
|
|
bodies.append(collection)
|
|
elif isinstance(collection, adsk.fusion.Occurrence):
|
|
# If the occurrence has a BRepBody, add it to the list
|
|
bRepBodies: adsk.fusion.BRepBodies = collection.bRepBodies
|
|
bodies.extend([bRepBodies.item(i) for i in range(bRepBodies.count)])
|
|
|
|
pockets: list[adsk.cam.RecognizedPocket] = []
|
|
if len(bodies) == 0:
|
|
return pockets
|
|
|
|
with parentTask.makeSubTask(numberOfTasks=len(bodies)) as task:
|
|
for i, body in enumerate(bodies):
|
|
with task.makeSubTaskByIndex(index=i) as bodyTask:
|
|
# Recognize pockets in the body using the search vector
|
|
#
|
|
# This can be a long operation, so we run it asynchronously and
|
|
# indeterminately increment the progress bar
|
|
async def worker() -> adsk.cam.RecognizedPockets:
|
|
return adsk.cam.RecognizedPocket.recognizePockets(
|
|
body,
|
|
POCKET_SEARCH_VECTOR,
|
|
)
|
|
|
|
recognizedPockets = bodyTask.runAsyncWorker(worker=worker)
|
|
|
|
for pocket in recognizedPockets:
|
|
if isCircularPocket(pocket):
|
|
pass # ignore circular pockets
|
|
else:
|
|
pockets.append(pocket)
|
|
return pockets
|
|
|
|
|
|
def recognizeHoleGroups(
|
|
bodies: list[adsk.fusion.BRepBody],
|
|
task: CAMFunctionTaskContext,
|
|
) -> adsk.cam.RecognizedHoleGroups:
|
|
"""
|
|
Recognize hole groups in 'bodies'.
|
|
Returns the collection of recognized hole groups.
|
|
"""
|
|
|
|
# This can be a long operation, so we run it asynchronously and
|
|
# indeterminately increment the progress bar
|
|
async def worker() -> adsk.cam.RecognizedHoleGroups:
|
|
recognizedHolesInput = adsk.cam.RecognizedHolesInput.create()
|
|
return adsk.cam.RecognizedHoleGroup.recognizeHoleGroupsWithInput(
|
|
bodies=bodies,
|
|
input=recognizedHolesInput,
|
|
)
|
|
|
|
return task.runAsyncWorker(worker=worker)
|
|
|
|
|
|
def classifyHoleGroup(
|
|
holeGroup: adsk.cam.RecognizedHoleGroup,
|
|
) -> HoleAttributes:
|
|
"""
|
|
Examine the hole group and return a set of attributes that describe its
|
|
profile and depth.
|
|
|
|
Returns HoleAttributes(0) if the profile is not recognized.
|
|
"""
|
|
|
|
referenceHole: adsk.cam.RecognizedHole = holeGroup.item(0)
|
|
includeHole: bool = True
|
|
|
|
if referenceHole.isThrough:
|
|
attributes: HoleAttributes = HoleAttributes.THROUGH
|
|
|
|
match referenceHole.segmentCount:
|
|
case 1: # Simple through hole
|
|
attributes |= HoleAttributes.SIMPLE
|
|
case 2: # Simple through hole with top chamfer
|
|
attributes |= HoleAttributes.SIMPLE | HoleAttributes.CHAMFER
|
|
case 3: # Counterbore through hole
|
|
attributes |= HoleAttributes.COUNTERBORE
|
|
case 4: # Counterbore through hole with top chamfer
|
|
attributes |= HoleAttributes.COUNTERBORE | HoleAttributes.CHAMFER
|
|
case _:
|
|
includeHole = False # Unrecognized hole profile
|
|
else:
|
|
attributes: HoleAttributes = HoleAttributes.BLIND
|
|
|
|
match referenceHole.segmentCount:
|
|
case 2: # Simple blind hole
|
|
attributes |= HoleAttributes.SIMPLE
|
|
case 3: # Simple blind hole with top chamfer
|
|
attributes |= HoleAttributes.SIMPLE | HoleAttributes.CHAMFER
|
|
case 4: # Counterbore blind hole
|
|
attributes |= HoleAttributes.COUNTERBORE
|
|
case 5: # Counterbore blind hole with top chamfer
|
|
attributes |= HoleAttributes.COUNTERBORE | HoleAttributes.CHAMFER
|
|
case _:
|
|
includeHole = False # Unrecognized hole profile
|
|
|
|
return attributes if includeHole else HoleAttributes(0)
|
|
|
|
|
|
def classifyHoleGroups(
|
|
holeGroups: adsk.cam.RecognizedHoleGroups,
|
|
task: CAMFunctionTaskContext,
|
|
) -> list[tuple[adsk.cam.RecognizedHoleGroup, HoleAttributes]]:
|
|
"""
|
|
Classify the hole groups and return a list of tuples containing the hole
|
|
group and its attributes.
|
|
|
|
Each tuple contains:
|
|
- adsk.cam.RecognizedHoleGroup: The recognized hole group
|
|
- HoleAttributes: The attributes of the hole group (SIMPLE, COUNTERBORE,
|
|
THROUGH, BLIND, CHAMFER)
|
|
"""
|
|
|
|
classifiedHoles: list[tuple[adsk.cam.RecognizedHoleGroup, HoleAttributes]] = []
|
|
|
|
for i, holeGroup in enumerate(holeGroups):
|
|
task.updateTaskProgress(i, holeGroups.count)
|
|
attributes: HoleAttributes = classifyHoleGroup(holeGroup)
|
|
if attributes != HoleAttributes(0):
|
|
classifiedHoles.append((holeGroup, attributes))
|
|
|
|
return classifiedHoles
|
|
|
|
|
|
def getAppearanceByColorName(
|
|
design: adsk.fusion.Design,
|
|
name: str,
|
|
color_code: tuple[int],
|
|
) -> adsk.core.Appearance | None:
|
|
"""Return a Fusion appearance from a color name. color_code is a tuple that defines the RGB code (0-255). Returns None if appearance cannot be created because of invalid color"""
|
|
|
|
# Look for the color
|
|
appearance: adsk.core.Appearance = design.appearances.itemByName(name)
|
|
|
|
if not appearance:
|
|
# first check if the color is valid
|
|
try:
|
|
color = adsk.core.Color.create(
|
|
color_code[0], color_code[1], color_code[2], 255
|
|
)
|
|
except Exception:
|
|
return None
|
|
|
|
# get the application
|
|
app: adsk.core.Application = design.parentDocument.parent
|
|
|
|
# Get the existing Red appearance.
|
|
fusionMaterials: adsk.core.MaterialLibrary = app.materialLibraries.itemById(
|
|
"BA5EE55E-9982-449B-9D66-9F036540E140"
|
|
)
|
|
redAppearance: adsk.core.Appearance = fusionMaterials.appearances.itemById(
|
|
"Prism-093"
|
|
)
|
|
# Copy it to the design, giving it a new name.
|
|
appearance: adsk.core.Appearance = design.appearances.addByCopy(
|
|
redAppearance, name
|
|
)
|
|
# Change the color of the default appearance to the provided one.
|
|
theColor: adsk.core.ColorProperty = appearance.appearanceProperties.itemById(
|
|
"opaque_albedo"
|
|
)
|
|
theColor.value = color
|
|
|
|
return appearance
|
|
|
|
|
|
def colorFaces(
|
|
app: adsk.core.Application,
|
|
faces: list[adsk.fusion.BRepFace],
|
|
appearance: adsk.core.Appearance,
|
|
task: CAMFunctionTaskContext,
|
|
):
|
|
"""
|
|
Color the given faces with the given appearance.
|
|
"""
|
|
|
|
if len(faces) == 0:
|
|
# If no faces are provided, return early
|
|
return
|
|
|
|
for i, face in enumerate(faces):
|
|
task.updateTaskProgress(i, len(faces))
|
|
|
|
# Set the appearance of the face
|
|
face.appearance = appearance
|
|
|
|
# Refresh the UI to apply the changes
|
|
app.activeViewport.refresh()
|
|
|
|
|
|
# Milling holes in a setup
|
|
def millHoleGroup(
|
|
setup: adsk.cam.Setup,
|
|
holeGroup: adsk.cam.RecognizedHoleGroup,
|
|
holeAttributes: HoleAttributes,
|
|
drillingToolLibraryDefinition: ToolLibraryDefinition | None,
|
|
millingToolLibraryDefinition: ToolLibraryDefinition | None,
|
|
task: CAMFunctionTaskContext,
|
|
) -> list[adsk.cam.Operation]:
|
|
"""
|
|
Create operations in the given setup for the holes in the given group.
|
|
Operations will be created based on the hole attributes.
|
|
Tools will be picked automatically from the given tool libraries.
|
|
"""
|
|
|
|
assert holeAttributes & (HoleAttributes.SIMPLE | HoleAttributes.COUNTERBORE)
|
|
|
|
referenceHole: adsk.cam.RecognizedHole = holeGroup.item(0)
|
|
|
|
with task.makeSubTask(message="Finding tools", progressRange=(0, 40)) as toolTask:
|
|
drill, counterbore, chamfer = getToolsToMakeHole(
|
|
hole=referenceHole,
|
|
holeAttributes=holeAttributes,
|
|
drillingToolLibraryDefinition=drillingToolLibraryDefinition,
|
|
millingToolLibraryDefinition=millingToolLibraryDefinition,
|
|
task=toolTask,
|
|
)
|
|
|
|
operations: list[adsk.cam.Operation] = []
|
|
|
|
numberOfOperations: int = sum(
|
|
[
|
|
True, # Always drill the hole
|
|
bool(holeAttributes & HoleAttributes.COUNTERBORE),
|
|
bool(holeAttributes & HoleAttributes.CHAMFER),
|
|
]
|
|
)
|
|
|
|
opTask = task.makeSubTask(
|
|
message="Creating operations",
|
|
progressRange=(40, 100),
|
|
numberOfTasks=numberOfOperations,
|
|
)
|
|
with opTask:
|
|
# Drill hole at the bottom
|
|
opTask.updateTaskProgress(len(operations), numberOfOperations)
|
|
drillOp: adsk.cam.Operation = createDrillOperation(
|
|
setup=setup,
|
|
tool=drill,
|
|
holeGroup=holeGroup,
|
|
drillTipThroughBottom=bool(holeAttributes & HoleAttributes.THROUGH),
|
|
)
|
|
operations.append(drillOp)
|
|
|
|
if holeAttributes & HoleAttributes.COUNTERBORE:
|
|
# If the hole is a counterbore, create a counterbore operation for the
|
|
# larger diameter hole above it
|
|
opTask.updateTaskProgress(len(operations), numberOfOperations)
|
|
counterboreOp: adsk.cam.Operation = createCounterboreMillOperation(
|
|
setup=setup,
|
|
tool=counterbore,
|
|
holeGroup=holeGroup,
|
|
)
|
|
operations.append(counterboreOp)
|
|
|
|
if holeAttributes & HoleAttributes.CHAMFER:
|
|
# If the hole has a top chamfer, create a chamfer operation
|
|
opTask.updateTaskProgress(len(operations), numberOfOperations)
|
|
chamferOp: adsk.cam.Operation = createChamferOperation(
|
|
setup=setup,
|
|
tool=chamfer,
|
|
holeGroup=holeGroup,
|
|
)
|
|
operations.append(chamferOp)
|
|
|
|
return operations
|
|
|
|
|
|
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: adsk.fusion.SketchLines = sketch.sketchCurves.sketchLines
|
|
line: adsk.fusion.SketchLine = lines.addByTwoPoints(startPoint, endPoint)
|
|
return line
|
|
|
|
|
|
def sketchTwoPointArc(
|
|
sketch: adsk.fusion.Sketch,
|
|
centerPoint: adsk.core.Point3D,
|
|
startPoint: adsk.core.Point3D,
|
|
sweepAngle: float,
|
|
normal: adsk.core.Vector3D,
|
|
) -> adsk.fusion.SketchArc:
|
|
"""Sketch an arc based on center, radius and sweep angle"""
|
|
arcs: adsk.fusion.SketchArcs = sketch.sketchCurves.sketchArcs
|
|
arc: adsk.fusion.SketchArc = arcs.addByCenterStartSweep(
|
|
centerPoint, startPoint, sweepAngle
|
|
)
|
|
arcNormal: adsk.core.Vector3D = 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 sketchCircles(
|
|
sketch: adsk.fusion.Sketch, centerPoint: adsk.core.Point3D, radius: float
|
|
) -> adsk.fusion.SketchCircle:
|
|
"""Create a circle based on the points"""
|
|
circles: adsk.fusion.SketchCircles = sketch.sketchCurves.sketchCircles
|
|
circle: adsk.fusion.SketchCircle = circles.addByCenterRadius(centerPoint, radius)
|
|
return circle
|
|
|
|
|
|
def sketchNurbsCurve(sketch: adsk.fusion.Sketch, nurbsCurve: adsk.core.NurbsCurve3D):
|
|
"""Sketch a spline"""
|
|
splines = sketch.sketchCurves.sketchFixedSplines.addByNurbsCurve(nurbsCurve)
|
|
return splines
|
|
|
|
|
|
def drawSketchCurves(
|
|
sketch: adsk.fusion.Sketch, boundary: list[adsk.core.Curve3D]
|
|
) -> None:
|
|
"""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)
|
|
ellipse3D = adsk.core.Ellipse3D.cast(segment)
|
|
nurbsCurve3D = adsk.core.NurbsCurve3D.cast(segment)
|
|
ellipticalArc3D = adsk.core.EllipticalArc3D.cast(segment)
|
|
|
|
if line3D:
|
|
startPoint: adsk.core.Point3D = line3D.startPoint
|
|
endPoint: adsk.core.Point3D = line3D.endPoint
|
|
sketchLine(sketch, startPoint, endPoint)
|
|
elif arc3D:
|
|
startPoint: adsk.core.Point3D = arc3D.startPoint
|
|
centerPoint: adsk.core.Point3D = arc3D.center
|
|
sweepAngle: float = arc3D.endAngle
|
|
normal: adsk.core.Vector3D = arc3D.normal
|
|
sketchTwoPointArc(sketch, centerPoint, startPoint, sweepAngle, normal)
|
|
elif circle3D:
|
|
centerPoint: adsk.core.Point3D = circle3D.center
|
|
radius: float = circle3D.radius
|
|
sketchCircles(sketch, centerPoint, radius)
|
|
elif ellipse3D:
|
|
nurbsCurve = ellipse3D.asNurbsCurve
|
|
sketchNurbsCurve(sketch, nurbsCurve)
|
|
elif nurbsCurve3D:
|
|
sketchNurbsCurve(sketch, nurbsCurve3D)
|
|
elif ellipticalArc3D:
|
|
nurbsCurve = ellipticalArc3D.asNurbsCurve
|
|
sketchNurbsCurve(sketch, nurbsCurve)
|
|
else:
|
|
classType = segment.classType()
|
|
raise Exception("Unsupported curve type: " + classType)
|
|
|
|
|
|
def sketchPocketBoundary(
|
|
component: adsk.fusion.Component, pocket: adsk.cam.RecognizedPocket
|
|
):
|
|
"""Create a sketch from the recognized pocket boundary under a component tree"""
|
|
sketches = component.sketches
|
|
sketch: adsk.fusion.Sketch = sketches.add(component.xYConstructionPlane)
|
|
sketch.isVisible = False
|
|
sketch.isComputeDeferred = True
|
|
for boundary in pocket.boundaries:
|
|
drawSketchCurves(sketch, boundary)
|
|
for island in pocket.islands:
|
|
drawSketchCurves(sketch, island)
|
|
sketch.isComputeDeferred = False
|
|
return sketch
|
|
|
|
|
|
def millPocket(
|
|
operation_type: str,
|
|
pocket: adsk.cam.RecognizedPocket,
|
|
setup: adsk.cam.Setup,
|
|
tool: adsk.cam.Tool,
|
|
sketch: adsk.fusion.Sketch,
|
|
):
|
|
input: adsk.cam.OperationInput = setup.operations.createInput(operation_type)
|
|
if tool:
|
|
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"
|
|
)
|
|
|
|
# Apply the sketch boundary to the operation input
|
|
pocketSelection: adsk.cam.CadContours2dParameterValue = input.parameters.itemByName(
|
|
"pockets"
|
|
).value
|
|
chains: adsk.cam.CurveSelections = pocketSelection.getCurveSelections()
|
|
chain: adsk.cam.SketchSelection = chains.createNewSketchSelection()
|
|
chain.inputGeometry = [sketch]
|
|
# chain.loopType = adsk.cam.LoopTypes.OnlyOutsideLoops
|
|
chain.loopType = adsk.cam.LoopTypes.AllLoops
|
|
chain.sideType = adsk.cam.SideTypes.AlwaysInsideSideType
|
|
# chain.sideType = adsk.cam.SideTypes.StartInsideSideType
|
|
pocketSelection.applyCurveSelections(chains)
|
|
|
|
# Add to the setup
|
|
op: adsk.cam.OperationBase = setup.operations.add(input)
|
|
return [op]
|
|
|
|
|
|
def millPockets(
|
|
operation_type: str,
|
|
pockets: list[adsk.cam.RecognizedPocket],
|
|
setup: adsk.cam.Setup,
|
|
tool: adsk.cam.Tool,
|
|
task: CAMFunctionTaskContext,
|
|
):
|
|
"""create operation in the given pockets, in a given setup with given tool"""
|
|
modelItem: adsk.fusion.BRepBody | adsk.fusion.Occurrence = setup.models.item(0)
|
|
component = None
|
|
if type(modelItem) is adsk.fusion.BRepBody:
|
|
component = modelItem.parentComponent
|
|
elif type(modelItem) is adsk.fusion.Occurrence:
|
|
component = modelItem.component
|
|
else:
|
|
CAMFunctionContext.warn(
|
|
message="Warning milling pockets:",
|
|
warning="Component is not a BRepBody or a Occurrence.",
|
|
)
|
|
return
|
|
|
|
operations: list[adsk.cam.Operation] = []
|
|
|
|
for i, pocket in enumerate(pockets):
|
|
task.updateTaskProgress(i, len(pockets))
|
|
sketch = sketchPocketBoundary(component, pocket)
|
|
ops = millPocket(operation_type, pocket, setup, tool, sketch)
|
|
operations.extend(ops)
|
|
|
|
return operations
|
|
|
|
|
|
# Additive helpers
|
|
def getDefaultOrientationStudyParameters(
|
|
additiveMachineType: adsk.cam.AdditiveTechnologies,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Return the default parameters for a new additive orientation study in a
|
|
setup using a machine of the given additive type.
|
|
"""
|
|
|
|
# Determine if the default values of parameters should be SLA style as
|
|
# opposed to FFF style
|
|
useSLAStyleParams = additiveMachineType in [
|
|
adsk.cam.AdditiveTechnologies.SLATechnology,
|
|
adsk.cam.AdditiveTechnologies.BinderJettingTechnology,
|
|
adsk.cam.AdditiveTechnologies.MFJTechnology,
|
|
adsk.cam.AdditiveTechnologies.SLSTechnology,
|
|
]
|
|
|
|
criticalAngle = "35 deg" if useSLAStyleParams else "45 deg"
|
|
distanceToPlatform = "7 mm" if useSLAStyleParams else "0 mm"
|
|
frameWidth = "0 mm" if useSLAStyleParams else "5 mm"
|
|
ceilingClearance = "0 mm" if useSLAStyleParams else "5 mm"
|
|
rankingSupportVolume = "'2'" if useSLAStyleParams else "'10'"
|
|
rankingSupportArea = "'10'" if useSLAStyleParams else "'0'"
|
|
rankingBoundingBoxVolume = "'0'" if useSLAStyleParams else "'2'"
|
|
rankingPartHeight = "'0'" if useSLAStyleParams else "'6'"
|
|
rankingCOGHeight = "'0'" if useSLAStyleParams else "'6'"
|
|
|
|
orientation_params = {
|
|
"optimizeOrientationSmallestRotation": "180 deg",
|
|
"optimizeOrientationUsePreciseCalculation": "true",
|
|
"optimizeOrientationCriticalAngle": criticalAngle,
|
|
"optimizeOrientationDistanceToPlatform": distanceToPlatform,
|
|
"optimizeOrientationMoveToCenter": "true",
|
|
"optimizeOrientationFrameWidth": frameWidth,
|
|
"optimizeOrientationCeilingClearance": ceilingClearance,
|
|
"optimizeOrientationRankingSupportVolume": rankingSupportVolume,
|
|
"optimizeOrientationRankingSupportArea": rankingSupportArea,
|
|
"optimizeOrientationRankingBoundingBoxVolume": rankingBoundingBoxVolume,
|
|
"optimizeOrientationRankingPartHeight": rankingPartHeight,
|
|
"optimizeOrientationRankingCOGHeight": rankingCOGHeight,
|
|
}
|
|
|
|
return orientation_params
|
|
|
|
|
|
def createAutomaticOrientationStudy(
|
|
cam: adsk.cam.CAM,
|
|
setup: adsk.cam.Setup,
|
|
orientationName: str | None,
|
|
camParameters: dict[str, Any] | None,
|
|
task: CAMFunctionTaskContext,
|
|
) -> list[adsk.cam.Operation]:
|
|
"""
|
|
Create an automatic orientation study for each model occurrence in the
|
|
given setup.
|
|
|
|
If 'orientationName' is not an empty string, it will be used as the prefix
|
|
of the new orientation studies (as "<name>: <occurrence_name>"). Otherwise,
|
|
the prefix will be "Automatic Orientation: ".
|
|
"""
|
|
|
|
# Collect the created orientations for this setup
|
|
orientations: list[adsk.cam.Operation] = []
|
|
|
|
# Activate the setup to find the active manufacturing model
|
|
setup.activate()
|
|
activeMfgModel = None
|
|
for mfgModel in cam.manufacturingModels:
|
|
if mfgModel.isActive:
|
|
activeMfgModel = mfgModel
|
|
break
|
|
|
|
if activeMfgModel is None:
|
|
# Error if there is no active manufacturing model
|
|
CAMFunctionContext.fail(
|
|
message="Error creating automatic orientation study:",
|
|
error=f"No manufacturing model is active for '{setup.name}'.",
|
|
)
|
|
return orientations
|
|
|
|
task.updatePercentageProgress(10)
|
|
|
|
# Find all bodies and components for each model in the setup
|
|
setupBodies = []
|
|
setupComps = []
|
|
models = setup.models
|
|
for i in range(models.count):
|
|
model_item = models.item(i)
|
|
if model_item.objectType == adsk.fusion.Occurrence.classType():
|
|
setupComps.append(model_item.component)
|
|
validOccs = getValidOccurrences(model_item)
|
|
for occ in validOccs:
|
|
for k in range(occ.bRepBodies.count):
|
|
setupBodies.append(occ.bRepBodies.item(k))
|
|
else:
|
|
setupBodies.append(model_item)
|
|
setupComps.append(model_item.parentComponent)
|
|
|
|
task.updatePercentageProgress(30)
|
|
|
|
# Find all occurrences from the mfg model that reference a component or a body in the setup
|
|
occs = []
|
|
mfgModelOccs = getValidOccurrences(activeMfgModel.occurrence)
|
|
for occ in mfgModelOccs:
|
|
if occ.component in setupComps:
|
|
occs.append(occ)
|
|
continue
|
|
hasMatchedBody = any(
|
|
b in setupBodies
|
|
for b in [occ.bRepBodies.item(j) for j in range(occ.bRepBodies.count)]
|
|
)
|
|
if hasMatchedBody:
|
|
occs.append(occ)
|
|
|
|
if len(occs) == 0:
|
|
CAMFunctionContext.fail(
|
|
message="Error creating automatic orientation study:",
|
|
error=f"No component has been added to the scene in '{setup.name}'.",
|
|
)
|
|
return orientations
|
|
|
|
task.updatePercentageProgress(40)
|
|
|
|
# Get the machine from the setup
|
|
machine = setup.machine
|
|
if machine is None:
|
|
CAMFunctionContext.fail(
|
|
message="Error creating automatic orientation study:",
|
|
error=f"No machine found for '{setup.name}'.",
|
|
)
|
|
return orientations
|
|
|
|
additiveType = machine.capabilities.additiveTechnology
|
|
if additiveType == adsk.cam.AdditiveTechnologies.NATechnology:
|
|
CAMFunctionContext.fail(
|
|
message="Error creating automatic orientation study:",
|
|
error=f"Machine '{machine.name}' is not an additive machine for '{setup.name}'.",
|
|
)
|
|
return orientations
|
|
|
|
occsTask = task.makeSubTask(
|
|
progressRange=(40, 100),
|
|
numberOfTasks=len(occs),
|
|
)
|
|
with occsTask:
|
|
# Create the automatic orientation study for each occurrence in the
|
|
# active manufacturing model
|
|
for i, occ in enumerate(occs):
|
|
studyTask = occsTask.makeSubTaskByIndex(
|
|
index=i,
|
|
message=f"Creating orientation for '{occ.name}'",
|
|
numberOfTasks=2,
|
|
)
|
|
with studyTask:
|
|
operationInput = setup.operations.createInput("automatic_orientation")
|
|
operationInput.isAutoCalculating = False
|
|
orientationTarget = operationInput.parameters.itemByName(
|
|
"optimizeOrientationTarget"
|
|
)
|
|
orientationTarget.value.value = [occ]
|
|
|
|
# Use provided name for the new orientation, or default to
|
|
# "Automatic Orientation: <occurrence name>"
|
|
if isNonEmptyString(orientationName):
|
|
operationInput.displayName = orientationName
|
|
else:
|
|
operationInput.displayName = f"Automatic Orientation: {occ.name}"
|
|
|
|
# Get the default values for parameters
|
|
orientation_params = getDefaultOrientationStudyParameters(additiveType)
|
|
|
|
# Override default values with CAMParameters if provided
|
|
if camParameters:
|
|
orientation_params.update(camParameters)
|
|
|
|
# Set parameters for the orientation study
|
|
with studyTask.makeSubTaskByIndex(index=0) as paramTask:
|
|
warningParams = setCAMParameters(
|
|
inputParameters=orientation_params,
|
|
camParameters=operationInput.parameters,
|
|
task=paramTask,
|
|
)
|
|
|
|
async def worker() -> adsk.cam.Operation:
|
|
return setup.operations.add(operationInput)
|
|
|
|
with studyTask.makeSubTaskByIndex(index=1) as createTask:
|
|
orientation = createTask.runAsyncWorker(worker=worker)
|
|
|
|
orientations.append(orientation)
|
|
|
|
# Base success message, based on the name inputs
|
|
if isNonEmptyString(orientationName):
|
|
successMessage = f"Created an automatic orientation study named '{orientation.name}' for the body '{occ.name}' in '{setup.name}'"
|
|
else:
|
|
successMessage = f"Created an automatic orientation study named '{orientation.name}' in '{setup.name}'"
|
|
|
|
# Log warnings or success
|
|
if len(warningParams) > 0:
|
|
CAMFunctionContext.warn(
|
|
message=f"{successMessage} with warnings:",
|
|
warning=f"Cannot set parameter(s): {getListAsString(warningParams)}. It may imply that the parameter(s) do not exist for orientation studies.",
|
|
)
|
|
else:
|
|
# If the orientation name is different from the actual name
|
|
# of the created orientation, interrupt the execution
|
|
if isNonEmptyString(orientationName) and orientation.name != orientationName:
|
|
CAMFunctionContext.blockFutureExecution(
|
|
warning=f"{successMessage} (instead of {orientationName})."
|
|
)
|
|
else:
|
|
CAMFunctionContext.succeed(
|
|
message=f"{successMessage}."
|
|
)
|
|
|
|
return orientations
|