| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | __title__ = "CAM Slot Operation" |
| | __author__ = "russ4262 (Russell Johnson)" |
| | __url__ = "https://www.freecad.org" |
| | __doc__ = "Class and implementation of Slot operation." |
| | __contributors__ = "" |
| |
|
| | import FreeCAD |
| | from PySide import QtCore |
| | import Path |
| | import Path.Op.Base as PathOp |
| | import PathScripts.PathUtils as PathUtils |
| | import math |
| |
|
| | |
| | from lazy_loader.lazy_loader import LazyLoader |
| |
|
| | Part = LazyLoader("Part", globals(), "Part") |
| | Arcs = LazyLoader("draftgeoutils.arcs", globals(), "draftgeoutils.arcs") |
| | if FreeCAD.GuiUp: |
| | FreeCADGui = LazyLoader("FreeCADGui", globals(), "FreeCADGui") |
| |
|
| |
|
| | translate = FreeCAD.Qt.translate |
| |
|
| |
|
| | if False: |
| | Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) |
| | Path.Log.trackModule(Path.Log.thisModule()) |
| | else: |
| | Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) |
| |
|
| |
|
| | class ObjectSlot(PathOp.ObjectOp): |
| | """Proxy object for Slot operation.""" |
| |
|
| | def opFeatures(self, obj): |
| | """opFeatures(obj) ... return all standard features""" |
| | return ( |
| | PathOp.FeatureTool |
| | | PathOp.FeatureDepths |
| | | PathOp.FeatureHeights |
| | | PathOp.FeatureStepDown |
| | | PathOp.FeatureCoolant |
| | | PathOp.FeatureBaseVertexes |
| | | PathOp.FeatureBaseEdges |
| | | PathOp.FeatureBaseFaces |
| | ) |
| |
|
| | def initOperation(self, obj): |
| | """initOperation(obj) ... Initialize the operation by |
| | managing property creation and property editor status.""" |
| | self.propertiesReady = False |
| |
|
| | self.initOpProperties(obj) |
| |
|
| | |
| | if Path.Log.getLevel(Path.Log.thisModule()) != 4: |
| | obj.setEditorMode("ShowTempObjects", 2) |
| |
|
| | if not hasattr(obj, "DoNotSetDefaultValues"): |
| | self.opSetEditorModes(obj) |
| |
|
| | def initOpProperties(self, obj, warn=False): |
| | """initOpProperties(obj) ... create operation specific properties""" |
| | Path.Log.track() |
| | self.addNewProps = list() |
| |
|
| | for prtyp, nm, grp, tt in self.opPropertyDefinitions(): |
| | if not hasattr(obj, nm): |
| | obj.addProperty(prtyp, nm, grp, tt) |
| | self.addNewProps.append(nm) |
| |
|
| | |
| | if len(self.addNewProps) > 0: |
| | enumDict = ObjectSlot.propertyEnumerations(dataType="raw") |
| | for k, tupList in enumDict.items(): |
| | if k in self.addNewProps: |
| | setattr(obj, k, [t[1] for t in tupList]) |
| |
|
| | if warn: |
| | newPropMsg = translate("CAM_Slot", "New property added to") |
| | newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + ". " |
| | newPropMsg += translate("CAM_Slot", "Check default value(s).") |
| | FreeCAD.Console.PrintWarning(newPropMsg + "\n") |
| |
|
| | self.propertiesReady = True |
| |
|
| | def opPropertyDefinitions(self): |
| | """opPropertyDefinitions(obj) ... Store operation specific properties""" |
| |
|
| | return [ |
| | ( |
| | "App::PropertyBool", |
| | "ShowTempObjects", |
| | "Debug", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Show the temporary toolpath construction objects when module is in DEBUG mode.", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyVectorDistance", |
| | "CustomPoint1", |
| | "Slot", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", "Enter custom start point for slot toolpath." |
| | ), |
| | ), |
| | ( |
| | "App::PropertyVectorDistance", |
| | "CustomPoint2", |
| | "Slot", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", "Enter custom end point for slot toolpath." |
| | ), |
| | ), |
| | ( |
| | "App::PropertyEnumeration", |
| | "CutPattern", |
| | "Slot", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Set the geometric clearing pattern to use for the operation.", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyDistance", |
| | "ExtendPathStart", |
| | "Slot", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Positive extends the beginning of the toolpath, negative shortens.", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyDistance", |
| | "ExtendPathEnd", |
| | "Slot", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Positive extends the end of the toolpath, negative shortens.", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyEnumeration", |
| | "LayerMode", |
| | "Slot", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Complete the operation in a single pass at depth, or multiple passes to final depth.", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyEnumeration", |
| | "PathOrientation", |
| | "Slot", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Choose the toolpath orientation with regard to the feature(s) selected.", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyEnumeration", |
| | "Reference1", |
| | "Slot", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Choose what point to use on the first selected feature.", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyEnumeration", |
| | "Reference2", |
| | "Slot", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Choose what point to use on the second selected feature.", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyDistance", |
| | "ExtendRadius", |
| | "Slot", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "For arcs/circular edges, offset the radius for the toolpath.", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyBool", |
| | "ReverseDirection", |
| | "Slot", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Enable to reverse the cut direction of the slot toolpath.", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyVectorDistance", |
| | "StartPoint", |
| | "Start Point", |
| | QtCore.QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "The custom start point for the toolpath of this operation", |
| | ), |
| | ), |
| | ( |
| | "App::PropertyBool", |
| | "UseStartPoint", |
| | "Start Point", |
| | QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"), |
| | ), |
| | ] |
| |
|
| | @classmethod |
| | def propertyEnumerations(self, dataType="data"): |
| | """propertyEnumerations(dataType="data")... return property enumeration lists of specified dataType. |
| | Args: |
| | dataType = 'data', 'raw', 'translated' |
| | Notes: |
| | 'data' is list of internal string literals used in code |
| | 'raw' is list of (translated_text, data_string) tuples |
| | 'translated' is list of translated string literals |
| | """ |
| | Path.Log.track() |
| |
|
| | enums = { |
| | "CutPattern": [ |
| | (translate("CAM_Slot", "Line"), "Line"), |
| | (translate("CAM_Slot", "ZigZag"), "ZigZag"), |
| | ], |
| | "LayerMode": [ |
| | (translate("CAM_Slot", "Single-pass"), "Single-pass"), |
| | (translate("CAM_Slot", "Multi-pass"), "Multi-pass"), |
| | ], |
| | "PathOrientation": [ |
| | (translate("CAM_Slot", "Start to End"), "Start to End"), |
| | (translate("CAM_Slot", "Perpendicular"), "Perpendicular"), |
| | ], |
| | "Reference1": [ |
| | (translate("CAM_Slot", "Center of Mass"), "Center of Mass"), |
| | ( |
| | translate("CAM_Slot", "Center of Bounding Box"), |
| | "Center of BoundBox", |
| | ), |
| | (translate("CAM_Slot", "Lowest Point"), "Lowest Point"), |
| | (translate("CAM_Slot", "Highest Point"), "Highest Point"), |
| | (translate("CAM_Slot", "Long Edge"), "Long Edge"), |
| | (translate("CAM_Slot", "Short Edge"), "Short Edge"), |
| | (translate("CAM_Slot", "Vertex"), "Vertex"), |
| | ], |
| | "Reference2": [ |
| | (translate("CAM_Slot", "Center of Mass"), "Center of Mass"), |
| | ( |
| | translate("CAM_Slot", "Center of Bounding Box"), |
| | "Center of BoundBox", |
| | ), |
| | (translate("CAM_Slot", "Lowest Point"), "Lowest Point"), |
| | (translate("CAM_Slot", "Highest Point"), "Highest Point"), |
| | (translate("CAM_Slot", "Vertex"), "Vertex"), |
| | ], |
| | } |
| |
|
| | if dataType == "raw": |
| | return enums |
| |
|
| | data = list() |
| | idx = 0 if dataType == "translated" else 1 |
| |
|
| | Path.Log.debug(enums) |
| |
|
| | for k, v in enumerate(enums): |
| | data.append((v, [tup[idx] for tup in enums[v]])) |
| | Path.Log.debug(data) |
| |
|
| | return data |
| |
|
| | def opPropertyDefaults(self, obj, job): |
| | """opPropertyDefaults(obj, job) ... returns a dictionary of default values |
| | for the operation's properties.""" |
| | defaults = { |
| | "CustomPoint1": FreeCAD.Vector(0, 0, 0), |
| | "ExtendPathStart": 0, |
| | "Reference1": "Center of Mass", |
| | "CustomPoint2": FreeCAD.Vector(0, 0, 0), |
| | "ExtendPathEnd": 0, |
| | "Reference2": "Center of Mass", |
| | "LayerMode": "Multi-pass", |
| | "CutPattern": "ZigZag", |
| | "PathOrientation": "Start to End", |
| | "ExtendRadius": 0, |
| | "ReverseDirection": False, |
| | |
| | "ShowTempObjects": False, |
| | } |
| |
|
| | return defaults |
| |
|
| | def getActiveEnumerations(self, obj): |
| | """getActiveEnumerations(obj) ... |
| | Method returns dictionary of property enumerations based on |
| | active conditions in the operation.""" |
| | ENUMS = dict() |
| | for prop, data in ObjectSlot.propertyEnumerations(): |
| | ENUMS[prop] = data |
| | if hasattr(obj, "Base"): |
| | if obj.Base: |
| | |
| | subsList = obj.Base[0][1] |
| | subCnt = len(subsList) |
| | if subCnt == 1: |
| | |
| | ENUMS["Reference1"] = self._makeReference1Enumerations(subsList[0], True) |
| | elif subCnt == 2: |
| | |
| | ENUMS["Reference1"] = self._makeReference1Enumerations(subsList[0]) |
| | ENUMS["Reference2"] = self._makeReference2Enumerations(subsList[1]) |
| | return ENUMS |
| |
|
| | def updateEnumerations(self, obj): |
| | """updateEnumerations(obj) ... |
| | Method updates property enumerations based on active conditions |
| | in the operation. Returns the updated enumerations dictionary. |
| | Existing property values must be stored, and then restored after |
| | the assignment of updated enumerations.""" |
| | Path.Log.debug("updateEnumerations()") |
| | |
| | pre_Ref1 = obj.Reference1 |
| | pre_Ref2 = obj.Reference2 |
| |
|
| | |
| | ENUMS = self.getActiveEnumerations(obj) |
| | obj.Reference1 = ENUMS["Reference1"] |
| | obj.Reference2 = ENUMS["Reference2"] |
| |
|
| | |
| | |
| | if pre_Ref1 in ENUMS["Reference1"]: |
| | obj.Reference1 = pre_Ref1 |
| | else: |
| | obj.Reference1 = ENUMS["Reference1"][0] |
| | if pre_Ref2 in ENUMS["Reference2"]: |
| | obj.Reference2 = pre_Ref2 |
| | else: |
| | obj.Reference2 = ENUMS["Reference2"][0] |
| |
|
| | return ENUMS |
| |
|
| | def opSetEditorModes(self, obj): |
| | |
| | A = B = 2 |
| | C = 0 |
| | if hasattr(obj, "Base"): |
| | if obj.Base: |
| | |
| | subsList = obj.Base[0][1] |
| | subCnt = len(subsList) |
| | if subCnt == 1: |
| | A = 0 |
| | elif subCnt == 2: |
| | A = B = 0 |
| | C = 2 |
| |
|
| | obj.setEditorMode("Reference1", A) |
| | obj.setEditorMode("Reference2", B) |
| | obj.setEditorMode("ExtendRadius", C) |
| |
|
| | def onChanged(self, obj, prop): |
| | if hasattr(self, "propertiesReady"): |
| | if self.propertiesReady: |
| | if prop in ["Base"]: |
| | self.updateEnumerations(obj) |
| | self.opSetEditorModes(obj) |
| |
|
| | if prop == "Active" and obj.ViewObject: |
| | obj.ViewObject.signalChangeIcon() |
| |
|
| | def opOnDocumentRestored(self, obj): |
| | self.propertiesReady = False |
| | job = PathUtils.findParentJob(obj) |
| |
|
| | self.initOpProperties(obj, warn=True) |
| | self.opApplyPropertyDefaults(obj, job, self.addNewProps) |
| |
|
| | mode = 2 if Path.Log.getLevel(Path.Log.thisModule()) != 4 else 0 |
| | obj.setEditorMode("ShowTempObjects", mode) |
| |
|
| | |
| | ENUMS = self.updateEnumerations(obj) |
| | for n in ENUMS: |
| | restore = False |
| | if hasattr(obj, n): |
| | val = obj.getPropertyByName(n) |
| | restore = True |
| | setattr(obj, n, ENUMS[n]) |
| | if restore: |
| | setattr(obj, n, val) |
| |
|
| | self.opSetEditorModes(obj) |
| |
|
| | def opApplyPropertyDefaults(self, obj, job, propList): |
| | |
| | PROP_DFLTS = self.opPropertyDefaults(obj, job) |
| | for n in PROP_DFLTS: |
| | if n in propList: |
| | prop = getattr(obj, n) |
| | val = PROP_DFLTS[n] |
| | setVal = False |
| | if hasattr(prop, "Value"): |
| | if isinstance(val, int) or isinstance(val, float): |
| | setVal = True |
| | if setVal: |
| | |
| | setattr(prop, "Value", val) |
| | else: |
| | setattr(obj, n, val) |
| |
|
| | def opSetDefaultValues(self, obj, job): |
| | """opSetDefaultValues(obj, job) ... initialize defaults""" |
| | job = PathUtils.findParentJob(obj) |
| |
|
| | self.opApplyPropertyDefaults(obj, job, self.addNewProps) |
| |
|
| | |
| | d = None |
| | if job: |
| | if job.Stock: |
| | d = PathUtils.guessDepths(job.Stock.Shape, None) |
| | Path.Log.debug("job.Stock exists") |
| | else: |
| | Path.Log.debug("job.Stock NOT exist") |
| | else: |
| | Path.Log.debug("job NOT exist") |
| |
|
| | if d is not None: |
| | obj.OpFinalDepth.Value = d.final_depth |
| | obj.OpStartDepth.Value = d.start_depth |
| | else: |
| | obj.OpFinalDepth.Value = -10 |
| | obj.OpStartDepth.Value = 10 |
| |
|
| | Path.Log.debug("Default OpFinalDepth: {}".format(obj.OpFinalDepth.Value)) |
| | Path.Log.debug("Default OpStartDepth: {}".format(obj.OpStartDepth.Value)) |
| |
|
| | def opApplyPropertyLimits(self, obj): |
| | """opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.""" |
| | pass |
| |
|
| | def opUpdateDepths(self, obj): |
| | if hasattr(obj, "Base") and obj.Base: |
| | base, sublist = obj.Base[0] |
| | fbb = base.Shape.getElement(sublist[0]).BoundBox |
| | zmin = fbb.ZMax |
| | for base, sublist in obj.Base: |
| | for sub in sublist: |
| | try: |
| | fbb = base.Shape.getElement(sub).BoundBox |
| | zmin = min(zmin, fbb.ZMin) |
| | except Part.OCCError as e: |
| | Path.Log.error(e) |
| | obj.OpFinalDepth = zmin |
| |
|
| | def opExecute(self, obj): |
| | """opExecute(obj) ... process surface operation""" |
| | Path.Log.track() |
| |
|
| | |
| | self.base = None |
| | self.shape1 = None |
| | self.shape2 = None |
| | self.shapeType1 = None |
| | self.shapeType2 = None |
| | self.shapeLength1 = None |
| | self.shapeLength2 = None |
| | self.dYdX1 = None |
| | self.dYdX2 = None |
| | self.bottomEdges = None |
| | self.isArc = 0 |
| | self.arcCenter = None |
| | self.arcMidPnt = None |
| | self.arcRadius = 0 |
| | self.newRadius = 0 |
| | self.featureDetails = ["", ""] |
| | self.commandlist = [] |
| | self.stockZMin = self.job.Stock.Shape.BoundBox.ZMin |
| |
|
| | |
| | self.isDebug = Path.Log.getLevel(Path.Log.thisModule()) == 4 |
| | self.showDebugObjects = self.isDebug and obj.ShowTempObjects |
| |
|
| | if self.showDebugObjects: |
| | self._clearDebugGroups() |
| | self.tmpGrp = FreeCAD.ActiveDocument.addObject( |
| | "App::DocumentObjectGroup", "tmpDebugGrp" |
| | ) |
| |
|
| | |
| | tool = obj.ToolController.Tool |
| | toolType = getattr(tool, "ShapeType", None) |
| | if toolType is None: |
| | Path.Log.warning("Tool does not define ShapeType, using label as fallback.") |
| | toolType = tool.Label |
| |
|
| | if obj.Comment: |
| | self.commandlist.append(Path.Command(f"N ({obj.Comment})", {})) |
| | self.commandlist.append(Path.Command(f"N ({obj.Label})", {})) |
| | self.commandlist.append(Path.Command(f"N (Tool type: {toolType})", {})) |
| | self.commandlist.append( |
| | Path.Command(f"N (Compensated Tool Path. Diameter: {tool.Diameter})", {}) |
| | ) |
| | self.commandlist.append(Path.Command("N ()", {})) |
| |
|
| | self.commandlist.append( |
| | Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}) |
| | ) |
| | if obj.UseStartPoint: |
| | self.commandlist.append( |
| | Path.Command( |
| | "G0", |
| | { |
| | "X": obj.StartPoint.x, |
| | "Y": obj.StartPoint.y, |
| | "F": self.horizRapid, |
| | }, |
| | ) |
| | ) |
| |
|
| | |
| | self.opApplyPropertyLimits(obj) |
| | self.depthParams = PathUtils.depth_params( |
| | obj.ClearanceHeight.Value, |
| | obj.SafeHeight.Value, |
| | obj.StartDepth.Value, |
| | obj.StepDown.Value, |
| | 0, |
| | obj.FinalDepth.Value, |
| | ) |
| |
|
| | |
| | cmds = self._makeOperation(obj) |
| | if cmds: |
| | self.commandlist.extend(cmds) |
| | else: |
| | |
| | self.commandlist.clear() |
| | return False |
| |
|
| | |
| | if self.showDebugObjects and FreeCAD.GuiUp: |
| | FreeCADGui.ActiveDocument.getObject(self.tmpGrp.Name).Visibility = False |
| | self.tmpGrp.purgeTouched() |
| |
|
| | return True |
| |
|
| | def _clearDebugGroups(self): |
| | doc = FreeCAD.ActiveDocument |
| | for name in ["tmpDebugGrp", "tmpDebugGrp001"]: |
| | grp = getattr(doc, name, None) |
| | if grp: |
| | for obj in grp.Group: |
| | doc.removeObject(obj.Name) |
| | doc.removeObject(name) |
| |
|
| | |
| | def _makeOperation(self, obj): |
| | """This method controls the overall slot creation process.""" |
| | pnts = False |
| | featureCount = 0 |
| |
|
| | if not hasattr(obj, "Base"): |
| | msg = translate("CAM_Slot", "No Base Geometry object in the operation.") |
| | FreeCAD.Console.PrintUserWarning(msg + "\n") |
| | return False |
| |
|
| | if not obj.Base: |
| | |
| | p1 = obj.CustomPoint1 |
| | p2 = obj.CustomPoint2 |
| | if p1 == p2: |
| | msg = translate( |
| | "CAM_Slot", "Custom points are identical. No slot path will be generated" |
| | ) |
| | FreeCAD.Console.PrintUserWarning(msg + "\n") |
| | return False |
| | elif p1.z == p2.z: |
| | pnts = (p1, p2) |
| | featureCount = 2 |
| | else: |
| | msg = translate( |
| | "CAM_Slot", "Custom points not at same Z height. No slot path will be generated" |
| | ) |
| | FreeCAD.Console.PrintUserWarning(msg + "\n") |
| | return False |
| | else: |
| | baseGeom = obj.Base[0] |
| | base, subsList = baseGeom |
| | self.base = base |
| |
|
| | featureCount = len(subsList) |
| | if featureCount == 1: |
| | Path.Log.debug("Reference 1: {}".format(obj.Reference1)) |
| | sub1 = subsList[0] |
| | shape_1 = getattr(base.Shape, sub1) |
| | self.shape1 = shape_1 |
| | pnts = self._processSingle(obj, shape_1, sub1) |
| | else: |
| | Path.Log.debug("Reference 1: {}".format(obj.Reference1)) |
| | Path.Log.debug("Reference 2: {}".format(obj.Reference2)) |
| | sub1 = subsList[0] |
| | sub2 = subsList[1] |
| | shape_1 = getattr(base.Shape, sub1) |
| | shape_2 = getattr(base.Shape, sub2) |
| | self.shape1 = shape_1 |
| | self.shape2 = shape_2 |
| | pnts = self._processDouble(obj, shape_1, sub1, shape_2, sub2) |
| |
|
| | if not pnts: |
| | return False |
| |
|
| | if self.isArc: |
| | cmds = self._finishArc(obj, pnts, featureCount) |
| | else: |
| | cmds = self._finishLine(obj, pnts, featureCount) |
| |
|
| | if cmds: |
| | return cmds |
| |
|
| | return False |
| |
|
| | def _finishArc(self, obj, pnts, featureCnt): |
| | """This method finishes an Arc Slot operation. |
| | It returns the gcode for the slot operation.""" |
| | Path.Log.debug("arc center: {}".format(self.arcCenter)) |
| | self._addDebugObject(Part.makeLine(self.arcCenter, self.arcMidPnt), "CentToMidPnt") |
| |
|
| | |
| | if obj.ExtendRadius.Value != 0: |
| | |
| | newRadius = self.arcRadius + obj.ExtendRadius.Value |
| | Path.Log.debug("arc radius: {}; offset radius: {}".format(self.arcRadius, newRadius)) |
| | if newRadius <= 0: |
| | msg = translate( |
| | "CAM_Slot", |
| | "Current Extend Radius value produces negative arc radius.", |
| | ) |
| | FreeCAD.Console.PrintError(msg + "\n") |
| | return False |
| | else: |
| | (p1, p2) = pnts |
| | pnts = self._makeOffsetArc(p1, p2, self.arcCenter, newRadius) |
| | self.newRadius = newRadius |
| | else: |
| | Path.Log.debug("arc radius: {}".format(self.arcRadius)) |
| | self.newRadius = self.arcRadius |
| |
|
| | |
| | |
| | if self.isArc == 1: |
| | |
| | if obj.ExtendPathStart.Value != 0 or obj.ExtendPathEnd.Value != 0: |
| | msg = translate("CAM_Slot", "No path extensions available for full circles.") |
| | FreeCAD.Console.PrintWarning(msg + "\n") |
| | else: |
| | |
| | |
| | (p1, p2) = pnts |
| | begExt = obj.ExtendPathStart.Value |
| | endExt = obj.ExtendPathEnd.Value |
| | |
| | |
| | pnts = self._extendArcSlot(p1, p2, self.arcCenter, endExt, begExt) |
| |
|
| | if not pnts: |
| | return False |
| |
|
| | (p1, p2) = pnts |
| | |
| | if self.isDebug: |
| | Path.Log.debug("Path Points are:\np1 = {}\np2 = {}".format(p1, p2)) |
| | if p1.sub(p2).Length != 0: |
| | self._addDebugObject(Part.makeLine(p1, p2), "Path") |
| |
|
| | if featureCnt: |
| | obj.CustomPoint1 = p1 |
| | obj.CustomPoint2 = p2 |
| |
|
| | if self._arcCollisionCheck(obj, p1, p2, self.arcCenter, self.newRadius): |
| | msg = obj.Label + " " |
| | msg += translate("CAM_Slot", "operation collides with model.") |
| | FreeCAD.Console.PrintError(msg + "\n") |
| |
|
| | |
| | cmds = self._makeArcGCode(obj, p1, p2) |
| | return cmds |
| |
|
| | def _makeArcGCode(self, obj, p1, p2): |
| | """This method is the last step in the overall arc slot creation process. |
| | It accepts the operation object and two end points for the path. |
| | It returns the gcode for the slot operation.""" |
| | CMDS = list() |
| | PATHS = [(p2, p1, "G2"), (p1, p2, "G3")] |
| | if obj.ReverseDirection: |
| | path_index = 1 |
| | else: |
| | path_index = 0 |
| |
|
| | def arcPass(POINTS, depth): |
| | cmds = list() |
| | (st_pt, end_pt, arcCmd) = POINTS |
| | |
| | cmds.append(Path.Command("G0", {"X": st_pt.x, "Y": st_pt.y, "F": self.horizRapid})) |
| | cmds.append(Path.Command("G1", {"Z": depth, "F": self.vertFeed})) |
| | vtc = self.arcCenter.sub(st_pt) |
| | cmds.append( |
| | Path.Command( |
| | arcCmd, |
| | { |
| | "X": end_pt.x, |
| | "Y": end_pt.y, |
| | "I": vtc.x, |
| | "J": vtc.y, |
| | "F": self.horizFeed, |
| | }, |
| | ) |
| | ) |
| | return cmds |
| |
|
| | if obj.LayerMode == "Single-pass": |
| | CMDS.extend(arcPass(PATHS[path_index], obj.FinalDepth.Value)) |
| | else: |
| | if obj.CutPattern == "Line": |
| | for depth in self.depthParams: |
| | CMDS.extend(arcPass(PATHS[path_index], depth)) |
| | CMDS.append( |
| | Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid}) |
| | ) |
| | elif obj.CutPattern == "ZigZag": |
| | i = 0 |
| | for depth in self.depthParams: |
| | if i % 2 == 0: |
| | CMDS.extend(arcPass(PATHS[path_index], depth)) |
| | else: |
| | CMDS.extend(arcPass(PATHS[not path_index], depth)) |
| | i += 1 |
| | |
| | CMDS.append(Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid})) |
| |
|
| | if self.isDebug: |
| | Path.Log.debug("G-code arc command is: {}".format(PATHS[path_index][2])) |
| |
|
| | return CMDS |
| |
|
| | def _finishLine(self, obj, pnts, featureCnt): |
| | """This method finishes a Line Slot operation. |
| | It returns the gcode for the line slot operation.""" |
| | |
| | perpZero = True |
| | if obj.PathOrientation == "Perpendicular": |
| | if featureCnt == 2: |
| | if self.shapeType1 == "Face" and self.shapeType2 == "Face": |
| | if self.bottomEdges: |
| | self.bottomEdges.sort(key=lambda edg: edg.Length, reverse=True) |
| | BE = self.bottomEdges[0] |
| | pnts = self._processSingleVertFace(obj, BE) |
| | perpZero = False |
| | elif self.shapeType1 == "Edge" and self.shapeType2 == "Edge": |
| | Path.Log.debug("_finishLine() Perp, featureCnt == 2") |
| | if perpZero: |
| | (p1, p2) = pnts |
| | initPerpDist = p1.sub(p2).Length |
| | pnts = self._makePerpendicular(p1, p2, initPerpDist) |
| | else: |
| | |
| | if featureCnt == 2 and self.shapeType1 == "Edge" and self.shapeType2 == "Edge": |
| | if self.featureDetails[0] == "arc" and self.featureDetails[1] == "arc": |
| | perpZero = False |
| | elif self._isParallel(self.dYdX1, self.dYdX2): |
| | Path.Log.debug("_finishLine() StE, featureCnt == 2 // edges") |
| | (p1, p2) = pnts |
| | edg1_len = self.shape1.Length |
| | edg2_len = self.shape2.Length |
| | set_length = max(edg1_len, edg2_len) |
| | pnts = self._makePerpendicular(p1, p2, 10 + set_length) |
| | if edg1_len != edg2_len: |
| | msg = obj.Label + " " |
| | msg += translate("CAM_Slot", "Verify slot path start and end points.") |
| | FreeCAD.Console.PrintWarning(msg + "\n") |
| | else: |
| | perpZero = False |
| |
|
| | |
| | if obj.ReverseDirection: |
| | (p2, p1) = pnts |
| | else: |
| | (p1, p2) = pnts |
| |
|
| | |
| | begExt = obj.ExtendPathStart.Value |
| | endExt = obj.ExtendPathEnd.Value |
| | if perpZero: |
| | |
| | begExt -= 5 |
| | endExt -= 5 |
| | pnts = self._extendLineSlot(p1, p2, begExt, endExt) |
| |
|
| | if not pnts: |
| | return False |
| |
|
| | (p1, p2) = pnts |
| | if self.isDebug: |
| | Path.Log.debug("Path Points are:\np1 = {}\np2 = {}".format(p1, p2)) |
| | if p1.sub(p2).Length != 0: |
| | self._addDebugObject(Part.makeLine(p1, p2), "Path") |
| |
|
| | if featureCnt: |
| | obj.CustomPoint1 = p1 |
| | obj.CustomPoint2 = p2 |
| |
|
| | if self._lineCollisionCheck(obj, p1, p2): |
| | msg = obj.Label + " " |
| | msg += translate("CAM_Slot", "operation collides with model.") |
| | FreeCAD.Console.PrintWarning(msg + "\n") |
| |
|
| | cmds = self._makeLineGCode(obj, p1, p2) |
| | return cmds |
| |
|
| | def _makeLineGCode(self, obj, p1, p2): |
| | """This method is the last in the overall line slot creation process. |
| | It accepts the operation object and two end points for the path. |
| | It returns the gcode for the slot operation.""" |
| | CMDS = list() |
| |
|
| | def linePass(p1, p2, depth): |
| | cmds = list() |
| | |
| | cmds.append(Path.Command("G0", {"X": p1.x, "Y": p1.y, "F": self.horizRapid})) |
| | cmds.append(Path.Command("G1", {"Z": depth, "F": self.vertFeed})) |
| | cmds.append(Path.Command("G1", {"X": p2.x, "Y": p2.y, "F": self.horizFeed})) |
| | return cmds |
| |
|
| | |
| | if obj.LayerMode == "Single-pass": |
| | CMDS.extend(linePass(p1, p2, obj.FinalDepth.Value)) |
| | CMDS.append(Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid})) |
| | else: |
| | if obj.CutPattern == "Line": |
| | for dep in self.depthParams: |
| | CMDS.extend(linePass(p1, p2, dep)) |
| | CMDS.append( |
| | Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid}) |
| | ) |
| | elif obj.CutPattern == "ZigZag": |
| | CMDS.append(Path.Command("G0", {"X": p1.x, "Y": p1.y, "F": self.horizRapid})) |
| | i = 0 |
| | for dep in self.depthParams: |
| | if i % 2 == 0: |
| | CMDS.append(Path.Command("G1", {"Z": dep, "F": self.vertFeed})) |
| | CMDS.append(Path.Command("G1", {"X": p2.x, "Y": p2.y, "F": self.horizFeed})) |
| | else: |
| | CMDS.append(Path.Command("G1", {"Z": dep, "F": self.vertFeed})) |
| | CMDS.append(Path.Command("G1", {"X": p1.x, "Y": p1.y, "F": self.horizFeed})) |
| | i += 1 |
| | CMDS.append(Path.Command("G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid})) |
| |
|
| | return CMDS |
| |
|
| | |
| | def _processSingle(self, obj, shape_1, sub1): |
| | """This is the control method for slots based on a |
| | single Base Geometry feature.""" |
| | done = False |
| | cat1 = sub1[:4] |
| |
|
| | if cat1 == "Face": |
| | pnts = False |
| | norm = shape_1.normalAt(0, 0) |
| | Path.Log.debug("{}.normalAt(): {}".format(sub1, norm)) |
| |
|
| | if Path.Geom.isRoughly(shape_1.BoundBox.ZMax, shape_1.BoundBox.ZMin): |
| | |
| | if norm.z == 1 or norm.z == -1: |
| | pnts = self._processSingleHorizFace(obj, shape_1) |
| | elif norm.z == 0: |
| | faceType = self._getVertFaceType(shape_1) |
| | if faceType: |
| | (geo, shp) = faceType |
| | if geo == "Face": |
| | pnts = self._processSingleComplexFace(obj, shp) |
| | if geo == "Wire": |
| | pnts = self._processSingleVertFace(obj, shp) |
| | if geo == "Edge": |
| | pnts = self._processSingleVertFace(obj, shp) |
| | else: |
| | if len(shape_1.Edges) == 4: |
| | pnts = self._processSingleHorizFace(obj, shape_1) |
| | else: |
| | pnts = self._processSingleComplexFace(obj, shape_1) |
| |
|
| | if not pnts: |
| | msg = translate("CAM_Slot", "The selected face is inaccessible.") |
| | FreeCAD.Console.PrintError(msg + "\n") |
| | return False |
| |
|
| | if pnts: |
| | (p1, p2) = pnts |
| | done = True |
| |
|
| | elif cat1 == "Edge": |
| | Path.Log.debug("Single edge") |
| | pnts = self._processSingleEdge(obj, shape_1) |
| | if pnts: |
| | (p1, p2) = pnts |
| | done = True |
| |
|
| | elif cat1 == "Vert": |
| | msg = translate( |
| | "CAM_Slot", |
| | "Only a vertex selected. Add another feature to the Base Geometry.", |
| | ) |
| | FreeCAD.Console.PrintError(msg + "\n") |
| |
|
| | if done: |
| | return (p1, p2) |
| |
|
| | return False |
| |
|
| | def _processSingleHorizFace(self, obj, shape): |
| | """Determine slot path endpoints from a single horizontally oriented face.""" |
| | Path.Log.debug("_processSingleHorizFace()") |
| | line_types = ["Part::GeomLine"] |
| |
|
| | def get_edge_angle_deg(edge): |
| | vect = self._dXdYdZ(edge) |
| | norm = self._normalizeVector(vect) |
| | rads = self._getVectorAngle(norm) |
| | deg = math.degrees(rads) |
| | if deg >= 180: |
| | deg -= 180 |
| | return deg |
| |
|
| | |
| | if len(shape.Edges) != 4: |
| | msg = translate("CAM_Slot", "A single selected face must have four edges.") |
| | FreeCAD.Console.PrintError(msg + "\n") |
| | return False |
| |
|
| | |
| | edge_info_list = [] |
| | for edge_index in range(4): |
| | edge = shape.Edges[edge_index] |
| | edge_length = edge.Length |
| | edge_angle = get_edge_angle_deg(edge) |
| | edge_info_list.append((edge_index, edge_length, edge_angle)) |
| |
|
| | |
| | edge_info_list.sort(key=lambda tup: tup[2]) |
| |
|
| | |
| | parallel_pairs = [] |
| | parallel_flags = [0] * len(shape.Edges) |
| | current_flag = 1 |
| | last_edge_index = min(len(shape.Edges), len(edge_info_list)) - 1 |
| |
|
| | for i in range(last_edge_index): |
| | next_i = i + 1 |
| | edge_a_info = edge_info_list[i] |
| | edge_b_info = edge_info_list[next_i] |
| | angle_a = edge_a_info[2] |
| | angle_b = edge_b_info[2] |
| |
|
| | if abs(angle_a - angle_b) >= 1e-6: |
| | continue |
| |
|
| | edge_a = shape.Edges[edge_a_info[0]] |
| | edge_b = shape.Edges[edge_b_info[0]] |
| |
|
| | debug_type_id = None |
| | if edge_a.Curve.TypeId not in line_types: |
| | debug_type_id = edge_a.Curve.TypeId |
| | elif edge_b.Curve.TypeId not in line_types: |
| | debug_type_id = edge_b.Curve.TypeId |
| |
|
| | if debug_type_id: |
| | Path.Log.debug(f"Erroneous Curve.TypeId: {debug_type_id}") |
| | else: |
| | parallel_pairs.append((edge_a, edge_b)) |
| | parallel_flags[edge_a_info[0]] = current_flag |
| | parallel_flags[edge_b_info[0]] = current_flag |
| | current_flag += 1 |
| |
|
| | pair_count = len(parallel_pairs) |
| | if pair_count > 1: |
| | |
| | parallel_pairs.sort(key=lambda pair: pair[0].Length, reverse=True) |
| |
|
| | if self.isDebug: |
| | Path.Log.debug(f" - Parallel pair count: {pair_count}") |
| | for edge1, edge2 in parallel_pairs: |
| | Path.Log.debug( |
| | f" - Pair lengths: {round(edge1.Length, 4)}, {round(edge2.Length, 4)}" |
| | ) |
| | Path.Log.debug(f" - Parallel flags: {parallel_flags}") |
| |
|
| | if pair_count == 0: |
| | msg = translate("CAM_Slot", "No parallel edges identified.") |
| | FreeCAD.Console.PrintError(msg + "\n") |
| | return False |
| |
|
| | if pair_count == 1: |
| | if len(shape.Edges) == 4: |
| | |
| | non_parallel_edges = [ |
| | shape.Edges[i] for i, flag in enumerate(parallel_flags) if flag == 0 |
| | ] |
| | if len(non_parallel_edges) == 2: |
| | selected_edges = (non_parallel_edges[0], non_parallel_edges[1]) |
| | else: |
| | selected_edges = parallel_pairs[0] |
| | else: |
| | selected_edges = parallel_pairs[0] |
| | else: |
| | if obj.Reference1 == "Long Edge": |
| | selected_edges = parallel_pairs[1] |
| | elif obj.Reference1 == "Short Edge": |
| | selected_edges = parallel_pairs[0] |
| | else: |
| | msg = "Reference1 " + translate("CAM_Slot", "value error.") |
| | FreeCAD.Console.PrintError(msg + "\n") |
| | return False |
| |
|
| | (point1, point2) = self._getOppMidPoints(selected_edges) |
| | return (point1, point2) |
| |
|
| | def _processSingleComplexFace(self, obj, shape): |
| | """Determine slot path endpoints from a single complex face.""" |
| | Path.Log.debug("_processSingleComplexFace()") |
| | pnts = list() |
| |
|
| | def zVal(p): |
| | return p.z |
| |
|
| | for E in shape.Wires[0].Edges: |
| | p = self._findLowestEdgePoint(E) |
| | pnts.append(p) |
| | pnts.sort(key=zVal) |
| | return (pnts[0], pnts[1]) |
| |
|
| | def _processSingleVertFace(self, obj, shape): |
| | """Determine slot path endpoints from a single vertically oriented face |
| | with no single bottom edge.""" |
| | Path.Log.debug("_processSingleVertFace()") |
| | eCnt = len(shape.Edges) |
| | V0 = shape.Edges[0].Vertexes[0] |
| | V1 = shape.Edges[eCnt - 1].Vertexes[1] |
| | v0 = FreeCAD.Vector(V0.X, V0.Y, V0.Z) |
| | v1 = FreeCAD.Vector(V1.X, V1.Y, V1.Z) |
| |
|
| | dX = V1.X - V0.X |
| | dY = V1.Y - V0.Y |
| | dZ = V1.Z - V0.Z |
| | temp = FreeCAD.Vector(dX, dY, dZ) |
| | slope = self._normalizeVector(temp) |
| | perpVect = FreeCAD.Vector(-1 * slope.y, slope.x, slope.z) |
| | perpVect.multiply(self.tool.Diameter / 2) |
| |
|
| | |
| | a1 = v0.add(perpVect) |
| | a2 = v1.add(perpVect) |
| | b1 = v0.sub(perpVect) |
| | b2 = v1.sub(perpVect) |
| | (p1, p2) = self._getCutSidePoints(obj, v0, v1, a1, a2, b1, b2) |
| |
|
| | msg = obj.Label + " " |
| | msg += translate("CAM_Slot", "Verify slot path start and end points.") |
| | FreeCAD.Console.PrintWarning(msg + "\n") |
| |
|
| | return (p1, p2) |
| |
|
| | def _processSingleEdge(self, obj, edge): |
| | """Determine slot path endpoints from a single horizontally oriented edge.""" |
| | Path.Log.debug("_processSingleEdge()") |
| | tol = 1e-7 |
| | lineTypes = {"Part::GeomLine"} |
| | curveTypes = {"Part::GeomCircle"} |
| |
|
| | def oversizedTool(holeDiam): |
| | if self.tool.Diameter > holeDiam: |
| | msg = translate("CAM_Slot", "Current tool larger than arc diameter.") |
| | FreeCAD.Console.PrintError(msg + "\n") |
| | return True |
| | return False |
| |
|
| | def isHorizontal(z1, z2, z3): |
| | return abs(z1 - z2) <= tol and abs(z1 - z3) <= tol |
| |
|
| | def circumCircleFrom3Points(P1, P2, P3): |
| | v1 = P2 - P1 |
| | v2 = P3 - P2 |
| | v3 = P1 - P3 |
| | L = v1.cross(v2).Length |
| | if round(L, 8) == 0: |
| | Path.Log.error("Three points are colinear. Arc is straight.") |
| | return False |
| | twoL2 = 2 * L * L |
| | a = -v2.dot(v2) * v1.dot(v3) / twoL2 |
| | b = -v3.dot(v3) * v2.dot(v1) / twoL2 |
| | c = -v1.dot(v1) * v3.dot(v2) / twoL2 |
| | return P1 * a + P2 * b + P3 * c |
| |
|
| | verts = edge.Vertexes |
| | V1 = verts[0] |
| | p1 = FreeCAD.Vector(V1.X, V1.Y, 0) |
| | p2 = p1 if len(verts) == 1 else FreeCAD.Vector(verts[1].X, verts[1].Y, 0) |
| |
|
| | curveType = edge.Curve.TypeId |
| | if curveType in lineTypes: |
| | return (p1, p2) |
| |
|
| | elif curveType in curveTypes: |
| | if len(verts) == 1: |
| | |
| | Path.Log.debug("Arc with single vertex (circle).") |
| | if oversizedTool(edge.BoundBox.XLength): |
| | return False |
| | self.isArc = 1 |
| | tp1 = edge.valueAt(edge.getParameterByLength(edge.Length * 0.33)) |
| | tp2 = edge.valueAt(edge.getParameterByLength(edge.Length * 0.66)) |
| | if not isHorizontal(V1.Z, tp1.z, tp2.z): |
| | return False |
| |
|
| | center = edge.BoundBox.Center |
| | self.arcCenter = FreeCAD.Vector(center.x, center.y, 0) |
| | mid = edge.valueAt(edge.getParameterByLength(edge.Length / 2)) |
| | self.arcMidPnt = FreeCAD.Vector(mid.x, mid.y, 0) |
| | self.arcRadius = edge.BoundBox.XLength / 2 |
| | else: |
| | |
| | Path.Log.debug("Arc with multiple vertices.") |
| | V2 = verts[1] |
| | mid = edge.valueAt(edge.getParameterByLength(edge.Length / 2)) |
| | if not isHorizontal(V1.Z, V2.Z, mid.z): |
| | return False |
| | mid.z = 0 |
| | center = circumCircleFrom3Points(p1, p2, FreeCAD.Vector(mid.x, mid.y, 0)) |
| | if not center: |
| | return False |
| |
|
| | self.isArc = 2 |
| | self.arcMidPnt = FreeCAD.Vector(mid.x, mid.y, 0) |
| | self.arcCenter = center |
| | self.arcRadius = (p1 - center).Length |
| |
|
| | if oversizedTool(self.arcRadius * 2): |
| | return False |
| |
|
| | return (p1, p2) |
| |
|
| | else: |
| | msg = translate( |
| | "CAM_Slot", "Failed, slot from edge only accepts lines, arcs and circles." |
| | ) |
| | FreeCAD.Console.PrintError(msg + "\n") |
| | return False |
| |
|
| | |
| | def _processDouble(self, obj, shape_1, sub1, shape_2, sub2): |
| | """This is the control method for slots based on a |
| | two Base Geometry features.""" |
| | Path.Log.debug("_processDouble()") |
| |
|
| | p1 = None |
| | p2 = None |
| | dYdX1 = None |
| | dYdX2 = None |
| | self.bottomEdges = list() |
| |
|
| | feature1 = self._processFeature(obj, shape_1, sub1, 1) |
| | if not feature1: |
| | msg = translate("CAM_Slot", "Failed to determine point 1 from") |
| | FreeCAD.Console.PrintError(msg + " {}.\n".format(sub1)) |
| | return False |
| | (p1, dYdX1, shpType) = feature1 |
| | self.shapeType1 = shpType |
| | if dYdX1: |
| | self.dYdX1 = dYdX1 |
| |
|
| | feature2 = self._processFeature(obj, shape_2, sub2, 2) |
| | if not feature2: |
| | msg = translate("CAM_Slot", "Failed to determine point 2 from") |
| | FreeCAD.Console.PrintError(msg + " {}.\n".format(sub2)) |
| | return False |
| | (p2, dYdX2, shpType) = feature2 |
| | self.shapeType2 = shpType |
| | if dYdX2: |
| | self.dYdX2 = dYdX2 |
| |
|
| | |
| | if dYdX1 and dYdX2: |
| | Path.Log.debug("dYdX1, dYdX2: {}, {}".format(dYdX1, dYdX2)) |
| | if not self._isParallel(dYdX1, dYdX2): |
| | if self.shapeType1 != "Edge" or self.shapeType2 != "Edge": |
| | msg = translate("CAM_Slot", "Selected geometry not parallel.") |
| | FreeCAD.Console.PrintError(msg + "\n") |
| | return False |
| |
|
| | if p2: |
| | return (p1, p2) |
| |
|
| | return False |
| |
|
| | |
| | def _dXdYdZ(self, E): |
| | v1 = E.Vertexes[0] |
| | v2 = E.Vertexes[1] |
| | dX = v2.X - v1.X |
| | dY = v2.Y - v1.Y |
| | dZ = v2.Z - v1.Z |
| | return FreeCAD.Vector(dX, dY, dZ) |
| |
|
| | def _normalizeVector(self, v): |
| | """Return a normalized vector with components rounded to nearest axis-aligned value if close.""" |
| | tol = 1e-10 |
| | V = FreeCAD.Vector(v).normalize() |
| |
|
| | def snap(val): |
| | if abs(val) < tol: |
| | return 0 |
| | if abs(1 - abs(val)) < tol: |
| | return 1 if val > 0 else -1 |
| | return val |
| |
|
| | return FreeCAD.Vector(snap(V.x), snap(V.y), snap(V.z)) |
| |
|
| | def _getLowestPoint(self, shape): |
| | """Return the average XY of the vertices with the lowest Z value.""" |
| | vertices = shape.Vertexes |
| | lowest_z = min(v.Z for v in vertices) |
| | lowest_vertices = [v for v in vertices if v.Z == lowest_z] |
| |
|
| | avg_x = sum(v.X for v in lowest_vertices) / len(lowest_vertices) |
| | avg_y = sum(v.Y for v in lowest_vertices) / len(lowest_vertices) |
| | return FreeCAD.Vector(avg_x, avg_y, lowest_z) |
| |
|
| | def _getHighestPoint(self, shape): |
| | """Return the average XY of the vertices with the highest Z value.""" |
| | vertices = shape.Vertexes |
| | highest_z = max(v.Z for v in vertices) |
| | highest_vertices = [v for v in vertices if v.Z == highest_z] |
| |
|
| | avg_x = sum(v.X for v in highest_vertices) / len(highest_vertices) |
| | avg_y = sum(v.Y for v in highest_vertices) / len(highest_vertices) |
| | return FreeCAD.Vector(avg_x, avg_y, highest_z) |
| |
|
| | def _processFeature(self, obj, shape, sub, pNum): |
| | """Analyze a shape and return a tuple: (working point, slope, category).""" |
| | p = None |
| | dYdX = None |
| |
|
| | Ref = getattr(obj, f"Reference{pNum}") |
| |
|
| | if sub.startswith("Face"): |
| | cat = "Face" |
| | BE = self._getBottomEdge(shape) |
| | if BE: |
| | self.bottomEdges.append(BE) |
| |
|
| | |
| | V0 = shape.Vertexes[0] |
| | v1 = shape.CenterOfMass |
| | temp = FreeCAD.Vector(v1.x - V0.X, v1.y - V0.Y, 0) |
| | dYdX = self._normalizeVector(temp) if temp.Length != 0 else FreeCAD.Vector(0, 0, 0) |
| |
|
| | |
| | norm = shape.normalAt(0, 0) |
| | if norm.z != 0: |
| | msg = translate("CAM_Slot", "The selected face is not oriented vertically:") |
| | FreeCAD.Console.PrintError(f"{msg} {sub}.\n") |
| | return False |
| |
|
| | |
| | if Ref == "Center of Mass": |
| | com = shape.CenterOfMass |
| | p = FreeCAD.Vector(com.x, com.y, 0) |
| | elif Ref == "Center of BoundBox": |
| | bbox = shape.BoundBox.Center |
| | p = FreeCAD.Vector(bbox.x, bbox.y, 0) |
| | elif Ref == "Lowest Point": |
| | p = self._getLowestPoint(shape) |
| | elif Ref == "Highest Point": |
| | p = self._getHighestPoint(shape) |
| |
|
| | elif sub.startswith("Edge"): |
| | cat = "Edge" |
| | featDetIdx = pNum - 1 |
| | if shape.Curve.TypeId == "Part::GeomCircle": |
| | self.featureDetails[featDetIdx] = "arc" |
| |
|
| | edge = shape.Edges[0] if hasattr(shape, "Edges") else shape |
| | v0 = edge.Vertexes[0] |
| | v1 = edge.Vertexes[1] |
| | temp = FreeCAD.Vector(v1.X - v0.X, v1.Y - v0.Y, 0) |
| | dYdX = self._normalizeVector(temp) if temp.Length != 0 else FreeCAD.Vector(0, 0, 0) |
| |
|
| | if Ref == "Center of Mass": |
| | com = shape.CenterOfMass |
| | p = FreeCAD.Vector(com.x, com.y, 0) |
| | elif Ref == "Center of BoundBox": |
| | bbox = shape.BoundBox.Center |
| | p = FreeCAD.Vector(bbox.x, bbox.y, 0) |
| | elif Ref == "Lowest Point": |
| | p = self._findLowestPointOnEdge(shape) |
| | elif Ref == "Highest Point": |
| | p = self._findHighestPointOnEdge(shape) |
| |
|
| | elif sub.startswith("Vert"): |
| | cat = "Vert" |
| | V = shape.Vertexes[0] |
| | p = FreeCAD.Vector(V.X, V.Y, 0) |
| |
|
| | else: |
| | Path.Log.warning(f"Unrecognized subfeature type: {sub}") |
| | return False |
| |
|
| | if p: |
| | return (p, dYdX, cat) |
| |
|
| | return False |
| |
|
| | def _extendArcSlot(self, p1, p2, cent, begExt, endExt): |
| | """Extend an arc defined by endpoints p1, p2 and center cent. |
| | begExt and endExt are extension lengths along the arc at each end. |
| | Returns new (p1, p2) as (n1, n2).""" |
| | if not begExt and not endExt: |
| | return (p1, p2) |
| |
|
| | def makeChord(angle_rad): |
| | x = self.newRadius * math.cos(angle_rad) |
| | y = self.newRadius * math.sin(angle_rad) |
| | a = FreeCAD.Vector(self.newRadius, 0, 0) |
| | b = FreeCAD.Vector(x, y, 0) |
| | return Part.makeLine(a, b) |
| |
|
| | origin = FreeCAD.Vector(0, 0, 0) |
| | z_axis = FreeCAD.Vector(0, 0, 1) |
| |
|
| | n1, n2 = p1, p2 |
| |
|
| | if begExt: |
| | ext_rad = abs(begExt / self.newRadius) |
| | angle = self._getVectorAngle(p1.sub(self.arcCenter)) |
| | angle += -2 * ext_rad if begExt > 0 else 0 |
| | chord = makeChord(ext_rad) |
| | chord.rotate(origin, z_axis, math.degrees(angle)) |
| | chord.translate(self.arcCenter) |
| | self._addDebugObject(chord, "ExtendStart") |
| | n1 = chord.Vertexes[1].Point |
| |
|
| | if endExt: |
| | ext_rad = abs(endExt / self.newRadius) |
| | angle = self._getVectorAngle(p2.sub(self.arcCenter)) |
| | angle += 0 if endExt > 0 else -2 * ext_rad |
| | chord = makeChord(ext_rad) |
| | chord.rotate(origin, z_axis, math.degrees(angle)) |
| | chord.translate(self.arcCenter) |
| | self._addDebugObject(chord, "ExtendEnd") |
| | n2 = chord.Vertexes[1].Point |
| |
|
| | return (n1, n2) |
| |
|
| | def _makeOffsetArc(self, p1, p2, center, newRadius): |
| | """_makeOffsetArc(p1, p2, center, newRadius)... |
| | This function offsets an arc defined by endpoints, p1 and p2, and the center. |
| | New end points are returned at the radius passed by newRadius. |
| | The angle of the original arc is maintained.""" |
| | n1 = p1.sub(center).normalize() * newRadius |
| | n2 = p2.sub(center).normalize() * newRadius |
| | return (n1.add(center), n2.add(center)) |
| |
|
| | def _extendLineSlot(self, p1, p2, begExt, endExt): |
| | """_extendLineSlot(p1, p2, begExt, endExt)... |
| | This function extends a line defined by endpoints, p1 and p2. |
| | The beginning is extended by begExt value and the end by endExt value.""" |
| | if begExt: |
| | beg = p1.sub(p2) |
| | n1 = p1.add(beg.normalize() * begExt) |
| | else: |
| | n1 = p1 |
| | if endExt: |
| | end = p2.sub(p1) |
| | n2 = p2.add(end.normalize() * endExt) |
| | else: |
| | n2 = p2 |
| | return (n1, n2) |
| |
|
| | def _getOppMidPoints(self, same): |
| | """_getOppMidPoints(same)... |
| | Find mid-points between ends of equal, oppossing edges passed in tuple (edge1, edge2).""" |
| | com1 = same[0].CenterOfMass |
| | com2 = same[1].CenterOfMass |
| | p1 = FreeCAD.Vector(com1.x, com1.y, 0) |
| | p2 = FreeCAD.Vector(com2.x, com2.y, 0) |
| | return (p1, p2) |
| |
|
| | def _isParallel(self, dYdX1, dYdX2): |
| | """Determine if two orientation vectors are parallel.""" |
| | return dYdX1.cross(dYdX2) == FreeCAD.Vector(0, 0, 0) |
| |
|
| | def _makePerpendicular(self, p1, p2, length): |
| | """Using a line defined by p1 and p2, returns a perpendicular vector |
| | centered at the midpoint of the line, with given length.""" |
| |
|
| | midPnt = (p1.add(p2)).multiply(0.5) |
| | halfDist = length / 2 |
| |
|
| | if getattr(self, "dYdX1", None): |
| | half = FreeCAD.Vector(self.dYdX1.x, self.dYdX1.y, 0).multiply(halfDist) |
| | n1 = midPnt.add(half) |
| | n2 = midPnt.sub(half) |
| | return (n1, n2) |
| |
|
| | elif getattr(self, "dYdX2", None): |
| | half = FreeCAD.Vector(self.dYdX2.x, self.dYdX2.y, 0).multiply(halfDist) |
| | n1 = midPnt.add(half) |
| | n2 = midPnt.sub(half) |
| | return (n1, n2) |
| |
|
| | else: |
| | toEnd = p2.sub(p1) |
| | perp = FreeCAD.Vector(-toEnd.y, toEnd.x, 0) |
| | perp = perp.normalize() |
| | perp = perp.multiply(halfDist) |
| | n1 = midPnt.add(perp) |
| | n2 = midPnt.sub(perp) |
| | return (n1, n2) |
| |
|
| | def _findLowestPointOnEdge(self, E): |
| | tol = 1e-7 |
| | zMin = E.BoundBox.ZMin |
| |
|
| | |
| | for v in E.Vertexes: |
| | if abs(v.Z - zMin) < tol: |
| | return FreeCAD.Vector(v.X, v.Y, v.Z) |
| |
|
| | |
| | mid = E.valueAt(E.getParameterByLength(E.Length / 2)) |
| | if abs(mid.z - zMin) < tol or E.BoundBox.ZLength < 1e-9: |
| | return mid |
| |
|
| | |
| | return self._findLowestEdgePoint(E) |
| |
|
| | def _findLowestEdgePoint(self, E): |
| | zMin = E.BoundBox.ZMin |
| | L0, L1 = 0, E.Length |
| | tol = 1e-5 |
| | max_iter = 2000 |
| | cnt = 0 |
| |
|
| | while (L1 - L0) > tol and cnt < max_iter: |
| | p0 = E.valueAt(E.getParameterByLength(L0)) |
| | p1 = E.valueAt(E.getParameterByLength(L1)) |
| |
|
| | diff0 = p0.z - zMin |
| | diff1 = p1.z - zMin |
| |
|
| | adj = (L1 - L0) * 0.1 |
| | if diff0 < diff1: |
| | L1 -= adj |
| | elif diff0 > diff1: |
| | L0 += adj |
| | else: |
| | |
| | L0 += adj |
| | L1 -= adj |
| | cnt += 1 |
| |
|
| | midLen = (L0 + L1) / 2 |
| | return E.valueAt(E.getParameterByLength(midLen)) |
| |
|
| | def _findHighestPointOnEdge(self, E): |
| | tol = 1e-7 |
| | zMax = E.BoundBox.ZMax |
| |
|
| | |
| | v = E.Vertexes[0] |
| | if abs(zMax - v.Z) < tol: |
| | return FreeCAD.Vector(v.X, v.Y, v.Z) |
| |
|
| | |
| | v = E.Vertexes[1] |
| | if abs(zMax - v.Z) < tol: |
| | return FreeCAD.Vector(v.X, v.Y, v.Z) |
| |
|
| | |
| | midLen = E.Length / 2 |
| | midPnt = E.valueAt(E.getParameterByLength(midLen)) |
| | if abs(zMax - midPnt.z) < tol or E.BoundBox.ZLength < 1e-9: |
| | return midPnt |
| |
|
| | return self._findHighestEdgePoint(E) |
| |
|
| | def _findHighestEdgePoint(self, E): |
| | zMax = E.BoundBox.ZMax |
| | eLen = E.Length |
| | L0 = 0 |
| | L1 = eLen |
| | cnt = 0 |
| | while L1 - L0 > 1e-5 and cnt < 2000: |
| | adj = (L1 - L0) * 0.1 |
| | p0 = E.valueAt(E.getParameterByLength(L0)) |
| | p1 = E.valueAt(E.getParameterByLength(L1)) |
| |
|
| | diff0 = zMax - p0.z |
| | diff1 = zMax - p1.z |
| |
|
| | |
| | if diff0 > diff1: |
| | |
| | L0 += adj |
| | elif diff0 < diff1: |
| | |
| | L1 -= adj |
| | else: |
| | L0 += adj |
| | L1 -= adj |
| |
|
| | cnt += 1 |
| |
|
| | midLen = (L0 + L1) / 2 |
| | return E.valueAt(E.getParameterByLength(midLen)) |
| |
|
| | def _getVectorAngle(self, v): |
| | return math.atan2(v.y, v.x) % (2 * math.pi) |
| |
|
| | def _getCutSidePoints(self, obj, v0, v1, a1, a2, b1, b2): |
| | ea1 = Part.makeLine(v0, a1) |
| | ea2 = Part.makeLine(a1, a2) |
| | ea3 = Part.makeLine(a2, v1) |
| | ea4 = Part.makeLine(v1, v0) |
| | boxA = Part.Face(Part.Wire([ea1, ea2, ea3, ea4])) |
| | cubeA = boxA.extrude(FreeCAD.Vector(0, 0, 1)) |
| | cmnA = self.base.Shape.common(cubeA) |
| | eb1 = Part.makeLine(v0, b1) |
| | eb2 = Part.makeLine(b1, b2) |
| | eb3 = Part.makeLine(b2, v1) |
| | eb4 = Part.makeLine(v1, v0) |
| | boxB = Part.Face(Part.Wire([eb1, eb2, eb3, eb4])) |
| | cubeB = boxB.extrude(FreeCAD.Vector(0, 0, 1)) |
| | cmnB = self.base.Shape.common(cubeB) |
| | if cmnA.Volume > cmnB.Volume: |
| | return (b1, b2) |
| | return (a1, a2) |
| |
|
| | def _getBottomEdge(self, shape): |
| | EDGES = list() |
| | |
| | eCnt = len(shape.Edges) |
| | eZMin = shape.BoundBox.ZMin |
| | for ei in range(0, eCnt): |
| | E = shape.Edges[ei] |
| | if abs(E.BoundBox.ZMax - eZMin) < 0.00000001: |
| | EDGES.append(E) |
| | if len(EDGES) == 1: |
| | return EDGES[0] |
| | return False |
| |
|
| | def _getVertFaceType(self, shape): |
| | bottom_edge = self._getBottomEdge(shape) |
| | if bottom_edge: |
| | return ("Edge", bottom_edge) |
| |
|
| | |
| | z_length = shape.BoundBox.ZLength |
| | extrude_vec = FreeCAD.Vector(0, 0, z_length * 2.2 + 10) |
| | extruded = shape.extrude(extrude_vec) |
| |
|
| | |
| | slice_z = shape.BoundBox.ZMin + extrude_vec.z / 2 |
| | slices = extruded.slice(FreeCAD.Vector(0, 0, 1), slice_z) |
| |
|
| | if not slices: |
| | return False |
| |
|
| | if (wire := slices[0]).isClosed() and (face := Part.Face(wire)) > 0: |
| | |
| | z_offset = shape.BoundBox.ZMin - face.BoundBox.ZMin |
| | face.translate(FreeCAD.Vector(0, 0, z_offset)) |
| | return ("Face", face) |
| | return ("Wire", wire) |
| |
|
| | def _makeReference1Enumerations(self, sub, single=False): |
| | """Customize Reference1 enumerations based on feature type.""" |
| | Path.Log.debug("_makeReference1Enumerations()") |
| | cat = sub[:4] |
| | if single: |
| | if cat == "Face": |
| | return ["Long Edge", "Short Edge"] |
| | elif cat == "Edge": |
| | return ["Long Edge"] |
| | elif cat == "Vert": |
| | return ["Vertex"] |
| | elif cat == "Vert": |
| | return ["Vertex"] |
| |
|
| | return ["Center of Mass", "Center of BoundBox", "Lowest Point", "Highest Point"] |
| |
|
| | def _makeReference2Enumerations(self, sub): |
| | """Customize Reference2 enumerations based on feature type.""" |
| | Path.Log.debug("_makeReference2Enumerations()") |
| | cat = sub[:4] |
| | if cat == "Vert": |
| | return ["Vertex"] |
| | return ["Center of Mass", "Center of BoundBox", "Lowest Point", "Highest Point"] |
| |
|
| | def _lineCollisionCheck(self, obj, p1, p2): |
| | """Model the swept volume of a linear tool move and check for collision with the model.""" |
| | rad = getattr(self.tool.Diameter, "Value", self.tool.Diameter) / 2 |
| | extVect = FreeCAD.Vector(0, 0, obj.StartDepth.Value - obj.FinalDepth.Value) |
| |
|
| | def make_cylinder(point): |
| | circle = Part.makeCircle(rad, point) |
| | face = Part.Face(Part.Wire(circle.Edges)) |
| | face.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - face.BoundBox.ZMin)) |
| | return face.extrude(extVect) |
| |
|
| | def make_rect_prism(p1, p2): |
| | toEnd = p2.sub(p1) |
| | if toEnd.Length == 0: |
| | return None |
| | perp = FreeCAD.Vector(-toEnd.y, toEnd.x, 0) |
| | if perp.Length == 0: |
| | return None |
| | perp.normalize() |
| | perp.multiply(rad) |
| |
|
| | v1, v2 = p1.add(perp), p1.sub(perp) |
| | v3, v4 = p2.sub(perp), p2.add(perp) |
| | edges = Part.__sortEdges__( |
| | [ |
| | Part.makeLine(v1, v2), |
| | Part.makeLine(v2, v3), |
| | Part.makeLine(v3, v4), |
| | Part.makeLine(v4, v1), |
| | ] |
| | ) |
| | face = Part.Face(Part.Wire(edges)) |
| | face.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - face.BoundBox.ZMin)) |
| | return face.extrude(extVect) |
| |
|
| | |
| | startShp = make_cylinder(p1) |
| | endShp = make_cylinder(p2) if p1 != p2 else None |
| | boxShp = make_rect_prism(p1, p2) |
| |
|
| | pathTravel = startShp |
| | if boxShp: |
| | pathTravel = pathTravel.fuse(boxShp) |
| | if endShp: |
| | pathTravel = pathTravel.fuse(endShp) |
| |
|
| | self._addDebugObject(pathTravel, "PathTravel") |
| |
|
| | try: |
| | cmn = self.base.Shape.common(pathTravel) |
| | return cmn.Volume > 1e-6 |
| | except Exception: |
| | Path.Log.debug("Failed to complete path collision check.") |
| | return False |
| |
|
| | def _arcCollisionCheck(self, obj, p1, p2, arcCenter, arcRadius): |
| | """Check for collision by modeling the swept volume of an arc toolpath.""" |
| |
|
| | def make_cylinder_at_point(point, radius, height, final_depth): |
| | circle = Part.makeCircle(radius, point) |
| | face = Part.Face(Part.Wire(circle.Edges)) |
| | face.translate(FreeCAD.Vector(0, 0, final_depth - face.BoundBox.ZMin)) |
| | return face.extrude(FreeCAD.Vector(0, 0, height)) |
| |
|
| | def make_arc_face(p1, p2, center, inner_radius, outer_radius): |
| | (pA, pB) = self._makeOffsetArc(p1, p2, center, inner_radius) |
| | arc_inside = Arcs.arcFrom2Pts(pA, pB, center) |
| |
|
| | (pC, pD) = self._makeOffsetArc(p1, p2, center, outer_radius) |
| | arc_outside = Arcs.arcFrom2Pts(pC, pD, center) |
| |
|
| | pa = FreeCAD.Vector(*arc_inside.Vertexes[0].Point[:2], 0) |
| | pb = FreeCAD.Vector(*arc_inside.Vertexes[1].Point[:2], 0) |
| | pc = FreeCAD.Vector(*arc_outside.Vertexes[1].Point[:2], 0) |
| | pd = FreeCAD.Vector(*arc_outside.Vertexes[0].Point[:2], 0) |
| |
|
| | e1 = Part.makeLine(pb, pc) |
| | e2 = Part.makeLine(pd, pa) |
| | edges = Part.__sortEdges__([arc_inside, e1, arc_outside, e2]) |
| | return Part.Face(Part.Wire(edges)) |
| |
|
| | |
| | rad = getattr(self.tool.Diameter, "Value", self.tool.Diameter) / 2 |
| | extVect = FreeCAD.Vector(0, 0, obj.StartDepth.Value - obj.FinalDepth.Value) |
| |
|
| | if self.isArc == 1: |
| | |
| | outer = Part.Face(Part.Wire(Part.makeCircle(arcRadius + rad, arcCenter).Edges)) |
| | iRadius = arcRadius - rad |
| | path = ( |
| | outer.cut(Part.Face(Part.Wire(Part.makeCircle(iRadius, arcCenter).Edges))) |
| | if iRadius > 0 |
| | else outer |
| | ) |
| | path.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - path.BoundBox.ZMin)) |
| | pathTravel = path.extrude(extVect) |
| |
|
| | else: |
| | |
| | startShp = make_cylinder_at_point(p1, rad, extVect.z, obj.FinalDepth.Value) |
| | endShp = make_cylinder_at_point(p2, rad, extVect.z, obj.FinalDepth.Value) |
| |
|
| | |
| | inner_radius = arcRadius - rad |
| | if inner_radius <= 0: |
| | FreeCAD.Console.PrintError( |
| | translate("CAM_Slot", "Current offset value produces negative radius.") + "\n" |
| | ) |
| | return False |
| |
|
| | |
| | outer_radius = arcRadius + rad |
| | if outer_radius <= 0: |
| | FreeCAD.Console.PrintError( |
| | translate("CAM_Slot", "Current offset value produces negative radius.") + "\n" |
| | ) |
| | return False |
| |
|
| | rectFace = make_arc_face(p1, p2, arcCenter, inner_radius, outer_radius) |
| | rectFace.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - rectFace.BoundBox.ZMin)) |
| | arcShp = rectFace.extrude(extVect) |
| |
|
| | pathTravel = startShp.fuse(arcShp).fuse(endShp) |
| |
|
| | self._addDebugObject(pathTravel, "PathTravel") |
| |
|
| | try: |
| | cmn = self.base.Shape.common(pathTravel) |
| | return cmn.Volume > 1e-6 |
| | except Exception: |
| | Path.Log.debug("Failed to complete path collision check.") |
| | return False |
| |
|
| | def _addDebugObject(self, objShape, objName): |
| | if self.showDebugObjects: |
| | do = FreeCAD.ActiveDocument.addObject("Part::Feature", "tmp_" + objName) |
| | do.Shape = objShape |
| | do.purgeTouched() |
| | self.tmpGrp.addObject(do) |
| |
|
| |
|
| | |
| |
|
| |
|
| | def SetupProperties(): |
| | """SetupProperties() ... Return list of properties required for operation.""" |
| | return [tup[1] for tup in ObjectSlot.opPropertyDefinitions(False)] |
| |
|
| |
|
| | def Create(name, obj=None, parentJob=None): |
| | """Create(name) ... Creates and returns a Slot operation.""" |
| | if obj is None: |
| | obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) |
| | obj.Proxy = ObjectSlot(obj, name, parentJob) |
| | return obj |
| |
|