# -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2020 Russell Johnson (russ4262) * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # * This program is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # * GNU Library General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with this program; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * # *************************************************************************** from __future__ import print_function __title__ = "Path Slot Operation" __author__ = "russ4262 (Russell Johnson)" __url__ = "http://www.freecadweb.org" __doc__ = "Class and implementation of Slot operation." __contributors__ = "" import FreeCAD from PySide import QtCore import Path import PathScripts.PathLog as PathLog import PathScripts.PathUtils as PathUtils import PathScripts.PathOp as PathOp import math # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader Part = LazyLoader('Part', globals(), 'Part') if FreeCAD.GuiUp: import FreeCADGui DEBUG = False if DEBUG: PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) PathLog.trackModule(PathLog.thisModule()) else: PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) # Qt translation handling def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) 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) # Initialize operation-specific properties # For debugging if PathLog.getLevel(PathLog.thisModule()) != 4: obj.setEditorMode('ShowTempObjects', 2) # hide if not hasattr(obj, 'DoNotSetDefaultValues'): self.opSetEditorModes(obj) def initOpProperties(self, obj, warn=False): '''initOpProperties(obj) ... create operation specific properties''' 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) # Set enumeration lists for enumeration properties if len(self.addNewProps) > 0: ENUMS = self.opPropertyEnumerations() # ENUMS = self.getActiveEnumerations(obj) for n in ENUMS: if n in self.addNewProps: setattr(obj, n, ENUMS[n]) if warn: newPropMsg = translate('PathSlot', 'New property added to') newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + '. ' newPropMsg += translate('PathSlot', '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 path construction objects when module is in DEBUG mode.")), ("App::PropertyVectorDistance", "CustomPoint1", "Slot", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enter custom start point for slot path.")), ("App::PropertyVectorDistance", "CustomPoint2", "Slot", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enter custom end point for slot path.")), ("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 path, negative shortens.")), ("App::PropertyDistance", "ExtendPathEnd", "Slot", QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive extends the end of the path, negative shortens.")), ("App::PropertyEnumeration", "LayerMode", "Slot", QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), ("App::PropertyEnumeration", "PathOrientation", "Slot", QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose the path 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/circlular edges, offset the radius for the path.")), ("App::PropertyBool", "ReverseDirection", "Slot", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable to reverse the cut direction of the slot path.")), ("App::PropertyVectorDistance", "StartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")), ("App::PropertyBool", "UseStartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) ] def opPropertyEnumerations(self): # Enumeration lists for App::PropertyEnumeration properties return { 'CutPattern': ['Line', 'ZigZag'], 'LayerMode': ['Single-pass', 'Multi-pass'], 'PathOrientation': ['Start to End', 'Perpendicular'], 'Reference1': ['Center of Mass', 'Center of BoundBox', 'Lowest Point', 'Highest Point', 'Long Edge', 'Short Edge', 'Vertex'], 'Reference2': ['Center of Mass', 'Center of BoundBox', 'Lowest Point', 'Highest Point', 'Vertex'] } 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.0, 0.0), 'ExtendPathStart': 0.0, 'Reference1': 'Center of Mass', 'CustomPoint2': FreeCAD.Vector(10.0, 10.0, 0.0), 'ExtendPathEnd': 0.0, 'Reference2': 'Center of Mass', 'LayerMode': 'Multi-pass', 'CutPattern': 'ZigZag', 'PathOrientation': 'Start to End', 'ExtendRadius': 0.0, 'ReverseDirection': False, # For debugging 'ShowTempObjects': False } return defaults def getActiveEnumerations(self, obj): """getActiveEnumerations(obj) ... Method returns dictionary of property enumerations based on active conditions in the operation.""" ENUMS = self.opPropertyEnumerations() if hasattr(obj, 'Base'): if obj.Base: (base, subsList) = obj.Base[0] subCnt = len(subsList) if subCnt == 1: # Adjust available enumerations ENUMS['Reference1'] = self._makeReference1Enumerations(subsList[0], True) elif subCnt == 2: # Adjust available enumerations 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.""" PathLog.debug('updateEnumerations()') # Save existing values pre_Ref1 = obj.Reference1 pre_Ref2 = obj.Reference2 # Update enumerations ENUMS = self.getActiveEnumerations(obj) obj.Reference1 = ENUMS['Reference1'] obj.Reference2 = ENUMS['Reference2'] # Restore pre-existing values if available with active enumerations. # If not, set to first element in active enumeration list. 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): # Used to hide inputs in properties list A = B = 2 C = 0 if hasattr(obj, 'Base'): if obj.Base: (base, subsList) = obj.Base[0] 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) 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 PathLog.getLevel(PathLog.thisModule()) != 4 else 0 obj.setEditorMode('ShowTempObjects', mode) # Repopulate enumerations in case of changes 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]) # set the enumerations list if restore: setattr(obj, n, val) # restore the value self.opSetEditorModes(obj) def opApplyPropertyDefaults(self, obj, job, propList): # Set standard property defaults 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: propVal = getattr(prop, 'Value') 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) # need to overwrite the default depth calculations for facing d = None if job: if job.Stock: d = PathUtils.guessDepths(job.Stock.Shape, None) PathLog.debug("job.Stock exists") else: PathLog.debug("job.Stock NOT exist") else: PathLog.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 PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value)) PathLog.debug('Defualt 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: PathLog.error(e) obj.OpFinalDepth = zmin def opExecute(self, obj): '''opExecute(obj) ... process surface operation''' PathLog.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.stockZMin = None self.isArc = 0 self.arcCenter = None self.arcMidPnt = None self.arcRadius = 0.0 self.newRadius = 0.0 self.isDebug = False if PathLog.getLevel(PathLog.thisModule()) != 4 else True self.showDebugObjects = obj.ShowTempObjects self.stockZMin = self.job.Stock.Shape.BoundBox.ZMin CMDS = list() FCAD = FreeCAD.ActiveDocument try: dotIdx = __name__.index('.') + 1 except Exception: dotIdx = 0 self.module = __name__[dotIdx:] if not self.isDebug: self.showDebugObjects = False if self.showDebugObjects: for grpNm in ['tmpDebugGrp', 'tmpDebugGrp001']: if hasattr(FreeCAD.ActiveDocument, grpNm): for go in FreeCAD.ActiveDocument.getObject(grpNm).Group: FreeCAD.ActiveDocument.removeObject(go.Name) FreeCAD.ActiveDocument.removeObject(grpNm) self.tmpGrp = FreeCAD.ActiveDocument.addObject('App::DocumentObjectGroup', 'tmpDebugGrp') tmpGrpNm = self.tmpGrp.Name # self.updateEnumerations(obj) # Begin GCode for operation with basic information # ... and move cutter to clearance height and startpoint tool = obj.ToolController.Tool toolType = tool.ToolType if hasattr(tool, 'ToolType') else tool.ShapeName output = '' if obj.Comment != '': self.commandlist.append(Path.Command('N ({})'.format(obj.Comment), {})) self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {})) self.commandlist.append(Path.Command('N (Tool type: {})'.format(toolType), {})) self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(tool.Diameter), {})) self.commandlist.append(Path.Command('N ({})'.format(output), {})) self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) if obj.UseStartPoint is True: self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid})) # Impose property limits self.opApplyPropertyLimits(obj) # Calculate default depthparams for operation self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) # ###### MAIN COMMANDS FOR OPERATION ###### cmds = self._makeOperation(obj) if cmds: CMDS.extend(cmds) # Save gcode produced CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) self.commandlist.extend(CMDS) # ###### CLOSING COMMANDS FOR OPERATION ###### # Hide the temporary objects if self.showDebugObjects: if FreeCAD.GuiUp: import FreeCADGui FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False self.tmpGrp.purgeTouched() return True # Control methods for operation def _makeOperation(self, obj): """This method controls the overall slot creation process.""" pnts = False featureCnt = 0 def eLen(E): return E.Length if not hasattr(obj, 'Base'): msg = translate('PathSlot', 'No Base Geometry object in the operation.') FreeCAD.Console.PrintError(msg + '\n') return False if not obj.Base: # Use custom inputs here p1 = obj.CustomPoint1 p2 = obj.CustomPoint2 if p1.z == p2.z: pnts = (p1, p2) else: msg = translate('PathSlot', 'Custom points not at same Z height.') FreeCAD.Console.PrintError(msg + '\n') return False if pnts: (p1, p2) = pnts else: baseGeom = obj.Base[0] base, subsList = baseGeom self.base = base lenSL = len(subsList) featureCnt = lenSL if lenSL == 1: PathLog.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: PathLog.debug('Reference 1: {}'.format(obj.Reference1)) PathLog.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, featureCnt) else: cmds = self._finishLine(obj, pnts, featureCnt) if cmds: return cmds return False def _finishArc(self, obj, pnts, featureCnt): """This method finishes an Arc Slot operation.""" PathLog.debug('arc center: {}'.format(self.arcCenter)) self._addDebugObject(Part.makeLine(self.arcCenter, self.arcMidPnt), 'CentToMidPnt') # PathLog.debug('Pre-offset points are:\np1 = {}\np2 = {}'.format(p1, p2)) if obj.ExtendRadius.Value != 0: # verify offset does not force radius < 0 newRadius = self.arcRadius + obj.ExtendRadius.Value PathLog.debug('arc radius: {}; offset radius: {}'.format(self.arcRadius, newRadius)) if newRadius <= 0: msg = translate('PathSlot', 'Current offset value is not possible.') FreeCAD.Console.PrintError(msg + '\n') return False else: (p1, p2) = pnts pnts = self._makeOffsetArc(p1, p2, self.arcCenter, newRadius) self.newRadius = newRadius else: PathLog.debug('arc radius: {}'.format(self.arcRadius)) self.newRadius = self.arcRadius # Apply path extension for arcs # PathLog.debug('Pre-extension points are:\np1 = {}\np2 = {}'.format(p1, p2)) if self.isArc == 1: # Complete circle if (obj.ExtendPathStart.Value != 0 or obj.ExtendPathEnd.Value != 0): msg = translate('PathSlot', 'No path extensions available for full circles.') FreeCAD.Console.PrintWarning(msg + '\n') else: # Arc segment # Apply extensions to slot path (p1, p2) = pnts begExt = obj.ExtendPathStart.Value endExt = obj.ExtendPathEnd.Value pnts = self._extendArcSlot(p1, p2, self.arcCenter, begExt, endExt) if not pnts: return False (p1, p2) = pnts # PathLog.error('Post-offset points are:\np1 = {}\np2 = {}'.format(p1, p2)) if self.isDebug: PathLog.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('PathSlot', 'operation collides with model.') FreeCAD.Console.PrintError(msg + '\n') # PathLog.warning('Unable to create G-code. _makeArcGCode() is incomplete.') cmds = self._makeArcGCode(obj, p1, p2) return cmds def _makeArcGCode(self, obj, p1, p2): """This method is the last in the overall slot creation process. It accepts the operation object and two end points for the path. It returns the slot gcode for the operation.""" CMDS = list() PATHS = [(p1, p2, 'G2'), (p2, p1, 'G3')] def arcPass(PNTS, depth): cmds = list() (p1, p2, cmd) = PNTS # cmds.append(Path.Command('N (Tool type: {})'.format(toolType), {})) cmds.append(Path.Command('G0', {'X': p1.x, 'Y': p1.y, 'F': self.horizRapid})) cmds.append(Path.Command('G1', {'Z': depth, 'F': self.vertFeed})) vtc = self.arcCenter.sub(p1) # vector to center cmds.append( Path.Command(cmd, {'X': p2.x, 'Y': p2.y, 'I': vtc.x, 'J': vtc.y, 'F': self.horizFeed })) return cmds if obj.LayerMode == 'Single-pass': PNTS = PATHS[0] if obj.ReverseDirection: PNTS = PATHS[1] CMDS.extend(arcPass(PNTS, obj.FinalDepth.Value)) else: if obj.CutPattern == 'Line': PNTS = PATHS[0] if obj.ReverseDirection: PNTS = PATHS[1] for dep in self.depthParams: CMDS.extend(arcPass(PNTS, dep)) CMDS.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) elif obj.CutPattern == 'ZigZag': i = 0 for dep in self.depthParams: if i % 2.0 == 0: # even CMDS.extend(arcPass(PATHS[0], dep)) else: # odd CMDS.extend(arcPass(PATHS[1], dep)) i += 1 # Raise to SafeHeight when finished CMDS.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) return CMDS def _finishLine(self, obj, pnts, featureCnt): """This method finishes an Line Slot operation.""" # Apply perpendicular rotation if requested 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 if perpZero: (p1, p2) = pnts pnts = self._makePerpendicular(p1, p2, 10.0) # 10.0 offset below else: perpZero = False # Reverse direction of path if requested if obj.ReverseDirection: (p2, p1) = pnts else: (p1, p2) = pnts # Apply extensions to slot path begExt = obj.ExtendPathStart.Value endExt = obj.ExtendPathEnd.Value if perpZero: # Offsets for 10.0 value above in _makePerpendicular() begExt -= 5.0 endExt -= 5.0 pnts = self._extendLineSlot(p1, p2, begExt, endExt) if not pnts: return False (p1, p2) = pnts if self.isDebug: PathLog.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('PathSlot', '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 slot creation process. It accepts the operation object and two end points for the path. It returns the slot gcode for the operation.""" CMDS = list() def linePass(p1, p2, depth): cmds = list() # cmds.append(Path.Command('N (Tool type: {})'.format(toolType), {})) 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 # CMDS.append(Path.Command('N (Tool type: {})'.format(toolType), {})) 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 == 0: # even 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: # odd 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 # Methods for processing single geometry def _processSingle(self, obj, shape_1, sub1): """This is the control method for slots based on a single Base Geometry feature.""" cmds = False make = False cat1 = sub1[:4] if cat1 == 'Face': pnts = False norm = shape_1.normalAt(0.0, 0.0) PathLog.debug('{}.normalAt(): {}'.format(sub1, norm)) 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: msg = translate('PathSlot', 'The selected face is not oriented horizontally or vertically.') FreeCAD.Console.PrintError(msg + '\n') return False if pnts: (p1, p2) = pnts make = True elif cat1 == 'Edge': PathLog.debug('Single edge') pnts = self._processSingleEdge(obj, shape_1) if pnts: (p1, p2) = pnts make = True elif cat1 == 'Vert': msg = translate('PathSlot', 'Only a vertex selected. Add another feature to the Base Geometry.') FreeCAD.Console.PrintError(msg + '\n') if make: return (p1, p2) return False def _processSingleHorizFace(self, obj, shape): """Determine slot path endpoints from a single horizontally oriented face.""" PathLog.debug('_processSingleHorizFace()') lineTypes = ['Part::GeomLine'] def getRadians(self, E): vect = self._dXdYdZ(E) norm = self._normalizeVector(vect) rads = self._xyToRadians(norm) deg = math.degrees(rads) if deg >= 180.0: deg -= 180.0 return deg # Reject triangular faces if len(shape.Edges) < 4: msg = translate('PathSlot', 'A single selected face must have four edges minimum.') FreeCAD.Console.PrintError(msg + '\n') return False # Create tuples as (edge index, length, angle) eTups = list() for i in range(0, 4): eTups.append((i, shape.Edges[i].Length, getRadians(self, shape.Edges[i])) ) # Sort tuples by edge angle eTups.sort(key=lambda tup: tup[2]) # Identify parallel edges pairs = list() eCnt = len(shape.Edges) lstE = eCnt - 1 for i in range(0, eCnt): if i < lstE: ni = i + 1 A = eTups[i] B = eTups[ni] if abs(A[2] - B[2]) < 0.00000001: # test slopes(yaw angles) debug = False eA = shape.Edges[A[0]] eB = shape.Edges[B[0]] if eA.Curve.TypeId not in lineTypes: debug = eA.Curve.TypeId if not debug: if eB.Curve.TypeId not in lineTypes: debug = eB.Curve.TypeId else: pairs.append((eA, eB)) if debug: msg = 'Erroneous Curve.TypeId: {}'.format(debug) PathLog.debug(msg) pairCnt = len(pairs) if pairCnt > 1: pairs.sort(key=lambda tup: tup[0].Length, reverse=True) if self.isDebug: PathLog.debug(' -pairCnt: {}'.format(pairCnt)) for (a, b) in pairs: PathLog.debug(' -pair: {}, {}'.format(round(a.Length, 4), round(b.Length,4))) if pairCnt == 0: msg = translate('PathSlot', 'No parallel edges identified.') FreeCAD.Console.PrintError(msg + '\n') return False elif pairCnt == 1: same = pairs[0] else: if obj.Reference1 == 'Long Edge': same = pairs[1] elif obj.Reference1 == 'Short Edge': same = pairs[0] else: msg = 'Reference1 ' msg += translate('PathSlot', 'value error.') FreeCAD.Console.PrintError(msg + '\n') return False (p1, p2) = self._getOppMidPoints(same) return (p1, p2) def _processSingleComplexFace(self, obj, shape): """Determine slot path endpoints from a single complex face.""" PathLog.debug('_processSingleComplexFace()') PNTS = list() def zVal(V): return V.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.""" PathLog.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.0) # Create offset endpoints for raw slot path 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) return (p1, p2) def _processSingleEdge(self, obj, edge): """Determine slot path endpoints from a single horizontally oriented face.""" PathLog.debug('_processSingleEdge()') tolrnc = 0.0000001 lineTypes = ['Part::GeomLine'] curveTypes = ['Part::GeomCircle'] def oversizedTool(holeDiam): # Test if tool larger than opening if self.tool.Diameter > holeDiam: msg = translate('PathSlot', 'Current tool larger than arc diameter.') FreeCAD.Console.PrintError(msg + '\n') return True return False def isHorizontal(z1, z2, z3): # Check that all Z values are equal (isRoughly same) if (abs(z1 - z2) > tolrnc or abs(z1 - z3) > tolrnc or abs(z2 - z3) > tolrnc): return False return True def circleCentFrom3Points(P1, P2, P3): # Source code for this function copied from: # https://wiki.freecadweb.org/Macro_Draft_Circle_3_Points_3D P1P2 = (P2 - P1).Length P2P3 = (P3 - P2).Length P3P1 = (P1 - P3).Length # Circle radius. l = ((P1 - P2).cross(P2 - P3)).Length try: r = P1P2 * P2P3 * P3P1 / 2 / l except: PathLog.error("The three points are aligned.") return False else: # Sphere center. a = P2P3**2 * (P1 - P2).dot(P1 - P3) / 2 / l**2 b = P3P1**2 * (P2 - P1).dot(P2 - P3) / 2 / l**2 c = P1P2**2 * (P3 - P1).dot(P3 - P2) / 2 / l**2 P1.multiply(a) P2.multiply(b) P3.multiply(c) PC = P1 + P2 + P3 return PC # Process edge based on curve type if edge.Curve.TypeId in lineTypes: V1 = edge.Vertexes[0] V2 = edge.Vertexes[1] p1 = FreeCAD.Vector(V1.X, V1.Y, 0.0) p2 = FreeCAD.Vector(V2.X, V2.Y, 0.0) return (p1, p2) elif edge.Curve.TypeId in curveTypes: if len(edge.Vertexes) == 1: # Circle edge PathLog.debug('Arc with single vertex.') if oversizedTool(edge.BoundBox.XLength): return False self.isArc = 1 V1 = edge.Vertexes[0] 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 cent = edge.BoundBox.Center self.arcCenter = FreeCAD.Vector(cent.x, cent.y, 0.0) midPnt = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0)) self.arcMidPnt = FreeCAD.Vector(midPnt.x, midPnt.y, 0.0) self.arcRadius = edge.BoundBox.XLength / 2.0 p1 = FreeCAD.Vector(V1.X, V1.Y, 0.0) p2 = FreeCAD.Vector(V1.X, V1.Y, 0.0) else: # Arc edge PathLog.debug('Arc with multiple vertices.') self.isArc = 2 V1 = edge.Vertexes[0] V2 = edge.Vertexes[1] midPnt = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0)) if not isHorizontal(V1.Z, V2.Z, midPnt.z): return False p1 = FreeCAD.Vector(V1.X, V1.Y, 0.0) p2 = FreeCAD.Vector(V2.X, V2.Y, 0.0) # Duplicate points required because # circleCentFrom3Points() alters original arguments pA = FreeCAD.Vector(V1.X, V1.Y, 0.0) pB = FreeCAD.Vector(V2.X, V2.Y, 0.0) pC = FreeCAD.Vector(midPnt.x, midPnt.y, 0.0) cCF3P = circleCentFrom3Points(pA, pB, pC) if not cCF3P: return False self.arcMidPnt = FreeCAD.Vector(midPnt.x, midPnt.y, 0.0) self.arcCenter = cCF3P self.arcRadius = p1.sub(cCF3P).Length if oversizedTool(self.arcRadius * 2.0): return False return (p1, p2) # Methods for processing double geometry def _processDouble(self, obj, shape_1, sub1, shape_2, sub2): PathLog.debug('_processDouble()') """This is the control method for slots based on a two Base Geometry features.""" cmds = False make = False cat2 = sub2[:4] p1 = None p2 = None dYdX1 = None dYdX2 = None self.bottomEdges = list() feature1 = self._processFeature(obj, shape_1, sub1, 1) if not feature1: msg = translate('PathSlot', '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('PathSlot', '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 # Parallel check for twin face, and face-edge cases if dYdX1 and dYdX2: if not self._isParallel(dYdX1, dYdX2): PathLog.debug('dYdX1, dYdX2: {}, {}'.format(dYdX1, dYdX2)) msg = translate('PathSlot', 'Selected geometry not parallel.') FreeCAD.Console.PrintError(msg + '\n') return False if p2: return (p1, p2) return False # Support methods 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): posTol = 0.0000000001 negTol = -1 * posTol V = FreeCAD.Vector(v.x, v.y, v.z) V.normalize() x = V.x y = V.y z = V.z if V.x != 0 and abs(V.x) < posTol: x = 0.0 if V.x != 1 and 1.0 - V.x < posTol: x = 1.0 if V.x != -1 and -1.0 - V.x > negTol: x = -1.0 if V.y != 0 and abs(V.y) < posTol: y = 0.0 if V.y != 1 and 1.0 - V.y < posTol: y = 1.0 if V.y != -1 and -1.0 - V.y > negTol: y = -1.0 if V.z != 0 and abs(V.z) < posTol: z = 0.0 if V.z != 1 and 1.0 - V.z < posTol: z = 1.0 if V.z != -1 and -1.0 - V.z > negTol: z = -1.0 return FreeCAD.Vector(x, y, z) def _getLowestPoint(self, shape_1): # find lowest vertex vMin = shape_1.Vertexes[0] zmin = vMin.Z same = [vMin] for V in shape_1.Vertexes: if V.Z < zmin: zmin = V.Z vMin = V elif V.Z == zmin: same.append(V) if len(same) > 1: X = [E.X for E in same] Y = [E.Y for E in same] avgX = sum(X) / len(X) avgY = sum(Y) / len(Y) return FreeCAD.Vector(avgX, avgY, zmin) else: return FreeCAD.Vector(V.X, V.Y, V.Z) def _getHighestPoint(self, shape_1): # find highest vertex vMax = shape_1.Vertexes[0] zmax = vMax.Z same = [vMax] for V in shape_1.Vertexes: if V.Z > zmax: zmax = V.Z vMax = V elif V.Z == zmax: same.append(V) if len(same) > 1: X = [E.X for E in same] Y = [E.Y for E in same] avgX = sum(X) / len(X) avgY = sum(Y) / len(Y) return FreeCAD.Vector(avgX, avgY, zmax) else: return FreeCAD.Vector(V.X, V.Y, V.Z) def _processFeature(self, obj, shape, sub, pNum): p = None dYdX = None cat = sub[:4] Ref = getattr(obj, 'Reference' + str(pNum)) if cat == 'Face': BE = self._getBottomEdge(shape) if BE: self.bottomEdges.append(BE) # calculate slope of face V0 = shape.Vertexes[0] v1 = shape.CenterOfMass temp = FreeCAD.Vector(v1.x - V0.X, v1.y - V0.Y, 0.0) dYdX = self._normalizeVector(temp) # Determine normal vector for face norm = shape.normalAt(0.0, 0.0) # FreeCAD.Console.PrintMessage('{} normal {}.\n'.format(sub, norm)) if norm.z != 0: msg = translate('PathSlot', 'The selected face is not oriented vertically:') FreeCAD.Console.PrintError(msg + ' {}.\n'.format(sub)) return False if Ref == 'Center of Mass': comS = shape.CenterOfMass p = FreeCAD.Vector(comS.x, comS.y, 0.0) elif Ref == 'Center of BoundBox': comS = shape.BoundBox.Center p = FreeCAD.Vector(comS.x, comS.y, 0.0) elif Ref == 'Lowest Point': p = self._getLowestPoint(shape) elif Ref == 'Highest Point': p = self._getHighestPoint(shape) elif cat == 'Edge': # calculate slope between end vertexes v0 = shape.Edges[0].Vertexes[0] v1 = shape.Edges[0].Vertexes[1] temp = FreeCAD.Vector(v1.X - v0.X, v1.Y - v0.Y, 0.0) dYdX = self._normalizeVector(temp) if Ref == 'Center of Mass': comS = shape.CenterOfMass p = FreeCAD.Vector(comS.x, comS.y, 0.0) elif Ref == 'Center of BoundBox': comS = shape.BoundBox.Center p = FreeCAD.Vector(comS.x, comS.y, 0.0) elif Ref == 'Lowest Point': p = self._findLowestPointOnEdge(shape) elif Ref == 'Highest Point': p = self._findHighestPointOnEdge(shape) elif cat == 'Vert': V = shape.Vertexes[0] p = FreeCAD.Vector(V.X, V.Y, 0.0) if p: return (p, dYdX, cat) return False def _extendArcSlot(self, p1, p2, cent, begExt, endExt): cancel = True n1 = p1 n2 = p2 def getArcLine(length, rads): rads = abs(length / self.newRadius) x = self.newRadius * math.cos(rads) y = self.newRadius * math.sin(rads) a = FreeCAD.Vector(self.newRadius, 0.0, 0.0) b = FreeCAD.Vector(x, y, 0.0) c = FreeCAD.Vector(0.0, 0.0, 0.0) return Part.makeLine(a, b) if begExt or endExt: cancel = False if cancel: return (p1, p2) # Convert extension to radians origin = FreeCAD.Vector(0.0, 0.0, 0.0) if begExt: # Create arc representing extension rads = abs(begExt / self.newRadius) line = getArcLine(begExt, rads) rotToRads = self._xyToRadians(p1.sub(self.arcCenter)) if begExt < 1: rotToRads -= rads rotToDeg = math.degrees(rotToRads) # PathLog.debug('begExt angles are: {}, {}'.format(rotToRads, rotToDeg)) line.rotate(origin, FreeCAD.Vector(0, 0, 1), rotToDeg) line.translate(self.arcCenter) self._addDebugObject(line, 'ExtendStart') v1 = line.Vertexes[1] if begExt < 1: v1 = line.Vertexes[0] n1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) if endExt: # Create arc representing extension rads = abs(endExt / self.newRadius) line = getArcLine(endExt, rads) rotToRads = self._xyToRadians(p2.sub(self.arcCenter)) - rads if endExt < 1: rotToRads += rads rotToDeg = math.degrees(rotToRads) # PathLog.debug('endExt angles are: {}, {}'.format(rotToRads, rotToDeg)) line.rotate(origin, FreeCAD.Vector(0, 0, 1), rotToDeg) line.translate(self.arcCenter) self._addDebugObject(line, 'ExtendEnd') v1 = line.Vertexes[0] if endExt < 1: v1 = line.Vertexes[1] n2 = FreeCAD.Vector(v1.X, v1.Y, 0.0) return (n1, n2) def _makeOffsetArc(self, p1, p2, center, newRadius): n1 = p1.sub(center).normalize() n2 = p2.sub(center).normalize() n1.multiply(newRadius) n2.multiply(newRadius) p1 = n1.add(center) p2 = n2.add(center) return (p1, p2) def _extendLineSlot(self, p1, p2, begExt, endExt): if begExt: beg = p1.sub(p2) beg.normalize() beg.multiply(begExt) n1 = p1.add(beg) else: n1 = p1 if endExt: end = p2.sub(p1) end.normalize() end.multiply(endExt) n2 = p2.add(end) else: n2 = p2 return (n1, n2) def _getOppMidPoints(self, same): # Find mid-points between ends of equal, oppossing edges com1 = same[0].CenterOfMass com2 = same[1].CenterOfMass p1 = FreeCAD.Vector(com1.x, com1.y, 0.0) p2 = FreeCAD.Vector(com2.x, com2.y, 0.0) return (p1, p2) def _isParallel(self, dYdX1, dYdX2): if dYdX1.add(dYdX2).Length == 0: return True if ((dYdX1.x + dYdX2.x) / 2.0 == dYdX1.x and (dYdX1.y + dYdX2.y) / 2.0 == dYdX1.y): return True return False def _makePerpendicular(self, p1, p2, length): line = Part.makeLine(p1, p2) midPnt = line.CenterOfMass halfDist = length / 2.0 if self.dYdX1: half = FreeCAD.Vector(self.dYdX1.x, self.dYdX1.y, 0.0).multiply(halfDist) n1 = midPnt.add(half) n2 = midPnt.sub(half) return (n1, n2) elif self.dYdX2: half = FreeCAD.Vector(self.dYdX2.x, self.dYdX2.y, 0.0).multiply(halfDist) n1 = midPnt.add(half) n2 = midPnt.sub(half) return (n1, n2) else: toEnd = p2.sub(p1) factor = halfDist / toEnd.Length perp = FreeCAD.Vector(-1 * toEnd.y, toEnd.x, 0.0) perp.normalize() perp.multiply(halfDist) n1 = midPnt.add(perp) n2 = midPnt.sub(perp) return (n1, n2) def _findLowestPointOnEdge(self, E): tol = 0.0000001 zMin = E.BoundBox.ZMin # Test first vertex v = E.Vertexes[0] if abs(v.Z - zMin) < tol: return FreeCAD.Vector(v.X, v.Y, v.Z) # Test second vertex v = E.Vertexes[1] if abs(v.Z - zMin) < tol: return FreeCAD.Vector(v.X, v.Y, v.Z) # Test middle point of edge eMidLen = E.Length / 2.0 eMidPnt = E.valueAt(E.getParameterByLength(eMidLen)) if abs(eMidPnt.z - zMin) < tol: return eMidPnt if E.BoundBox.ZLength < 0.000000001: # roughly horizontal edge return eMidPnt return self._findLowestEdgePoint(E) def _findLowestEdgePoint(self, E): zMin = E.BoundBox.ZMin eLen = E.Length L0 = 0 L1 = eLen p0 = None p1 = None cnt = 0 while L1 - L0 > 0.00001 and cnt < 2000: adj = (L1 - L0) * 0.1 # Get points at L0 and L1 along edge p0 = E.valueAt(E.getParameterByLength(L0)) p1 = E.valueAt(E.getParameterByLength(L1)) # Adjust points based on proximity to target depth diff0 = p0.z - zMin diff1 = p1.z - zMin if diff0 < diff1: L1 -= adj elif diff0 > diff1: L0 += adj else: L0 += adj L1 -= adj cnt += 1 midLen = (L0 + L1) / 2.0 return E.valueAt(E.getParameterByLength(midLen)) def _findHighestPointOnEdge(self, E): tol = 0.0000001 zMax = E.BoundBox.ZMax # Test first vertex v = E.Vertexes[0] if abs(zMax - v.Z) < tol: return FreeCAD.Vector(v.X, v.Y, v.Z) # Test second vertex v = E.Vertexes[1] if abs(zMax - v.Z) < tol: return FreeCAD.Vector(v.X, v.Y, v.Z) # Test middle point of edge eMidLen = E.Length / 2.0 eMidPnt = E.valueAt(E.getParameterByLength(eMidLen)) if abs(zMax - eMidPnt.z) < tol: return eMidPnt if E.BoundBox.ZLength < 0.000000001: # roughly horizontal edge return eMidPnt return self._findHighestEdgePoint(E) def _findHighestEdgePoint(self, E): zMax = E.BoundBox.ZMax eLen = E.Length L0 = 0 L1 = eLen p0 = None p1 = None cnt = 0 while L1 - L0 > 0.00001 and cnt < 2000: adj = (L1 - L0) * 0.1 # Get points at L0 and L1 along edge p0 = E.valueAt(E.getParameterByLength(L0)) p1 = E.valueAt(E.getParameterByLength(L1)) # Adjust points based on proximity to target depth diff0 = zMax - p0.z diff1 = zMax - p1.z if diff0 < diff1: L1 -= adj elif diff0 > diff1: L0 += adj else: L0 += adj L1 -= adj cnt += 1 midLen = (L0 + L1) / 2.0 return E.valueAt(E.getParameterByLength(midLen)) def _xyToRadians(self, v): # Assumes Z value of vector is zero halfPi = math.pi / 2 if v.y == 1 and v.x == 0: return halfPi if v.y == -1 and v.x == 0: return math.pi + halfPi if v.y == 0 and v.x == 1: return 0.0 if v.y == 0 and v.x == -1: return math.pi x = abs(v.x) y = abs(v.y) rads = math.atan(y/x) if v.x > 0: if v.y > 0: return rads else: return (2 * math.pi) - rads if v.x < 0: if v.y > 0: return math.pi - rads else: return math.pi + rads 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, 0.0, 1.0)) 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, 0.0, 1.0)) cmnB = self.base.Shape.common(cubeB) if cmnA.Volume > cmnB.Volume: return (b1, b2) return (a1, a2) def _getBottomEdge(self, shape): EDGES = list() # Determine if selected face has a single bottom horizontal edge 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: # single bottom horiz. edge return EDGES[0] return False def _getVertFaceType(self, shape): wires = list() bottomEdge = self._getBottomEdge(shape) if bottomEdge: return ('Edge', bottomEdge) # Extract cross-section of face extFwd = (shape.BoundBox.ZLength * 2.2) + 10 extShp = shape.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) sliceZ = shape.BoundBox.ZMin + (extFwd / 2.0) slcs = extShp.slice(FreeCAD.Vector(0, 0, 1), sliceZ) for i in slcs: wires.append(i) if len(wires) > 0: isFace = False csWire = wires[0] if wires[0].isClosed(): face = Part.Face(wires[0]) if face.Area > 0: face.translate(FreeCAD.Vector(0.0, 0.0, shape.BoundBox.ZMin - face.BoundBox.ZMin)) return ('Face', face) return ('Wire', wires[0]) return False def _makeReference1Enumerations(self, sub, single=False): """Customize Reference1 enumerations based on feature type.""" PathLog.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.""" PathLog.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): """Make simple circle with diameter of tool, at start point. Extrude it latterally along path. Extrude it vertically. Check for collision with model.""" # Make path travel of tool as 3D solid. rad = self.tool.Diameter / 2.0 def getPerp(p1, p2, dist): toEnd = p2.sub(p1) perp = FreeCAD.Vector(-1 * toEnd.y, toEnd.x, 0.0) if perp.x == 0 and perp.y == 0: return perp perp.normalize() perp.multiply(dist) return perp # Make first cylinder ce1 = Part.Wire(Part.makeCircle(rad, p1).Edges) C1 = Part.Face(ce1) zTrans = obj.FinalDepth.Value - C1.BoundBox.ZMin C1.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) extFwd = obj.StartDepth.Value - obj.FinalDepth.Value extVect = FreeCAD.Vector(0.0, 0.0, extFwd) startShp = C1.extrude(extVect) if p2.sub(p1).Length > 0: # Make second cylinder ce2 = Part.Wire(Part.makeCircle(rad, p2).Edges) C2 = Part.Face(ce2) zTrans = obj.FinalDepth.Value - C2.BoundBox.ZMin C2.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) endShp = C2.extrude(extVect) # Make extruded rectangle to connect cylinders perp = getPerp(p1, p2, rad) v1 = p1.add(perp) v2 = p1.sub(perp) v3 = p2.sub(perp) v4 = p2.add(perp) e1 = Part.makeLine(v1, v2) e2 = Part.makeLine(v2, v3) e3 = Part.makeLine(v3, v4) e4 = Part.makeLine(v4, v1) edges = Part.__sortEdges__([e1, e2, e3, e4]) rectFace = Part.Face(Part.Wire(edges)) zTrans = obj.FinalDepth.Value - rectFace.BoundBox.ZMin rectFace.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) boxShp = rectFace.extrude(extVect) # Fuse two cylinders and box together part1 = startShp.fuse(boxShp) pathTravel = part1.fuse(endShp) else: pathTravel = startShp self._addDebugObject(pathTravel, 'PathTravel') # Check for collision with model try: cmn = self.base.Shape.common(pathTravel) if cmn.Volume > 0.000001: return True except Exception: PathLog.debug('Failed to complete path collision check.') return False def _arcCollisionCheck(self, obj, p1, p2, arcCenter, arcRadius): """Make simple circle with diameter of tool, at start and end points. Make arch face between circles. Fuse and extrude it vertically. Check for collision with model.""" # Make path travel of tool as 3D solid. rad = self.tool.Diameter / 2.0 extFwd = obj.StartDepth.Value - obj.FinalDepth.Value extVect = FreeCAD.Vector(0.0, 0.0, extFwd) if self.isArc == 1: # full circular slot # make outer circle oCircle = Part.makeCircle(arcRadius + rad, arcCenter) oWire = Part.Wire(oCircle.Edges[0]) outer = Part.Face(oWire) # make inner circle iRadius = arcRadius - rad if iRadius > 0: iCircle = Part.makeCircle(iRadius, arcCenter) iWire = Part.Wire(iCircle.Edges[0]) inner = Part.Face(iWire) # Cut outer with inner path = outer.cut(inner) else: path = outer zTrans = obj.FinalDepth.Value - path.BoundBox.ZMin path.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) pathTravel = path.extrude(extVect) else: # arc slot # Make first cylinder ce1 = Part.Wire(Part.makeCircle(rad, p1).Edges) C1 = Part.Face(ce1) zTrans = obj.FinalDepth.Value - C1.BoundBox.ZMin C1.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) startShp = C1.extrude(extVect) # self._addDebugObject(startShp, 'StartCyl') # Make second cylinder ce2 = Part.Wire(Part.makeCircle(rad, p2).Edges) C2 = Part.Face(ce2) zTrans = obj.FinalDepth.Value - C2.BoundBox.ZMin C2.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) endShp = C2.extrude(extVect) # self._addDebugObject(endShp, 'EndCyl') # Make wire with inside and outside arcs, and lines on ends. # Convert wire to face, then extrude import draftgeoutils.arcs as Arcs # Arc 1 - inside # verify offset does not force radius < 0 newRadius = arcRadius - rad # PathLog.debug('arcRadius, newRadius: {}, {}'.format(arcRadius, newRadius)) if newRadius <= 0: msg = translate('PathSlot', 'Current offset value is not possible.') FreeCAD.Console.PrintError(msg + '\n') return False else: (pA, pB) = self._makeOffsetArc(p1, p2, arcCenter, newRadius) arc_inside = Arcs.arcFrom2Pts(pA, pB, arcCenter) # Arc 2 - outside # verify offset does not force radius < 0 newRadius = arcRadius + rad # PathLog.debug('arcRadius, newRadius: {}, {}'.format(arcRadius, newRadius)) if newRadius <= 0: msg = translate('PathSlot', 'Current offset value is not possible.') FreeCAD.Console.PrintError(msg + '\n') return False else: (pC, pD) = self._makeOffsetArc(p1, p2, arcCenter, newRadius) arc_outside = Arcs.arcFrom2Pts(pC, pD, arcCenter) # Make end lines to connect arcs vA = arc_inside.Vertexes[0] vB = arc_inside.Vertexes[1] vC = arc_outside.Vertexes[1] vD = arc_outside.Vertexes[0] pa = FreeCAD.Vector(vA.X, vA.Y, 0.0) pb = FreeCAD.Vector(vB.X, vB.Y, 0.0) pc = FreeCAD.Vector(vC.X, vC.Y, 0.0) pd = FreeCAD.Vector(vD.X, vD.Y, 0.0) # Make closed arch face and extrude e1 = Part.makeLine(pb, pc) e2 = Part.makeLine(pd, pa) edges = Part.__sortEdges__([arc_inside, e1, arc_outside, e2]) rectFace = Part.Face(Part.Wire(edges)) zTrans = obj.FinalDepth.Value - rectFace.BoundBox.ZMin rectFace.translate(FreeCAD.Vector(0.0, 0.0, zTrans)) boxShp = rectFace.extrude(extVect) # self._addDebugObject(boxShp, 'ArcBox') # Fuse two cylinders and box together part1 = startShp.fuse(boxShp) pathTravel = part1.fuse(endShp) self._addDebugObject(pathTravel, 'PathTravel') # Check for collision with model try: cmn = self.base.Shape.common(pathTravel) if cmn.Volume > 0.000001: return True except Exception: PathLog.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.tempGroup.addObject(do) # Eclass def SetupProperties(): ''' SetupProperties() ... Return list of properties required for operation.''' return [tup[1] for tup in ObjectSlot.opPropertyDefinitions(False)] def Create(name, obj=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) return obj