From edbe6c4f2f52ec629afd5fc30af7626ecd3aef4a Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Sun, 10 May 2020 23:06:33 -0500 Subject: [PATCH] Path: Additional fixes and improvements to unified Profile operation --- src/Mod/Path/PathScripts/PathProfile.py | 123 ++++--- src/Mod/Path/PathScripts/PathProfileGui.py | 360 +++++++++++---------- 2 files changed, 265 insertions(+), 218 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathProfile.py b/src/Mod/Path/PathScripts/PathProfile.py index 6652b63a50..784632b48c 100644 --- a/src/Mod/Path/PathScripts/PathProfile.py +++ b/src/Mod/Path/PathScripts/PathProfile.py @@ -3,6 +3,7 @@ # *************************************************************************** # * * # * Copyright (c) 2014 Yorik van Havre * +# * Copyright (c) 2016 sliptonic * # * Copyright (c) 2020 Schildkroet * # * * # * This program is free software; you can redistribute it and/or modify * @@ -38,13 +39,13 @@ from PySide import QtCore from lazy_loader.lazy_loader import LazyLoader ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') Part = LazyLoader('Part', globals(), 'Part') -DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') __title__ = "Path Profile Faces Operation" -__author__ = "sliptonic (Brad Collette), Schildkroet" +__author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" __doc__ = "Path Profile operation based on faces." +__contributors__ = "Schildkroet" PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) @@ -57,17 +58,14 @@ def translate(context, text, disambig=None): class ObjectProfile(PathAreaOp.ObjectOp): '''Proxy object for Profile operations based on faces.''' - def baseObject(self): - '''baseObject() ... returns super of receiver - Used to call base implementation in overwritten functions.''' - return super(self.__class__, self) - def areaOpFeatures(self, obj): - '''areaOpFeatures(obj) ... returns features specific to the operation''' - return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels | PathOp.FeatureBaseEdges + '''areaOpFeatures(obj) ... returns operation-specific features''' + return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels \ + | PathOp.FeatureBaseEdges def initAreaOp(self, obj): '''initAreaOp(obj) ... creates all profile specific properties.''' + self.propertiesReady = False self.initAreaOpProperties(obj) obj.setEditorMode('MiterLimit', 2) @@ -75,29 +73,26 @@ class ObjectProfile(PathAreaOp.ObjectOp): def initAreaOpProperties(self, obj, warn=False): '''initAreaOpProperties(obj) ... create operation specific properties''' - missing = list() - JOB = PathUtils.findParentJob(obj) + self.addNewProps = list() for (prtyp, nm, grp, tt) in self.areaOpProperties(): if not hasattr(obj, nm): obj.addProperty(prtyp, nm, grp, tt) - missing.append(nm) - if warn: - newPropMsg = translate('PathProfile', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. ' - newPropMsg += translate('PathProfile', 'Check its default value.') - PathLog.warning(newPropMsg) + self.addNewProps.append(nm) - if len(missing) > 0: + if len(self.addNewProps) > 0: # Set enumeration lists for enumeration properties ENUMS = self.areaOpPropertyEnumerations() for n in ENUMS: - if n in missing: + if n in self.addNewProps: setattr(obj, n, ENUMS[n]) - # Set default values - PROP_DFLTS = self.areaOpPropertyDefaults(obj, JOB) - for n in PROP_DFLTS: - if n in missing: - setattr(obj, n, PROP_DFLTS[n]) + if warn: + newPropMsg = translate('PathProfile', 'New property added to') + newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + '. ' + newPropMsg += translate('PathProfile', 'Check its default value.') + '\n' + FreeCAD.Console.PrintWarning(newPropMsg) + + self.propertiesReady = True def areaOpProperties(self): '''areaOpProperties(obj) ... returns a tuples. @@ -146,8 +141,8 @@ class ObjectProfile(PathAreaOp.ObjectOp): 'Side': ['Outside', 'Inside'], # side of profile that cutter is on in relation to direction of profile } - def areaOpPropertyDefaults(self, obj=None, job=None): - '''areaOpPropertyDefaults(obj=None, job=None) ... returns a dictionary of default values + def areaOpPropertyDefaults(self, obj, job): + '''areaOpPropertyDefaults(obj, job) ... returns a dictionary of default values for the operation's properties.''' return { 'AttemptInverseAngle': True, @@ -166,40 +161,77 @@ class ObjectProfile(PathAreaOp.ObjectOp): 'processPerimeter': True } - def areaOpOnChanged(self, obj, prop): - '''areaOpOnChanged(obj, prop) ... updates certain property visibilities depending on changed properties.''' - if prop in ['UseComp', 'JoinType', 'EnableRotation']: - self.setOpEditorProperties(obj) + def areaOpApplyPropertyDefaults(self, obj, job, propList): + # Set standard property defaults + PROP_DFLTS = self.areaOpPropertyDefaults(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 areaOpSetDefaultValues(self, obj, job): + if self.addNewProps and self.addNewProps.__len__() > 0: + self.areaOpApplyPropertyDefaults(obj, job, self.addNewProps) def setOpEditorProperties(self, obj): '''setOpEditorProperties(obj, porp) ... Process operation-specific changes to properties visibility.''' - side = 2 - if obj.UseComp: - if len(obj.Base) > 0: - side = 0 + fc = 2 + # ml = 0 if obj.JoinType == 'Miter' else 2 + rotation = 2 if obj.EnableRotation == 'Off' else 0 + side = 0 if obj.UseComp else 2 + opType = self.getOperationType(obj) + + if opType == 'Contour': + side = 2 + elif opType == 'Face': + fc = 0 + elif opType == 'Edge': + pass + + obj.setEditorMode('JoinType', 2) + obj.setEditorMode('MiterLimit', 2) # ml + obj.setEditorMode('Side', side) + obj.setEditorMode('HandleMultipleFeatures', fc) + obj.setEditorMode('processCircles', fc) + obj.setEditorMode('processHoles', fc) + obj.setEditorMode('processPerimeter', fc) - if obj.JoinType == 'Miter': - obj.setEditorMode('MiterLimit', 0) - else: - obj.setEditorMode('MiterLimit', 2) - - rotation = 2 - if obj.EnableRotation != 'Off': - rotation = 0 obj.setEditorMode('ReverseDirection', rotation) obj.setEditorMode('InverseAngle', rotation) obj.setEditorMode('AttemptInverseAngle', rotation) obj.setEditorMode('LimitDepthToFace', rotation) + def getOperationType(self, obj): + if len(obj.Base) == 0: + return 'Contour' + + # return first geometry type selected + (base, subsList) = obj.Base[0] + return subsList[0][:4] + def areaOpOnDocumentRestored(self, obj): + self.propertiesReady = False + self.initAreaOpProperties(obj, warn=True) - - for prop in ['UseComp', 'JoinType']: - self.areaOpOnChanged(obj, prop) - + self.areaOpSetDefaultValues(obj, PathUtils.findParentJob(obj)) self.setOpEditorProperties(obj) + def areaOpOnChanged(self, obj, prop): + '''areaOpOnChanged(obj, prop) ... updates certain property visibilities depending on changed properties.''' + if prop in ['UseComp', 'JoinType', 'EnableRotation', 'Base']: + if hasattr(self, 'propertiesReady') and self.propertiesReady: + self.setOpEditorProperties(obj) + def areaOpAreaParams(self, obj, isHole): '''areaOpAreaParams(obj, isHole) ... returns dictionary with area parameters. Do not overwrite.''' @@ -499,6 +531,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): # Edges pre-processing def _processEdges(self, obj): + import DraftGeomUtils shapes = list() basewires = list() delPairs = list() diff --git a/src/Mod/Path/PathScripts/PathProfileGui.py b/src/Mod/Path/PathScripts/PathProfileGui.py index 756ff2bd30..3e4ea54c9a 100644 --- a/src/Mod/Path/PathScripts/PathProfileGui.py +++ b/src/Mod/Path/PathScripts/PathProfileGui.py @@ -1,173 +1,187 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2017 sliptonic * -# * * -# * 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.PathGui as PathGui -import PathScripts.PathOpGui as PathOpGui -import PathScripts.PathProfile as PathProfile - -from PySide import QtCore - - -__title__ = "Path Profile Operation UI" -__author__ = "sliptonic (Brad Collette)" -__url__ = "http://www.freecadweb.org" -__doc__ = "Profile operation page controller and command implementation." - - -FeatureSide = 0x01 -FeatureProcessing = 0x02 - -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - - -class TaskPanelOpPage(PathOpGui.TaskPanelPage): - '''Base class for profile operation page controllers. Two sub features are supported: - FeatureSide ... Is the Side property exposed in the UI - FeatureProcessing ... Are the processing check boxes supported by the operation - ''' - - def initPage(self, obj): - self.updateVisibility(obj) - - def profileFeatures(self): - '''profileFeatures() ... return which of the optional profile features are supported. - Currently two features are supported and returned: - FeatureSide ... Is the Side property exposed in the UI - FeatureProcessing ... Are the processing check boxes supported by the operation - .''' - return FeatureSide | FeatureProcessing - - def getForm(self): - '''getForm() ... returns UI customized according to profileFeatures()''' - form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpProfileFullEdit.ui") - return form - - def getFields(self, obj): - '''getFields(obj) ... transfers values from UI to obj's proprties''' - self.updateToolController(obj, self.form.toolController) - self.updateCoolant(obj, self.form.coolantController) - - if obj.Side != str(self.form.cutSide.currentText()): - obj.Side = str(self.form.cutSide.currentText()) - if obj.Direction != str(self.form.direction.currentText()): - obj.Direction = str(self.form.direction.currentText()) - PathGui.updateInputField(obj, 'OffsetExtra', self.form.extraOffset) - if obj.EnableRotation != str(self.form.enableRotation.currentText()): - obj.EnableRotation = str(self.form.enableRotation.currentText()) - - if obj.UseComp != self.form.useCompensation.isChecked(): - obj.UseComp = self.form.useCompensation.isChecked() - if obj.UseStartPoint != self.form.useStartPoint.isChecked(): - obj.UseStartPoint = self.form.useStartPoint.isChecked() - - if obj.processHoles != self.form.processHoles.isChecked(): - obj.processHoles = self.form.processHoles.isChecked() - if obj.processPerimeter != self.form.processPerimeter.isChecked(): - obj.processPerimeter = self.form.processPerimeter.isChecked() - if obj.processCircles != self.form.processCircles.isChecked(): - obj.processCircles = self.form.processCircles.isChecked() - - def setFields(self, obj): - '''setFields(obj) ... transfers obj's property values to UI''' - self.setupToolController(obj, self.form.toolController) - self.setupCoolant(obj, self.form.coolantController) - - self.selectInComboBox(obj.Side, self.form.cutSide) - self.selectInComboBox(obj.Direction, self.form.direction) - self.form.extraOffset.setText(FreeCAD.Units.Quantity(obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString) - self.selectInComboBox(obj.EnableRotation, self.form.enableRotation) - - self.form.useCompensation.setChecked(obj.UseComp) - self.form.useStartPoint.setChecked(obj.UseStartPoint) - self.form.processHoles.setChecked(obj.processHoles) - self.form.processPerimeter.setChecked(obj.processPerimeter) - self.form.processCircles.setChecked(obj.processCircles) - - self.updateVisibility(obj) - - def getSignalsForUpdate(self, obj): - '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' - signals = [] - signals.append(self.form.toolController.currentIndexChanged) - signals.append(self.form.coolantController.currentIndexChanged) - signals.append(self.form.cutSide.currentIndexChanged) - signals.append(self.form.direction.currentIndexChanged) - signals.append(self.form.extraOffset.editingFinished) - signals.append(self.form.enableRotation.currentIndexChanged) - signals.append(self.form.useCompensation.stateChanged) - signals.append(self.form.useStartPoint.stateChanged) - signals.append(self.form.processHoles.stateChanged) - signals.append(self.form.processPerimeter.stateChanged) - signals.append(self.form.processCircles.stateChanged) - - return signals - - def updateVisibility(self, obj): - hasFace = False - fullModel = False - if len(obj.Base) > 0: - for (base, subsList) in obj.Base: - for sub in subsList: - if sub[:4] == 'Face': - hasFace = True - break - else: - fullModel = True - - if hasFace: - self.form.processCircles.show() - self.form.processHoles.show() - self.form.processPerimeter.show() - else: - self.form.processCircles.hide() - self.form.processHoles.hide() - self.form.processPerimeter.hide() - - if self.form.useCompensation.isChecked() is True and not fullModel: - self.form.cutSide.show() - self.form.cutSideLabel.show() - else: - # Reset cutSide to 'Outside' for full model before hiding cutSide input - if self.form.cutSide.currentText() == 'Inside': - self.selectInComboBox('Outside', self.form.cutSide) - self.form.cutSide.hide() - self.form.cutSideLabel.hide() - - def registerSignalHandlers(self, obj): - self.form.useCompensation.stateChanged.connect(self.updateVisibility) -# Eclass - - -Command = PathOpGui.SetupOperation('Profile', - PathProfile.Create, - TaskPanelOpPage, - 'Path-Contour', - QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile"), - QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile entire model, selected face(s) or selected edge(s)"), - PathProfile.SetupProperties) - -FreeCAD.Console.PrintLog("Loading PathProfileFacesGui... done\n") +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2017 sliptonic * +# * * +# * 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.PathGui as PathGui +import PathScripts.PathOpGui as PathOpGui +import PathScripts.PathProfile as PathProfile + +from PySide import QtCore + + +__title__ = "Path Profile Operation UI" +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Profile operation page controller and command implementation." + + +FeatureSide = 0x01 +FeatureProcessing = 0x02 + +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class TaskPanelOpPage(PathOpGui.TaskPanelPage): + '''Base class for profile operation page controllers. Two sub features are supported: + FeatureSide ... Is the Side property exposed in the UI + FeatureProcessing ... Are the processing check boxes supported by the operation + ''' + + def initPage(self, obj): + self.updateVisibility(obj) + + def profileFeatures(self): + '''profileFeatures() ... return which of the optional profile features are supported. + Currently two features are supported and returned: + FeatureSide ... Is the Side property exposed in the UI + FeatureProcessing ... Are the processing check boxes supported by the operation + .''' + return FeatureSide | FeatureProcessing + + def getForm(self): + '''getForm() ... returns UI customized according to profileFeatures()''' + form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpProfileFullEdit.ui") + return form + + def getFields(self, obj): + '''getFields(obj) ... transfers values from UI to obj's proprties''' + self.updateToolController(obj, self.form.toolController) + self.updateCoolant(obj, self.form.coolantController) + + if obj.Side != str(self.form.cutSide.currentText()): + obj.Side = str(self.form.cutSide.currentText()) + if obj.Direction != str(self.form.direction.currentText()): + obj.Direction = str(self.form.direction.currentText()) + PathGui.updateInputField(obj, 'OffsetExtra', self.form.extraOffset) + if obj.EnableRotation != str(self.form.enableRotation.currentText()): + obj.EnableRotation = str(self.form.enableRotation.currentText()) + + if obj.UseComp != self.form.useCompensation.isChecked(): + obj.UseComp = self.form.useCompensation.isChecked() + if obj.UseStartPoint != self.form.useStartPoint.isChecked(): + obj.UseStartPoint = self.form.useStartPoint.isChecked() + + if obj.processHoles != self.form.processHoles.isChecked(): + obj.processHoles = self.form.processHoles.isChecked() + if obj.processPerimeter != self.form.processPerimeter.isChecked(): + obj.processPerimeter = self.form.processPerimeter.isChecked() + if obj.processCircles != self.form.processCircles.isChecked(): + obj.processCircles = self.form.processCircles.isChecked() + + def setFields(self, obj): + '''setFields(obj) ... transfers obj's property values to UI''' + self.setupToolController(obj, self.form.toolController) + self.setupCoolant(obj, self.form.coolantController) + + self.selectInComboBox(obj.Side, self.form.cutSide) + self.selectInComboBox(obj.Direction, self.form.direction) + self.form.extraOffset.setText(FreeCAD.Units.Quantity(obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString) + self.selectInComboBox(obj.EnableRotation, self.form.enableRotation) + + self.form.useCompensation.setChecked(obj.UseComp) + self.form.useStartPoint.setChecked(obj.UseStartPoint) + self.form.processHoles.setChecked(obj.processHoles) + self.form.processPerimeter.setChecked(obj.processPerimeter) + self.form.processCircles.setChecked(obj.processCircles) + + self.updateVisibility(obj) + + def getSignalsForUpdate(self, obj): + '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' + signals = [] + signals.append(self.form.toolController.currentIndexChanged) + signals.append(self.form.coolantController.currentIndexChanged) + signals.append(self.form.cutSide.currentIndexChanged) + signals.append(self.form.direction.currentIndexChanged) + signals.append(self.form.extraOffset.editingFinished) + signals.append(self.form.enableRotation.currentIndexChanged) + signals.append(self.form.useCompensation.stateChanged) + signals.append(self.form.useStartPoint.stateChanged) + signals.append(self.form.processHoles.stateChanged) + signals.append(self.form.processPerimeter.stateChanged) + signals.append(self.form.processCircles.stateChanged) + + return signals + + def updateVisibility(self, sentObj=None): + hasFace = False + hasGeom = False + fullModel = False + objBase = list() + + if sentObj: + if hasattr(sentObj, 'Base'): + objBase = sentObj.Base + elif hasattr(self.obj, 'Base'): + objBase = self.obj.Base + + if objBase.__len__() > 0: + for (base, subsList) in objBase: + for sub in subsList: + if sub[:4] == 'Face': + hasFace = True + break + else: + fullModel = True + + if hasFace: + self.form.processCircles.show() + self.form.processHoles.show() + self.form.processPerimeter.show() + else: + self.form.processCircles.hide() + self.form.processHoles.hide() + self.form.processPerimeter.hide() + + side = False + if self.form.useCompensation.isChecked() is True: + if not fullModel: + side = True + + if side: + self.form.cutSide.show() + self.form.cutSideLabel.show() + else: + # Reset cutSide to 'Outside' for full model before hiding cutSide input + if self.form.cutSide.currentText() == 'Inside': + self.selectInComboBox('Outside', self.form.cutSide) + self.form.cutSide.hide() + self.form.cutSideLabel.hide() + + def registerSignalHandlers(self, obj): + self.form.useCompensation.stateChanged.connect(self.updateVisibility) +# Eclass + + +Command = PathOpGui.SetupOperation('Profile', + PathProfile.Create, + TaskPanelOpPage, + 'Path-Contour', + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile"), + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile entire model, selected face(s) or selected edge(s)"), + PathProfile.SetupProperties) + +FreeCAD.Console.PrintLog("Loading PathProfileFacesGui... done\n")