# -*- coding: utf-8 -*-
# ***************************************************************************
# *   Copyright (c) 2020 Russell Johnson (russ4262) <russ4262@gmail.com>    *
# *                                                                         *
# *   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')
Arcs = LazyLoader('draftgeoutils.arcs', globals(), 'draftgeoutils.arcs')
if FreeCAD.GuiUp:
    FreeCADGui = LazyLoader('FreeCADGui', globals(), 'FreeCADGui')


# Qt translation handling
def translate(context, text, disambig=None):
    return QtCore.QCoreApplication.translate(context, text, disambig)


PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# PathLog.trackModule(PathLog.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)  # 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]
                subsList = obj.Base[0][1]
                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]
                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)

    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 = False
        self.stockZMin = self.job.Stock.Shape.BoundBox.ZMin
        CMDS = list()

        try:
            dotIdx = __name__.index('.') + 1
        except Exception:
            dotIdx = 0
        self.module = __name__[dotIdx:]

        # Setup debugging group for temp objects, when in DEBUG mode
        if self.isDebug:
            self.showDebugObjects = obj.ShowTempObjects
        if self.showDebugObjects:
            FCAD = FreeCAD.ActiveDocument
            for grpNm in ['tmpDebugGrp', 'tmpDebugGrp001']:
                if hasattr(FCAD, grpNm):
                    for go in FCAD.getObject(grpNm).Group:
                        FCAD.removeObject(go.Name)
                    FCAD.removeObject(grpNm)
            self.tmpGrp = FCAD.addObject('App::DocumentObjectGroup', 'tmpDebugGrp')

        # 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:
                FreeCADGui.ActiveDocument.getObject(self.tmpGrp.Name).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
        featureCount = 0

        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

        baseGeom = obj.Base[0]
        base, subsList = baseGeom
        self.base = base

        featureCount = len(subsList)
        if  featureCount == 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, 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."""
        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 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:
            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
            # invert endExt, begExt args to apply extensions to correct ends
            # XY geom is postitive CCW; Gcode postitive CW
            pnts = self._extendArcSlot(p1, p2, self.arcCenter, endExt, begExt)

        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 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('N (Tool type: {})'.format(toolType), {}))
            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)  # vector to center
            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 == 0:  # even
                        CMDS.extend(arcPass(PATHS[path_index], depth))
                    else:  # odd
                        CMDS.extend(arcPass(PATHS[not path_index], depth))
                    i += 1
        # Raise to SafeHeight when finished
        CMDS.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))

        if self.isDebug:
            PathLog.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."""
        # 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
                elif self.shapeType1 == 'Edge' and self.shapeType2 == 'Edge':
                    PathLog.debug('_finishLine() Perp, featureCnt == 2')
            if perpZero:
                (p1, p2) = pnts
                pnts = self._makePerpendicular(p1, p2, 10.0)  # 10.0 offset below
        else:
            # Modify path points if user selected two parallel edges
            if (featureCnt == 2 and self.shapeType1 == 'Edge' and 
                    self.shapeType2 == 'Edge' and self._isParallel(self.dYdX1, self.dYdX2)):
                (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.0 + set_length)  # 10.0 offset below
                if edg1_len != edg2_len:
                    msg = obj.Label + ' '
                    msg += translate('PathSlot',
                            'Verify slot path start and end points.')
                    FreeCAD.Console.PrintWarning(msg + '\n')
            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 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('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."""
        done = 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
                done = True

        elif cat1 == 'Edge':
            PathLog.debug('Single edge')
            pnts = self._processSingleEdge(obj, shape_1)
            if pnts:
                (p1, p2) = pnts
                done = True

        elif cat1 == 'Vert':
            msg = translate('PathSlot',
                '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."""
        PathLog.debug('_processSingleHorizFace()')
        lineTypes = ['Part::GeomLine']

        def getRadians(self, E):
            vect = self._dXdYdZ(E)
            norm = self._normalizeVector(vect)
            rads = self._getVectorAngle(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
        parallel_edge_pairs = list()
        parallel_edge_flags = list()
        flag = 1
        eCnt = len(shape.Edges)
        lstE = eCnt - 1
        for i in range(0, eCnt):  # populate empty parallel edge flag list
            parallel_edge_flags.append(0)        
        for i in range(0, eCnt):  # Cycle through edges to identify parallel pairs
            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:
                            parallel_edge_pairs.append((eA, eB))
                            # set parallel flags for this pair of edges
                            parallel_edge_flags[A[0]] = flag
                            parallel_edge_flags[B[0]] = flag
                            flag += 1
                    if debug:
                        msg = 'Erroneous Curve.TypeId: {}'.format(debug)
                        PathLog.debug(msg)

        pairCnt = len(parallel_edge_pairs)
        if pairCnt > 1:
            parallel_edge_pairs.sort(key=lambda tup: tup[0].Length, reverse=True)

        if self.isDebug:
            PathLog.debug(' -pairCnt: {}'.format(pairCnt))
            for (a, b) in parallel_edge_pairs:
                PathLog.debug(' -pair: {}, {}'.format(round(a.Length, 4), round(b.Length,4)))
            PathLog.debug(' -parallel_edge_flags: {}'.format(parallel_edge_flags))

        if pairCnt == 0:
            msg = translate('PathSlot',
                'No parallel edges identified.')
            FreeCAD.Console.PrintError(msg + '\n')
            return False
        elif pairCnt == 1:
            # One pair of parallel edges identified
            if eCnt == 4:
                flag_set = list()
                for i in range(0, 4):
                    e = parallel_edge_flags[i]
                    if e == 0:
                        flag_set.append(shape.Edges[i])
                if len(flag_set) == 2:
                    same = (flag_set[0], flag_set[1])
                else:
                    same = parallel_edge_pairs[0]
            else:
                same = parallel_edge_pairs[0]
        else:
            if obj.Reference1 == 'Long Edge':
                same = parallel_edge_pairs[1]
            elif obj.Reference1 == 'Short Edge':
                same = parallel_edge_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)

        msg = obj.Label + ' '
        msg += translate('PathSlot',
                '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."""
        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 ):
#                abs(z2 - z3) > tolrnc):  3rd test redundant.
                return False
            return True

        def circumCircleFrom3Points(P1, P2, P3):
            # Source code for this function copied from (with modifications):
            # https://wiki.freecadweb.org/Macro_Draft_Circle_3_Points_3D
            vP2P1 = (P2 - P1)
            vP3P2 = (P3 - P2)
            vP1P3 = (P1 - P3)

            L = vP2P1.cross(vP3P2).Length
            # Circle radius (not used)
            # r = vP1P2.Length * vP2P3.Length * vP3P1.Length / 2 / l
            if round(L, 8) == 0.0:
                PathLog.error("The three points are colinear, arc is a straight.")
                return False

            # Sphere center.
            twolsqr= 2*L*L
            a = -vP3P2.dot(vP3P2) * vP2P1.dot(vP1P3) / twolsqr
            b = -vP1P3.dot(vP1P3) * vP3P2.dot(vP2P1) / twolsqr
            c = -vP2P1.dot(vP2P1) * vP1P3.dot(vP3P2) / twolsqr
            return P1*a + P2*b + P3*c

        V1 = edge.Vertexes[0]
        p1 = FreeCAD.Vector(V1.X, V1.Y, 0.0)
        if len(edge.Vertexes) == 1:   # circle has one virtex
            p2 = FreeCAD.Vector(p1)
        else :  
            V2 = edge.Vertexes[1]
            p2 = FreeCAD.Vector(V2.X, V2.Y, 0.0)
         
        # Process edge based on curve type
        if edge.Curve.TypeId in lineTypes:
            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
                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.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
            else:
                # Arc edge
                PathLog.debug('Arc with multiple vertices.')
                self.isArc = 2
                midPnt = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0))
                if not isHorizontal(V1.Z, V2.Z, midPnt.z):
                    return False
                    
                midPnt.z = 0.0
                circleCenter = circumCircleFrom3Points(p1, p2, midPnt)
                if not circleCenter:
                    return False
                self.arcMidPnt = midPnt
                self.arcCenter = circleCenter
                self.arcRadius = p1.sub(circleCenter).Length

                if oversizedTool(self.arcRadius * 2.0):
                    return False

            return (p1, p2)
        else :
            msg = translate('PathSlot','Failed, slot from edge only accepts lines, arcs and circles.')
            FreeCAD.Console.PrintError(msg + '\n')
           
            return False  # not line , not circle

    # Methods for processing double geometry
    def _processDouble(self, obj, shape_1, sub1, shape_2, sub2):
        """This is the control method for slots based on a
        two Base Geometry features."""
        PathLog.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('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:
            PathLog.debug('dYdX1, dYdX2: {}, {}'.format(dYdX1, dYdX2))
            if not self._isParallel(dYdX1, dYdX2):
                if self.shapeType1 != 'Edge' or self.shapeType2 != 'Edge':
                    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):
        """_normalizeVector(v)...
        Returns a copy of the vector received with values rounded to 10 decimal places."""
        posTol = 0.0000000001  # abitrary, use job Geometry Tolerance ???
        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):
        """_getLowestPoint(shape)... Returns lowest vertex of shape as vector."""
        # 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):
        """_getHighestPoint(shape)... Returns highest vertex of shape as vector."""
        # 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):
        """_processFeature(obj, shape, sub, pNum)...
        This function analyzes a shape and returns a three item tuple containing:
            working point,
            shape orientation/slope,
            shape category as face, edge, or vert."""
        p = None
        dYdX = None
        cat = sub[:4]
        PathLog.debug('sub-feature is {}'.format(cat))
        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):
        """_extendArcSlot(p1, p2, cent, begExt, endExt)...
                This function extends an arc defined by two end points, p1 and p2, and the center.
        The arc is extended along the circumference with begExt and endExt values.
        The function returns the new end points as tuple (n1, n2) to replace p1 and p2."""
        cancel = True
        if not begExt and not endExt :
            return (p1, p2)

        n1 = p1
        n2 = p2

        # Create a chord of the right length, on XY plane, starting on x axis 
        def makeChord(rads):
            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)
            return Part.makeLine(a, b)


        # Convert extension to radians; make a generic chord ( line ) on XY plane from the x axis
        # rotate and shift into place so it has same vertices as the required arc extension
        # adjust rotation angle to provide +ve or -ve extension as needed 
        origin = FreeCAD.Vector(0.0, 0.0, 0.0)
        if begExt:
            ExtRadians = abs(begExt / self.newRadius)
            chord = makeChord(ExtRadians)

            beginRadians = self._getVectorAngle(p1.sub(self.arcCenter))
            if begExt < 0:
                beginRadians += 0  # negative Ext shortens slot so chord endpoint is slot start point 
            else :
                beginRadians -= 2*ExtRadians  # positive Ext lengthens slot so decrease start point angle
                
            # PathLog.debug('begExt angles are: {},  {}'.format(beginRadians, math.degrees(beginRadians)))

            chord.rotate(origin, FreeCAD.Vector(0, 0, 1), math.degrees(beginRadians))
            chord.translate(self.arcCenter)
            self._addDebugObject(chord, 'ExtendStart')
            
            v1 = chord.Vertexes[1]
            n1 = FreeCAD.Vector(v1.X, v1.Y, 0.0)
            
        if endExt:
            ExtRadians = abs(endExt / self.newRadius)
            chord = makeChord(ExtRadians)

            endRadians = self._getVectorAngle(p2.sub(self.arcCenter))
            if endExt > 0:
                endRadians += 0  # positive Ext lengthens slot so chord endpoint is good
            else :
                endRadians -= 2*ExtRadians  # negative Ext shortens slot so decrease end point angle
                
            # PathLog.debug('endExt angles are: {},  {}'.format(endRadians, math.degrees(endRadians)))

            chord.rotate(origin, FreeCAD.Vector(0, 0, 1), math.degrees(endRadians))
            chord.translate(self.arcCenter)
            self._addDebugObject(chord, 'ExtendEnd')
               
            v1 = chord.Vertexes[1]
            n2 = FreeCAD.Vector(v1.X, v1.Y, 0.0)

        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.0)
        p2 = FreeCAD.Vector(com2.x, com2.y, 0.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) )
 #       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):
        """_makePerpendicular(p1, p2, length)...
        Using a line defined by p1 and p2, returns a perpendicular vector centered
        at the midpoint of the line, with length value."""
        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)
            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 _getVectorAngle(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:
            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.
        if hasattr(self.tool.Diameter, 'Value'):
            rad = self.tool.Diameter.Value / 2.0
        else:
            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

            # 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 produces negative radius.')
                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 produces negative radius.')
                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:
                print ("volume=",cmn.Volume)
                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.tmpGrp.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
