From f323e635bdd0b73a3a7b14091bb883e74d0ee7bf Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Thu, 18 Jun 2020 19:55:38 -0500
Subject: [PATCH 1/2] Path: Add new `Slot` operation
New slotting operation:
- Accepts selection of up to two features.
- Selectable features include: faces, edges, and vertices.
- User may select a single face or edge, or a pair of features.
- The path may be extended at the start and end.
- The path may be reversed.
- The path may be altered to the perpendicular in some cases.
- A unique Slot icon is included.
- Includes simple collision detection with warning message.
---
src/Mod/Path/CMakeLists.txt | 2 +
src/Mod/Path/Gui/Resources/Path.qrc | 2 +
.../Path/Gui/Resources/icons/Path-Slot.svg | 754 ++++++++++
.../Gui/Resources/panels/PageOpSlotEdit.ui | 440 ++++++
src/Mod/Path/InitGui.py | 17 +-
src/Mod/Path/PathScripts/PathGuiInit.py | 1 +
src/Mod/Path/PathScripts/PathSelection.py | 16 +
src/Mod/Path/PathScripts/PathSlot.py | 1275 +++++++++++++++++
src/Mod/Path/PathScripts/PathSlotGui.py | 241 ++++
9 files changed, 2743 insertions(+), 5 deletions(-)
create mode 100644 src/Mod/Path/Gui/Resources/icons/Path-Slot.svg
create mode 100644 src/Mod/Path/Gui/Resources/panels/PageOpSlotEdit.ui
create mode 100644 src/Mod/Path/PathScripts/PathSlot.py
create mode 100644 src/Mod/Path/PathScripts/PathSlotGui.py
diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt
index 12538fbe55..7280763abc 100644
--- a/src/Mod/Path/CMakeLists.txt
+++ b/src/Mod/Path/CMakeLists.txt
@@ -103,6 +103,8 @@ SET(PathScripts_SRCS
PathScripts/PathSetupSheetOpPrototypeGui.py
PathScripts/PathSimpleCopy.py
PathScripts/PathSimulatorGui.py
+ PathScripts/PathSlot.py
+ PathScripts/PathSlotGui.py
PathScripts/PathStock.py
PathScripts/PathStop.py
PathScripts/PathSurface.py
diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc
index 59eea1c06b..8f54e59f77 100644
--- a/src/Mod/Path/Gui/Resources/Path.qrc
+++ b/src/Mod/Path/Gui/Resources/Path.qrc
@@ -57,6 +57,7 @@
icons/Path-Shape.svg
icons/Path-SimpleCopy.svg
icons/Path-Simulator.svg
+ icons/Path-Slot.svg
icons/Path-Speed.svg
icons/Path-Stock.svg
icons/Path-Stop.svg
@@ -106,6 +107,7 @@
panels/PageOpPocketFullEdit.ui
panels/PageOpProbeEdit.ui
panels/PageOpProfileFullEdit.ui
+ panels/PageOpSlotEdit.ui
panels/PageOpSurfaceEdit.ui
panels/PageOpWaterlineEdit.ui
panels/PathEdit.ui
diff --git a/src/Mod/Path/Gui/Resources/icons/Path-Slot.svg b/src/Mod/Path/Gui/Resources/icons/Path-Slot.svg
new file mode 100644
index 0000000000..f848870805
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/icons/Path-Slot.svg
@@ -0,0 +1,754 @@
+
+
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpSlotEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpSlotEdit.ui
new file mode 100644
index 0000000000..a2466277ee
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpSlotEdit.ui
@@ -0,0 +1,440 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 350
+ 400
+
+
+
+ Form
+
+
+
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 3
+
+
-
+
+
+ ToolController
+
+
+
+ -
+
+
+ <html><head/><body><p>The tool and its settings to be used for this operation.</p></body></html>
+
+
+
+ -
+
+
+ -
+
+
+ Coolant Mode
+
+
+
+
+
+
+ -
+
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ <html><head/><body><p>Choose what point to use on the first selected feature.</p></body></html>
+
+
+ true
+
+
-
+
+ Center of Mass
+
+
+ -
+
+ Center of BoundBox
+
+
+ -
+
+ Lowest Point
+
+
+ -
+
+ Highest Point
+
+
+ -
+
+ Long Edge
+
+
+ -
+
+ Short Edge
+
+
+ -
+
+ Vertex
+
+
+
+
+ -
+
+
+ Start Feature Reference
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ End Feature Reference
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 8
+
+
+
+ <html><head/><body><p>Choose what point to use on the second selected feature.</p></body></html>
+
+
+ true
+
+
-
+
+ Center of Mass
+
+
+ -
+
+ Center of BoundBox
+
+
+ -
+
+ Lowest Point
+
+
+ -
+
+ Highest Point
+
+
+ -
+
+ Vertex
+
+
+
+
+
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+
+ 9
+ 50
+ false
+
+
+
+ <html><head/><body><p>No Base Geometry selected.</p></body></html>
+
+
+ color:blue
+
+
+ No Base Geometry selected.
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+ -
+
+
+ <html><head/><body><p>Currently using custom point inputs in the Property View of the Data tab.</p></body></html>
+
+
+ Currently using custom point inputs available in the Property View of the Data tab.
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ Extend Path End
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ <html><head/><body><p>Positive extends the end of the path, negative shortens.</p></body></html>
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ <html><head/><body><p>Positive extends the beginning of the path, negative shortens.</p></body></html>
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Extend Path Start
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ Layer Mode
+
+
+
+ -
+
+
+ <html><head/><body><p>Choose the path orientation with regard to the feature(s) selected.</p></body></html>
+
+
-
+
+ Start to End
+
+
+ -
+
+ Perpendicular
+
+
+
+
+ -
+
+
+ Path Orientation
+
+
+
+ -
+
+
+
+ 8
+
+
+
+ <html><head/><body><p>Complete the operation in a single pass at depth, or mulitiple passes to final depth.</p></body></html>
+
+
-
+
+ Single-pass
+
+
+ -
+
+ Multi-pass
+
+
+
+
+ -
+
+
+ <html><head/><body><p>Enable to reverse the cut direction of the slot path.</p></body></html>
+
+
+ Reverse cut direction
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ Gui::InputField
+ QLineEdit
+
+
+
+
+ toolController
+ coolantController
+ geo1Reference
+ geo2Reference
+ geo1Extension
+ geo2Extension
+ layerMode
+ pathOrientation
+ reverseDirection
+
+
+
+
diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py
index e73ef45654..6b8b545bae 100644
--- a/src/Mod/Path/InitGui.py
+++ b/src/Mod/Path/InitGui.py
@@ -88,14 +88,21 @@ class PathWorkbench (Workbench):
# build commands list
projcmdlist = ["Path_Job", "Path_Post"]
- toolcmdlist = ["Path_Inspect", "Path_Simulator", "Path_ToolLibraryEdit", "Path_SelectLoop", "Path_OpActiveToggle"]
- prepcmdlist = ["Path_Fixture", "Path_Comment", "Path_Stop", "Path_Custom", "Path_Probe"]
- # twodopcmdlist = ["Path_Profile", "Path_Contour", "Path_Profile_Faces", "Path_Profile_Edges", "Path_Pocket_Shape", "Path_Drilling", "Path_MillFace", "Path_Helix", "Path_Adaptive"]
- twodopcmdlist = ["Path_Profile", "Path_Pocket_Shape", "Path_Drilling", "Path_MillFace", "Path_Helix", "Path_Adaptive"]
+ toolcmdlist = ["Path_Inspect", "Path_Simulator",
+ "Path_ToolLibraryEdit", "Path_SelectLoop",
+ "Path_OpActiveToggle"]
+ prepcmdlist = ["Path_Fixture", "Path_Comment", "Path_Stop",
+ "Path_Custom", "Path_Probe"]
+ twodopcmdlist = ["Path_Profile", "Path_Pocket_Shape", "Path_Drilling",
+ "Path_MillFace", "Path_Helix", "Path_Adaptive",
+ "Path_Slot"]
threedopcmdlist = ["Path_Pocket_3D"]
engravecmdlist = ["Path_Engrave", "Path_Deburr"]
modcmdlist = ["Path_OperationCopy", "Path_Array", "Path_SimpleCopy"]
- dressupcmdlist = ["Path_DressupAxisMap", "Path_DressupPathBoundary", "Path_DressupDogbone", "Path_DressupDragKnife", "Path_DressupLeadInOut", "Path_DressupRampEntry", "Path_DressupTag", "Path_DressupZCorrect"]
+ dressupcmdlist = ["Path_DressupAxisMap", "Path_DressupPathBoundary",
+ "Path_DressupDogbone", "Path_DressupDragKnife",
+ "Path_DressupLeadInOut", "Path_DressupRampEntry",
+ "Path_DressupTag", "Path_DressupZCorrect"]
extracmdlist = []
# modcmdmore = ["Path_Hop",]
# remotecmdlist = ["Path_Remote"]
diff --git a/src/Mod/Path/PathScripts/PathGuiInit.py b/src/Mod/Path/PathScripts/PathGuiInit.py
index 21aadba1d5..6697f6bcd8 100644
--- a/src/Mod/Path/PathScripts/PathGuiInit.py
+++ b/src/Mod/Path/PathScripts/PathGuiInit.py
@@ -72,6 +72,7 @@ def Startup():
from PathScripts import PathSetupSheetGui
from PathScripts import PathSimpleCopy
from PathScripts import PathSimulatorGui
+ from PathScripts import PathSlotGui
from PathScripts import PathStop
# from PathScripts import PathSurfaceGui # Added in initGui.py due to OCL dependency
from PathScripts import PathToolController
diff --git a/src/Mod/Path/PathScripts/PathSelection.py b/src/Mod/Path/PathScripts/PathSelection.py
index 943d6ae28a..25f5c4b1c6 100644
--- a/src/Mod/Path/PathScripts/PathSelection.py
+++ b/src/Mod/Path/PathScripts/PathSelection.py
@@ -233,6 +233,17 @@ class PROBEGate:
pass
+class ALLGate(PathBaseGate):
+ def allow(self, doc, obj, sub): # pylint: disable=unused-argument
+ if sub and sub[0:6] == 'Vertex':
+ return True
+ if sub and sub[0:4] == 'Edge':
+ return True
+ if sub and sub[0:4] == 'Face':
+ return True
+ return False
+
+
def contourselect():
FreeCADGui.Selection.addSelectionGate(CONTOURGate())
FreeCAD.Console.PrintWarning("Contour Select Mode\n")
@@ -278,6 +289,10 @@ def adaptiveselect():
FreeCAD.Console.PrintWarning("Adaptive Select Mode\n")
+def slotselect():
+ FreeCADGui.Selection.addSelectionGate(ALLGate())
+ FreeCAD.Console.PrintWarning("Slot Cutter Select Mode\n")
+
def surfaceselect():
gate = False
if(MESHGate() or FACEGate()):
@@ -305,6 +320,7 @@ def select(op):
opsel['Profile Edges'] = eselect # (depreciated)
opsel['Profile Faces'] = fselect # (depreciated)
opsel['Profile'] = profileselect
+ opsel['Slot'] = slotselect
opsel['Surface'] = surfaceselect
opsel['Waterline'] = surfaceselect
opsel['Adaptive'] = adaptiveselect
diff --git a/src/Mod/Path/PathScripts/PathSlot.py b/src/Mod/Path/PathScripts/PathSlot.py
new file mode 100644
index 0000000000..8618e59ecd
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathSlot.py
@@ -0,0 +1,1275 @@
+# -*- 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
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+# PathLog.trackModule(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 Surfacing 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.setEditorProperties(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()
+ 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::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::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 {
+ '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': 'Single-pass',
+ 'PathOrientation': 'Start to End',
+ 'ReverseDirection': False,
+
+ # For debugging
+ 'ShowTempObjects': False
+ }
+
+ return defaults
+
+ def setEditorProperties(self, obj):
+ # Used to hide inputs in properties list
+ A = B = 2
+ if hasattr(obj, 'Base'):
+ enums2 = self.opPropertyEnumerations()['Reference2']
+ if obj.Base:
+ (base, subsList) = obj.Base[0]
+ subCnt = len(subsList)
+ if subCnt == 1:
+ # Adjust available enumerations
+ obj.Reference1 = self._getReference1Enums(subsList[0], True)
+ A = 0
+ elif subCnt == 2:
+ # Adjust available enumerations
+ obj.Reference1 = self._getReference1Enums(subsList[0])
+ obj.Reference2 = self._getReference2Enums(subsList[1])
+ A = B = 0
+ else:
+ ENUMS = self.opPropertyEnumerations()
+ obj.Reference1 = ENUMS['Reference1']
+ obj.Reference2 = ENUMS['Reference2']
+
+ obj.setEditorMode('Reference1', A)
+ obj.setEditorMode('Reference2', B)
+
+ def onChanged(self, obj, prop):
+ if hasattr(self, 'propertiesReady'):
+ if self.propertiesReady:
+ if prop in ['Base']:
+ self.setEditorProperties(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.opPropertyEnumerations()
+ for n in ENUMS:
+ restore = False
+ if hasattr(obj, n):
+ val = obj.getPropertyByName(n)
+ restore = True
+ setattr(obj, n, ENUMS[n])
+ if restore:
+ setattr(obj, n, val)
+
+ self.setEditorProperties(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.cancelOperation = False
+ 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.isDebug = False if PathLog.getLevel(PathLog.thisModule()) != 4 else True
+ self.showTempObjects = obj.ShowTempObjects
+ CMDS = list()
+ FCAD = FreeCAD.ActiveDocument
+
+ try:
+ dotIdx = __name__.index('.') + 1
+ except Exception:
+ dotIdx = 0
+ self.module = __name__[dotIdx:]
+
+ if not self.isDebug:
+ self.showTempObjects = False
+ if self.showTempObjects:
+ 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
+
+ # Identify parent Job
+ JOB = PathUtils.findParentJob(obj)
+ self.JOB = JOB
+ if JOB is None:
+ PathLog.error(translate('PathSlot', "No JOB"))
+ return
+ self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin
+
+ # 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.showTempObjects:
+ if FreeCAD.GuiUp:
+ import FreeCADGui
+ FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False
+ self.tmpGrp.purgeTouched()
+
+ if self.cancelOperation:
+ FreeCAD.ActiveDocument.openTransaction(translate("PathSlot", "Canceled the Slot operation."))
+ FreeCAD.ActiveDocument.removeObject(obj.Name)
+ FreeCAD.ActiveDocument.commitTransaction()
+
+ return True
+
+ # Control method 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
+
+ # Calculate extensions to slot path
+ begExt, endExt = 0, 0
+ if obj.ExtendPathStart.Value != 0:
+ begExt = obj.ExtendPathStart.Value
+ if obj.ExtendPathEnd.Value != 0:
+ endExt = obj.ExtendPathEnd.Value
+
+ 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:
+ sub1 = subsList[0]
+ shape_1 = getattr(base.Shape, sub1)
+ self.shape1 = shape_1
+ pnts = self._processSingle(obj, shape_1, sub1)
+ else:
+ 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
+
+ # 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)
+ else:
+ perpZero = False
+
+ # Reverse direction of path if requested
+ if obj.ReverseDirection:
+ (p2, p1) = pnts
+ else:
+ (p1, p2) = pnts
+
+ # Apply extensions to slot path
+ if perpZero:
+ begExt -= 5.0
+ endExt -= 5.0
+ pnts = self._extendSlot(p1, p2, begExt, endExt)
+
+ if not pnts:
+ return False
+
+ (p1, p2) = pnts
+ if self.isDebug:
+ PathLog.debug('p1, p2: {}, {}'.format(p1, p2))
+ if p1.sub(p2).Length != 0 and self.showTempObjects:
+ O = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp_Path')
+ O.Shape = Part.makeLine(p1, p2)
+ O.purgeTouched()
+ self.tmpGrp.addObject(O)
+
+ 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._makeGCode(obj, p1, p2)
+ return cmds
+
+ def _makeGCode(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 layerPass(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), {}))
+ # CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
+ if obj.LayerMode == 'Single-pass':
+ CMDS.extend(layerPass(p1, p2, obj.FinalDepth.Value))
+ CMDS.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+ else:
+ prvDep = obj.StartDepth.Value
+ for dep in self.depthParams:
+ CMDS.extend(layerPass(p1, p2, dep))
+ CMDS.append(Path.Command('G0', {'Z': prvDep, 'F': self.vertRapid}))
+ prvDep = dep
+ 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('Face.normalAt(): {}'.format(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, shape_1)
+ 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')
+ V1 = shape_1.Vertexes[0]
+ V2 = shape_1.Vertexes[1]
+ p1 = FreeCAD.Vector(V1.X, V1.Y, 0.0)
+ p2 = FreeCAD.Vector(V2.X, V2.Y, 0.0)
+ 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."""
+ 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
+ I = [i for i in range(0, eCnt)]
+ I.append(0)
+ 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 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[0]
+ elif obj.Reference1 == 'Short Edge':
+ same = pairs[1]
+ 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."""
+ 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."""
+ 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)
+
+ # 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."""
+ 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 _extendSlot(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 _getEndMidPoints(self, same):
+ # Find mid-points between ends of equal, oppossing edges
+ e0va = same[0].Vertexes[0]
+ e0vb = same[0].Vertexes[1]
+ e1va = same[1].Vertexes[0]
+ e1vb = same[1].Vertexes[1]
+
+ if False:
+ midX1 = (e0va.X + e0vb.X) / 2.0
+ midY1 = (e0va.Y + e0vb.Y) / 2.0
+ midX2 = (e1va.X + e1vb.X) / 2.0
+ midY2 = (e1va.Y + e1vb.Y) / 2.0
+ m1 = FreeCAD.Vector(midX1, midY1, e0va.Z)
+ m2 = FreeCAD.Vector(midX2, midY2, e0va.Z)
+
+ p1 = FreeCAD.Vector(e0va.X, e0va.Y, e0va.Z)
+ p2 = FreeCAD.Vector(e0vb.X, e0vb.Y, e0vb.Z)
+ p3 = FreeCAD.Vector(e1va.X, e1va.Y, e1va.Z)
+ p4 = FreeCAD.Vector(e1vb.X, e1vb.Y, e1vb.Z)
+
+ L0 = Part.makeLine(p1, p2)
+ L1 = Part.makeLine(p3, p4)
+ comL0 = L0.CenterOfMass
+ comL1 = L1.CenterOfMass
+ m1 = FreeCAD.Vector(comL0.x, comL0.y, 0.0)
+ m2 = FreeCAD.Vector(comL1.x, comL1.y, 0.0)
+
+ return (m1, m2)
+
+ def _getOppMidPoints(self, same):
+ # Find mid-points between ends of equal, oppossing edges
+ v1 = same[0].Vertexes[0]
+ v2 = same[0].Vertexes[1]
+ a1 = same[1].Vertexes[0]
+ a2 = same[1].Vertexes[1]
+ midX1 = (v1.X + a2.X) / 2.0
+ midY1 = (v1.Y + a2.Y) / 2.0
+ midX2 = (v2.X + a1.X) / 2.0
+ midY2 = (v2.Y + a1.Y) / 2.0
+ p1 = FreeCAD.Vector(midX1, midY1, v1.Z)
+ p2 = FreeCAD.Vector(midX2, midY2, v1.Z)
+ 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():
+ csWire = Part.Face(wires[0])
+ if csWire.Area > 0:
+ csWire.translate(FreeCAD.Vector(0.0, 0.0, shape.BoundBox.ZMin - csWire.BoundBox.ZMin))
+ return ('Face', csWire)
+ return ('Wire', wires[0])
+ return False
+
+ def _getReference1Enums(self, sub, single=False):
+ # Adjust available enumerations
+ enums1 = self.opPropertyEnumerations()['Reference1']
+ for ri in removeIndexesFromReference_1(sub, single):
+ enums1.pop(ri)
+ return enums1
+
+ def _getReference2Enums(self, sub):
+ # Adjust available enumerations
+ enums2 = self.opPropertyEnumerations()['Reference2']
+ for ri in removeIndexesFromReference_2(sub):
+ enums2.pop(ri)
+ return enums2
+
+ 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)
+ factor = dist / toEnd.Length
+ perp = FreeCAD.Vector(-1 * toEnd.y, toEnd.x, 0.0)
+ perp.normalize()
+ perp.multiply(dist)
+ return perp
+
+ ce1 = Part.Wire(Part.makeCircle(rad, p1).Edges)
+ ce2 = Part.Wire(Part.makeCircle(rad, p2).Edges)
+ C1 = Part.Face(ce1)
+ C2 = Part.Face(ce2)
+
+ zTrans = obj.FinalDepth.Value - C1.BoundBox.ZMin
+ C1.translate(FreeCAD.Vector(0.0, 0.0, zTrans))
+ zTrans = obj.FinalDepth.Value - C2.BoundBox.ZMin
+ C2.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)
+ endShp = C2.extrude(extVect)
+
+ 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)
+
+ part1 = startShp.fuse(boxShp)
+ pathTravel = part1.fuse(endShp)
+
+ if self.showTempObjects:
+ O = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp_PathTravel')
+ O.Shape = pathTravel
+ O.purgeTouched()
+ self.tmpGrp.addObject(O)
+
+ # 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
+# Eclass
+
+
+# Determine applicable enumerations
+def removeIndexesFromReference_1(sub, single=False):
+ """Determine which enumerations to remove for Reference1 input
+ based upon the feature type(category)."""
+ cat = sub[:4]
+ remIdxs = [6, 5, 4]
+ if cat == 'Face':
+ if single:
+ remIdxs = [6, 3, 2, 1, 0]
+ elif cat == 'Edge':
+ if single:
+ remIdxs = [6, 5, 3, 2, 1, 0]
+ elif cat == 'Vert':
+ remIdxs = [5, 4, 3, 2, 1, 0]
+ return remIdxs
+
+
+def removeIndexesFromReference_2(sub):
+ """Determine which enumerations to remove for Reference2 input
+ based upon the feature type(category)."""
+ cat = sub[:4]
+ remIdxs = [4]
+ # Customize Reference combobox options
+ if cat == 'Vert':
+ remIdxs = [3, 2, 1, 0]
+ return remIdxs
+
+
+
+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
diff --git a/src/Mod/Path/PathScripts/PathSlotGui.py b/src/Mod/Path/PathScripts/PathSlotGui.py
new file mode 100644
index 0000000000..4b1d25e2c7
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathSlotGui.py
@@ -0,0 +1,241 @@
+# -*- 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 *
+# * *
+# ***************************************************************************
+
+import FreeCAD
+import FreeCADGui
+import PathScripts.PathSlot as PathSlot
+import PathScripts.PathGui as PathGui
+import PathScripts.PathOpGui as PathOpGui
+
+from PySide import QtCore
+
+__title__ = "Path Slot Operation UI"
+__author__ = "russ4262 (Russell Johnson)"
+__url__ = "http://www.freecadweb.org"
+__doc__ = "Slot operation page controller and command implementation."
+__contributors__ = ""
+
+
+class TaskPanelOpPage(PathOpGui.TaskPanelPage):
+ '''Page controller class for the Slot operation.'''
+
+ def initPage(self, obj):
+ # pylint: disable=attribute-defined-outside-init
+ self.CATS = [None, None]
+ self.setTitle("Slot - " + obj.Label)
+ # retrieve property enumerations
+ self.propEnums = PathSlot.ObjectSlot.opPropertyEnumerations(False)
+ # Requirements due to Gui::QuantitySpinBox class use in UI panel
+ self.geo1Extension = PathGui.QuantitySpinBox(self.form.geo1Extension, obj, 'ExtendPathStart')
+ self.geo2Extension = PathGui.QuantitySpinBox(self.form.geo2Extension, obj, 'ExtendPathEnd')
+ # self.updateVisibility()
+
+ def getForm(self):
+ '''getForm() ... returns UI'''
+ # FreeCAD.Console.PrintMessage('getForm()\n')
+ return FreeCADGui.PySideUic.loadUi(":/panels/PageOpSlotEdit.ui")
+
+ def updateQuantitySpinBoxes(self):
+ # FreeCAD.Console.PrintMessage('updateQuantitySpinBoxes()\n')
+ self.geo1Extension.updateSpinBox()
+ self.geo2Extension.updateSpinBox()
+
+ def getFields(self, obj):
+ '''getFields(obj) ... transfers values from UI to obj's proprties'''
+ # FreeCAD.Console.PrintMessage('getFields()\n')
+ self.updateToolController(obj, self.form.toolController)
+ self.updateCoolant(obj, self.form.coolantController)
+
+ obj.Reference1 = str(self.form.geo1Reference.currentText())
+ self.geo1Extension.updateProperty()
+
+ obj.Reference2 = str(self.form.geo2Reference.currentText())
+ self.geo2Extension.updateProperty()
+
+ val = self.propEnums['LayerMode'][self.form.layerMode.currentIndex()]
+ obj.LayerMode = val
+
+ val = self.propEnums['PathOrientation'][self.form.pathOrientation.currentIndex()]
+ obj.PathOrientation = val
+
+ if hasattr(self.form, 'reverseDirection'):
+ obj.ReverseDirection = self.form.reverseDirection.isChecked()
+
+ def setFields(self, obj):
+ '''setFields(obj) ... transfers obj's property values to UI'''
+ # FreeCAD.Console.PrintMessage('setFields()\n')
+ self.updateQuantitySpinBoxes()
+
+ self.setupToolController(obj, self.form.toolController)
+ self.setupCoolant(obj, self.form.coolantController)
+
+ idx = self.propEnums['Reference1'].index(obj.Reference1)
+ self.form.geo1Reference.setCurrentIndex(idx)
+ idx = self.propEnums['Reference2'].index(obj.Reference2)
+ self.form.geo2Reference.setCurrentIndex(idx)
+
+ self.selectInComboBox(obj.LayerMode, self.form.layerMode)
+ self.selectInComboBox(obj.PathOrientation, self.form.pathOrientation)
+
+ if obj.ReverseDirection:
+ self.form.reverseDirection.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.form.reverseDirection.setCheckState(QtCore.Qt.Unchecked)
+
+ # FreeCAD.Console.PrintMessage('... calling updateVisibility()\n')
+ self.updateVisibility()
+
+ def getSignalsForUpdate(self, obj):
+ '''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
+ # FreeCAD.Console.PrintMessage('getSignalsForUpdate()\n')
+ signals = []
+ signals.append(self.form.toolController.currentIndexChanged)
+ signals.append(self.form.coolantController.currentIndexChanged)
+ signals.append(self.form.geo1Extension.editingFinished)
+ signals.append(self.form.geo1Reference.currentIndexChanged)
+ signals.append(self.form.geo2Extension.editingFinished)
+ signals.append(self.form.geo2Reference.currentIndexChanged)
+ signals.append(self.form.layerMode.currentIndexChanged)
+ signals.append(self.form.pathOrientation.currentIndexChanged)
+ signals.append(self.form.reverseDirection.stateChanged)
+ return signals
+
+ def updateVisibility(self, sentObj=None):
+ '''updateVisibility(sentObj=None)... Updates visibility of Tasks panel objects.'''
+ # FreeCAD.Console.PrintMessage('updateVisibility()\n')
+ hideFeatures = True
+ if hasattr(self.obj, 'Base'):
+ if self.obj.Base:
+ self.form.customPoints.hide()
+ self.form.featureReferences.show()
+ self.form.pathOrientation_label.show()
+ self.form.pathOrientation.show()
+ hideFeatures = False
+ base, sublist = self.obj.Base[0]
+ subCnt = len(sublist)
+
+ if subCnt == 1:
+ # Save value, then reset choices
+ self.resetRef1Choices()
+ n1 = sublist[0]
+ s1 = getattr(base.Shape, n1)
+ # Show Reference1 and cusomize options within
+ self.form.geo1Reference.show()
+ self.form.geo1Reference_label.show()
+ self.form.geo1Reference_label.setText('Reference: {}'.format(n1))
+ self.customizeReference_1(n1, single=True)
+ # Hide Reference2
+ self.form.geo2Reference.hide()
+ self.form.geo2Reference_label.hide()
+ self.form.geo2Reference_label.setText('End Reference')
+ if self.CATS[1]:
+ self.CATS[1] = None
+ elif subCnt == 2:
+ self.resetRef1Choices()
+ self.resetRef2Choices()
+ n1 = sublist[0]
+ n2 = sublist[1]
+ s1 = getattr(base.Shape, n1)
+ s2 = getattr(base.Shape, n2)
+ # Show Reference1 and cusomize options within
+ self.form.geo1Reference.show()
+ self.form.geo1Reference_label.show()
+ self.form.geo1Reference_label.setText('Start Reference: {}'.format(n1))
+ self.customizeReference_1(n1)
+ # Show Reference2 and cusomize options within
+ self.form.geo2Reference.show()
+ self.form.geo2Reference_label.show()
+ self.form.geo2Reference_label.setText('End Reference: {}'.format(n2))
+ self.customizeReference_2(n2)
+ else:
+ self.form.pathOrientation_label.hide()
+ self.form.pathOrientation.hide()
+ if hideFeatures:
+ self.form.featureReferences.hide()
+ self.form.customPoints.show()
+
+ """
+ '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 customizeReference_1(self, sub, single=False):
+ # Customize Reference1 combobox options
+ # by removing unavailable choices
+ cat = sub[:4]
+ if cat != self.CATS[0]:
+ self.CATS[0] = cat
+ cBox = self.form.geo1Reference
+ cBox.blockSignals(True)
+ for ri in PathSlot.removeIndexesFromReference_1(sub, single):
+ cBox.removeItem(ri)
+ cBox.blockSignals(False)
+
+ def customizeReference_2(self, sub):
+ # Customize Reference2 combobox options
+ # by removing unavailable choices
+ cat = sub[:4]
+ if cat != self.CATS[1]:
+ self.CATS[1] = cat
+ cBox = self.form.geo2Reference
+ cBox.blockSignals(True)
+ for ri in PathSlot.removeIndexesFromReference_2(sub):
+ cBox.removeItem(ri)
+ cBox.blockSignals(False)
+ cBox.setCurrentIndex(0)
+
+ def resetRef1Choices(self):
+ # Reset Reference1 choices
+ ref1 = self.form.geo1Reference
+ ref1.blockSignals(True)
+ ref1.clear() # Empty the combobox
+ ref1.addItems(self.propEnums['Reference1'])
+ ref1.blockSignals(False)
+
+ def resetRef2Choices(self):
+ # Reset Reference2 choices
+ ref2 = self.form.geo2Reference
+ ref2.blockSignals(True)
+ ref2.clear() # Empty the combobox
+ ref2.addItems(self.propEnums['Reference2'])
+ ref2.blockSignals(False)
+
+ def registerSignalHandlers(self, obj):
+ # FreeCAD.Console.PrintMessage('registerSignalHandlers()\n')
+ # self.form.pathOrientation.currentIndexChanged.connect(self.updateVisibility)
+ pass
+
+
+Command = PathOpGui.SetupOperation('Slot',
+ PathSlot.Create,
+ TaskPanelOpPage,
+ 'Path-Slot',
+ QtCore.QT_TRANSLATE_NOOP("Slot", "Slot"),
+ QtCore.QT_TRANSLATE_NOOP("Slot", "Create a Slot operation from selected geometry or custom points."),
+ PathSlot.SetupProperties)
+
+FreeCAD.Console.PrintLog("Loading PathSlotGui... done\n")
From 40faed1b7d5e53667bcb7b180c14b4c950eb6c43 Mon Sep 17 00:00:00 2001
From: Russell Johnson <47639332+Russ4262@users.noreply.github.com>
Date: Wed, 17 Jun 2020 20:39:40 -0500
Subject: [PATCH 2/2] Path: Fix timing of `updatePanelVisibility()` call
Targeted function call needed to be after `updateBase()` call after the `for` loop.
---
src/Mod/Path/PathScripts/PathOpGui.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/Mod/Path/PathScripts/PathOpGui.py b/src/Mod/Path/PathScripts/PathOpGui.py
index 02561a47ba..3841b1dc73 100644
--- a/src/Mod/Path/PathScripts/PathOpGui.py
+++ b/src/Mod/Path/PathScripts/PathOpGui.py
@@ -542,10 +542,8 @@ class TaskPanelBaseGeometryPage(TaskPanelPage):
for item in selected:
self.form.baseList.takeItem(self.form.baseList.row(item))
self.setDirty()
- self.updatePanelVisibility('Operation', self.obj)
self.updateBase()
- # self.obj.Proxy.execute(self.obj)
- # FreeCAD.ActiveDocument.recompute()
+ self.updatePanelVisibility('Operation', self.obj)
def updateBase(self):
newlist = []