Merge pull request #3443 from Russ4262/UnifiedProfile

Path:  Combine Contour, Profile Faces and Profile Edges into unified Profile operation
This commit is contained in:
sliptonic
2020-05-12 12:06:36 -05:00
committed by GitHub
16 changed files with 2335 additions and 2200 deletions

View File

@@ -87,14 +87,14 @@ SET(PathScripts_SRCS
PathScripts/PathPreferencesPathJob.py
PathScripts/PathProbe.py
PathScripts/PathProbeGui.py
PathScripts/PathProfileBase.py
PathScripts/PathProfileBaseGui.py
PathScripts/PathProfile.py
PathScripts/PathProfileContour.py
PathScripts/PathProfileContourGui.py
PathScripts/PathProfileEdges.py
PathScripts/PathProfileEdgesGui.py
PathScripts/PathProfileFaces.py
PathScripts/PathProfileFacesGui.py
PathScripts/PathProfileGui.py
PathScripts/PathSanity.py
PathScripts/PathSelection.py
PathScripts/PathSetupSheet.py

View File

@@ -90,7 +90,8 @@ class PathWorkbench (Workbench):
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_Contour", "Path_Profile_Faces", "Path_Profile_Edges", "Path_Pocket_Shape", "Path_Drilling", "Path_MillFace", "Path_Helix", "Path_Adaptive"]
# 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"]
threedopcmdlist = ["Path_Pocket_3D"]
engravecmdlist = ["Path_Engrave", "Path_Deburr"]
modcmdlist = ["Path_OperationCopy", "Path_Array", "Path_SimpleCopy"]

View File

@@ -354,7 +354,7 @@ class ObjectOp(PathOp.ObjectOp):
self.tempObjectNames = [] # pylint: disable=attribute-defined-outside-init
self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init
self.useTempJobClones('Delete') # Clear temporary group and recreate for temp job clones
self.profileEdgesIsOpen = False
self.rotStartDepth = None # pylint: disable=attribute-defined-outside-init
if obj.EnableRotation != 'Off':
# Calculate operation heights based upon rotation radii
@@ -371,6 +371,7 @@ class ObjectOp(PathOp.ObjectOp):
strDep = max(self.xRotRad, self.yRotRad)
finDep = -1 * strDep
self.rotStartDepth = strDep
obj.ClearanceHeight.Value = strDep + self.clrOfset
obj.SafeHeight.Value = strDep + self.safOfst
@@ -419,15 +420,17 @@ class ObjectOp(PathOp.ObjectOp):
shapes = [j['shape'] for j in jobs]
if self.profileEdgesIsOpen is True:
if PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint:
osp = obj.StartPoint
self.commandlist.append(Path.Command('G0', {'X': osp.x, 'Y': osp.y, 'F': self.horizRapid}))
sims = []
numShapes = len(shapes)
for ns in range(0, numShapes):
profileEdgesIsOpen = False
(shape, isHole, sub, angle, axis, strDep, finDep) = shapes[ns] # pylint: disable=unused-variable
if sub == 'OpenEdge':
profileEdgesIsOpen = True
if PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint:
osp = obj.StartPoint
self.commandlist.append(Path.Command('G0', {'X': osp.x, 'Y': osp.y, 'F': self.horizRapid}))
if ns < numShapes - 1:
nextAxis = shapes[ns + 1][4]
else:
@@ -436,7 +439,7 @@ class ObjectOp(PathOp.ObjectOp):
self.depthparams = self._customDepthParams(obj, strDep, finDep)
try:
if self.profileEdgesIsOpen is True:
if profileEdgesIsOpen:
(pp, sim) = self._buildProfileOpenEdges(obj, shape, isHole, start, getsim)
else:
(pp, sim) = self._buildPathArea(obj, shape, isHole, start, getsim)
@@ -444,7 +447,7 @@ class ObjectOp(PathOp.ObjectOp):
FreeCAD.Console.PrintError(e)
FreeCAD.Console.PrintError("Something unexpected happened. Check project and tool config.")
else:
if self.profileEdgesIsOpen is True:
if profileEdgesIsOpen:
ppCmds = pp
else:
ppCmds = pp.Commands

View File

@@ -64,9 +64,10 @@ def Startup():
from PathScripts import PathPocketShapeGui
from PathScripts import PathPost
from PathScripts import PathProbeGui
from PathScripts import PathProfileContourGui
from PathScripts import PathProfileEdgesGui
from PathScripts import PathProfileFacesGui
# from PathScripts import PathProfileContourGui
# from PathScripts import PathProfileEdgesGui
# from PathScripts import PathProfileFacesGui
from PathScripts import PathProfileGui
from PathScripts import PathSanity
from PathScripts import PathSetupSheetGui
from PathScripts import PathSimpleCopy

File diff suppressed because it is too large Load Diff

View File

@@ -204,6 +204,13 @@ class TaskPanelPage(object):
self.setIcon(None)
self.features = features
self.isdirty = False
self.parent = None
self.panelTitle = 'Operation'
def setParent(self, parent):
'''setParent() ... used to transfer parent object link to child class.
Do not overwrite.'''
self.parent = parent
def onDirtyChanged(self, callback):
'''onDirtyChanged(callback) ... set callback when dirty state changes.'''
@@ -387,11 +394,27 @@ class TaskPanelPage(object):
if obj.CoolantMode != option:
obj.CoolantMode = option
def updatePanelVisibility(self, panelTitle, obj):
if hasattr(self, 'parent'):
parent = getattr(self, 'parent')
if parent and hasattr(parent, 'featurePages'):
for page in parent.featurePages:
if hasattr(page, 'panelTitle'):
if page.panelTitle == panelTitle and hasattr(page, 'updateVisibility'):
page.updateVisibility(obj)
break
class TaskPanelBaseGeometryPage(TaskPanelPage):
'''Page controller for the base geometry.'''
DataObject = QtCore.Qt.ItemDataRole.UserRole
DataObjectSub = QtCore.Qt.ItemDataRole.UserRole + 1
def __init__(self, obj, features):
super(TaskPanelBaseGeometryPage, self).__init__(obj, features)
self.panelTitle = 'Base Geometry'
def getForm(self):
return FreeCADGui.PySideUic.loadUi(":/panels/PageBaseGeometryEdit.ui")
@@ -447,7 +470,9 @@ class TaskPanelBaseGeometryPage(TaskPanelPage):
def selectionSupportedAsBaseGeometry(self, selection, ignoreErrors):
if len(selection) != 1:
if not ignoreErrors:
PathLog.error(translate("PathProject", "Please select %s from a single solid" % self.featureName()))
msg = translate("PathProject", "Please select %s from a single solid" % self.featureName())
FreeCAD.Console.PrintError(msg + '\n')
PathLog.debug(msg)
return False
sel = selection[0]
if sel.HasSubObjects:
@@ -470,7 +495,6 @@ class TaskPanelBaseGeometryPage(TaskPanelPage):
return False
return True
def addBaseGeometry(self, selection):
PathLog.track(selection)
if self.selectionSupportedAsBaseGeometry(selection, False):
@@ -485,6 +509,7 @@ class TaskPanelBaseGeometryPage(TaskPanelPage):
# self.obj.Proxy.execute(self.obj)
self.setFields(self.obj)
self.setDirty()
self.updatePanelVisibility('Operation', self.obj)
def deleteBase(self):
PathLog.track()
@@ -492,6 +517,7 @@ 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()
@@ -514,6 +540,7 @@ class TaskPanelBaseGeometryPage(TaskPanelPage):
def clearBase(self):
self.obj.Base = []
self.setDirty()
self.updatePanelVisibility('Operation', self.obj)
def registerSignalHandlers(self, obj):
self.form.baseList.itemSelectionChanged.connect(self.itemActivated)
@@ -531,6 +558,7 @@ class TaskPanelBaseGeometryPage(TaskPanelPage):
else:
self.form.addBase.setEnabled(False)
class TaskPanelBaseLocationPage(TaskPanelPage):
'''Page controller for base locations. Uses PathGetPoint.'''
@@ -541,6 +569,7 @@ class TaskPanelBaseLocationPage(TaskPanelPage):
# members initialized later
self.editRow = None
self.panelTitle = 'Base Location'
def getForm(self):
self.formLoc = FreeCADGui.PySideUic.loadUi(":/panels/PageBaseLocationEdit.ui")
@@ -656,6 +685,7 @@ class TaskPanelHeightsPage(TaskPanelPage):
# members initialized later
self.clearanceHeight = None
self.safeHeight = None
self.panelTitle = 'Heights'
def getForm(self):
return FreeCADGui.PySideUic.loadUi(":/panels/PageHeightsEdit.ui")
@@ -697,6 +727,7 @@ class TaskPanelDepthsPage(TaskPanelPage):
self.finalDepth = None
self.finishDepth = None
self.stepDown = None
self.panelTitle = 'Depths'
def getForm(self):
return FreeCADGui.PySideUic.loadUi(":/panels/PageDepthsEdit.ui")
@@ -882,6 +913,7 @@ class TaskPanel(object):
for page in self.featurePages:
page.initPage(obj)
page.onDirtyChanged(self.pageDirtyChanged)
page.setParent(self)
taskPanelLayout = PathPreferences.defaultTaskPanelLayout()
@@ -945,8 +977,11 @@ class TaskPanel(object):
FreeCAD.ActiveDocument.abortTransaction()
if self.deleteOnReject:
FreeCAD.ActiveDocument.openTransaction(translate("Path", "Uncreate AreaOp Operation"))
PathUtil.clearExpressionEngine(self.obj)
FreeCAD.ActiveDocument.removeObject(self.obj.Name)
try:
PathUtil.clearExpressionEngine(self.obj)
FreeCAD.ActiveDocument.removeObject(self.obj.Name)
except Exception as ee:
PathLog.debug('{}\n'.format(ee))
FreeCAD.ActiveDocument.commitTransaction()
self.cleanup(resetEdit)
return True

File diff suppressed because it is too large Load Diff

View File

@@ -1,170 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2020 Schildkroet *
# * Copyright (c) 2020 russ4262 (Russell Johnson) *
# * *
# * 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 PathScripts.PathAreaOp as PathAreaOp
import PathScripts.PathLog as PathLog
from PySide import QtCore
__title__ = "Base Path Profile Operation"
__author__ = "sliptonic (Brad Collette), Schildkroet"
__url__ = "http://www.freecadweb.org"
__doc__ = "Base class and implementation for Path profile operations."
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 ObjectProfile(PathAreaOp.ObjectOp):
'''Base class for proxy objects of all profile operations.'''
def initAreaOp(self, obj):
'''initAreaOp(obj) ... creates all profile specific properties.
Do not overwrite.'''
# Profile Properties
obj.addProperty("App::PropertyEnumeration", "Side", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Side of edge that tool should cut"))
obj.Side = ['Outside', 'Inside'] # side of profile that cutter is on in relation to direction of profile
obj.addProperty("App::PropertyEnumeration", "Direction", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part ClockWise (CW) or CounterClockWise (CCW)"))
obj.Direction = ['CW', 'CCW'] # this is the direction that the profile runs
obj.addProperty("App::PropertyBool", "UseComp", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if using Cutter Radius Compensation"))
obj.addProperty("App::PropertyDistance", "OffsetExtra", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Extra value to stay away from final profile- good for roughing toolpath"))
obj.addProperty("App::PropertyEnumeration", "JoinType", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Controls how tool moves around corners. Default=Round"))
obj.JoinType = ['Round', 'Square', 'Miter'] # this is the direction that the Profile runs
obj.addProperty("App::PropertyFloat", "MiterLimit", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Maximum distance before a miter join is truncated"))
obj.setEditorMode('MiterLimit', 2)
def areaOpOnChanged(self, obj, prop):
'''areaOpOnChanged(obj, prop) ... updates Side and MiterLimit visibility depending on changed properties.
Do not overwrite.'''
if prop == "UseComp":
if not obj.UseComp:
obj.setEditorMode('Side', 2)
else:
obj.setEditorMode('Side', 0)
if prop == 'JoinType':
if obj.JoinType == 'Miter':
obj.setEditorMode('MiterLimit', 0)
else:
obj.setEditorMode('MiterLimit', 2)
self.extraOpOnChanged(obj, prop)
def extraOpOnChanged(self, obj, prop):
'''otherOpOnChanged(obj, porp) ... overwrite to process onChange() events.
Can safely be overwritten by subclasses.'''
pass # pylint: disable=unnecessary-pass
def setOpEditorProperties(self, obj):
'''setOpEditorProperties(obj, porp) ... overwrite to process operation specific changes to properties.
Can safely be overwritten by subclasses.'''
pass # pylint: disable=unnecessary-pass
def areaOpOnDocumentRestored(self, obj):
for prop in ['UseComp', 'JoinType']:
self.areaOpOnChanged(obj, prop)
self.setOpEditorProperties(obj)
def areaOpAreaParams(self, obj, isHole):
'''areaOpAreaParams(obj, isHole) ... returns dictionary with area parameters.
Do not overwrite.'''
params = {}
params['Fill'] = 0
params['Coplanar'] = 0
params['SectionCount'] = -1
offset = 0.0
if obj.UseComp:
offset = self.radius + obj.OffsetExtra.Value
if obj.Side == 'Inside':
offset = 0 - offset
if isHole:
offset = 0 - offset
params['Offset'] = offset
jointype = ['Round', 'Square', 'Miter']
params['JoinType'] = jointype.index(obj.JoinType)
if obj.JoinType == 'Miter':
params['MiterLimit'] = obj.MiterLimit
return params
def areaOpPathParams(self, obj, isHole):
'''areaOpPathParams(obj, isHole) ... returns dictionary with path parameters.
Do not overwrite.'''
params = {}
# Reverse the direction for holes
if isHole:
direction = "CW" if obj.Direction == "CCW" else "CCW"
else:
direction = obj.Direction
if direction == 'CCW':
params['orientation'] = 0
else:
params['orientation'] = 1
if not obj.UseComp:
if direction == 'CCW':
params['orientation'] = 1
else:
params['orientation'] = 0
return params
def areaOpUseProjection(self, obj):
'''areaOpUseProjection(obj) ... returns True'''
return True
def areaOpSetDefaultValues(self, obj, job):
'''areaOpSetDefaultValues(obj, job) ... sets default values.
Do not overwrite.'''
obj.Side = "Outside"
obj.OffsetExtra = 0.0
obj.Direction = "CW"
obj.UseComp = True
obj.JoinType = "Round"
obj.MiterLimit = 0.1
def SetupProperties():
setup = PathAreaOp.SetupProperties()
setup.append('Side')
setup.append('OffsetExtra')
setup.append('Direction')
setup.append('UseComp')
setup.append('JoinType')
setup.append('MiterLimit')
return setup

View File

@@ -21,100 +21,32 @@
# * USA *
# * *
# ***************************************************************************
from __future__ import print_function
# * Major modifications: 2020 Russell Johnson <russ4262@gmail.com> *
import FreeCAD
import Path
import PathScripts.PathProfileBase as PathProfileBase
import PathScripts.PathLog as PathLog
import PathScripts.PathProfile as PathProfile
from PathScripts import PathUtils
from PySide import QtCore
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel')
Part = LazyLoader('Part', globals(), 'Part')
FreeCAD.setLogLevel('Path.Area', 0)
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)
__title__ = "Path Contour Operation"
__title__ = "Path Contour Operation (depreciated)"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Implementation of the Contour operation."
__doc__ = "Implementation of the Contour operation (depreciated)."
class ObjectContour(PathProfileBase.ObjectProfile):
'''Proxy object for Contour operations.'''
class ObjectContour(PathProfile.ObjectProfile):
'''Psuedo class for Profile operation,
allowing for backward compatibility with pre-existing "Contour" operations.'''
pass
# Eclass
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 0, Contour only requires the base profile features.'''
return 0
def initAreaOp(self, obj):
'''initAreaOp(obj) ... call super's implementation and hide Side property.'''
self.baseObject().initAreaOp(obj)
obj.setEditorMode('Side', 2) # it's always outside
def areaOpOnDocumentRestored(self, obj):
obj.setEditorMode('Side', 2) # it's always outside
def areaOpSetDefaultValues(self, obj, job):
'''areaOpSetDefaultValues(obj, job) ... call super's implementation and set Side="Outside".'''
self.baseObject().areaOpSetDefaultValues(obj, job)
obj.Side = 'Outside'
def areaOpShapes(self, obj):
'''areaOpShapes(obj) ... return envelope over the job's Base.Shape or all Arch.Panel shapes.'''
if obj.UseComp:
self.commandlist.append(Path.Command("(Compensated Tool Path. Diameter: " + str(self.radius * 2) + ")"))
else:
self.commandlist.append(Path.Command("(Uncompensated Tool Path)"))
isPanel = False
if 1 == len(self.model) and hasattr(self.model[0], "Proxy"):
if isinstance(self.model[0].Proxy, ArchPanel.PanelSheet): # process the sheet
panel = self.model[0]
isPanel = True
panel.Proxy.execute(panel)
shapes = panel.Proxy.getOutlines(panel, transform=True)
for shape in shapes:
f = Part.makeFace([shape], 'Part::FaceMakerSimple')
thickness = panel.Group[0].Source.Thickness
return [(f.extrude(FreeCAD.Vector(0, 0, thickness)), False)]
if not isPanel:
return [(PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthparams), False) for base in self.model if hasattr(base, 'Shape')]
def areaOpAreaParams(self, obj, isHole):
params = self.baseObject().areaOpAreaParams(obj, isHole)
params['Coplanar'] = 2
return params
def opUpdateDepths(self, obj):
obj.OpStartDepth = obj.OpStockZMax
obj.OpFinalDepth = obj.OpStockZMin
def SetupProperties():
return [p for p in PathProfileBase.SetupProperties() if p != 'Side']
return PathProfile.SetupProperties()
def Create(name, obj = None):
'''Create(name) ... Creates and returns a Contour operation.'''
def Create(name, obj=None):
'''Create(name) ... Creates and returns a Profile operation.'''
if obj is None:
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
obj.Proxy = ObjectContour(obj, name)
return obj

View File

@@ -21,32 +21,34 @@
# * USA *
# * *
# ***************************************************************************
# * Major modifications: 2020 Russell Johnson <russ4262@gmail.com> *
import FreeCAD
import PathScripts.PathOpGui as PathOpGui
import PathScripts.PathProfileBaseGui as PathProfileBaseGui
import PathScripts.PathProfileContour as PathProfileContour
import PathScripts.PathProfile as PathProfile
import PathScripts.PathProfileGui as PathProfileGui
from PySide import QtCore
__title__ = "Path Contour Operation UI"
__title__ = "Path Contour Operation UI (depreciated)"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Contour operation page controller and command implementation."
__doc__ = "Contour operation page controller and command implementation (depreciated)."
class TaskPanelOpPage(PathProfileBaseGui.TaskPanelOpPage):
'''Page controller for the contour operation UI.'''
def profileFeatures(self):
'''profileFeatues() ... return 0 - profile doesn't support any of the optional UI features.'''
return 0
class TaskPanelOpPage(PathProfileGui.TaskPanelOpPage):
'''Psuedo page controller class for Profile operation,
allowing for backward compatibility with pre-existing "Contour" operations.'''
pass
# Eclass
Command = PathOpGui.SetupOperation('Contour',
PathProfileContour.Create,
Command = PathOpGui.SetupOperation('Profile',
PathProfile.Create,
TaskPanelOpPage,
'Path-Contour',
QtCore.QT_TRANSLATE_NOOP("PathProfileContour", "Contour"),
QtCore.QT_TRANSLATE_NOOP("PathProfileContour", "Creates a Contour Path for the Base Object "),
PathProfileContour.SetupProperties)
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 PathProfileContourGui... done\n")

View File

@@ -21,937 +21,32 @@
# * USA *
# * *
# ***************************************************************************
# * Major modifications: 2020 Russell Johnson <russ4262@gmail.com> *
import FreeCAD
import Path
import PathScripts.PathLog as PathLog
import PathScripts.PathOp as PathOp
import PathScripts.PathProfileBase as PathProfileBase
import PathScripts.PathUtils as PathUtils
import math
import PySide
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
Part = LazyLoader('Part', globals(), 'Part')
DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils')
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# PathLog.trackModule(PathLog.thisModule())
import PathScripts.PathProfile as PathProfile
# Qt translation handling
def translate(context, text, disambig=None):
return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
__title__ = "Path Profile Edges Operation"
__title__ = "Path Profile Edges Operation (depreciated)"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Path Profile operation based on edges."
__doc__ = "Path Profile operation based on edges (depreciated)."
__contributors__ = "russ4262 (Russell Johnson)"
class ObjectProfile(PathProfileBase.ObjectProfile):
'''Proxy object for Profile operations based on edges.'''
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) ... add support for edge base geometry.'''
return PathOp.FeatureBaseEdges
def areaOpShapes(self, obj):
'''areaOpShapes(obj) ... returns envelope for all wires formed by the base edges.'''
PathLog.track()
inaccessible = translate('PathProfileEdges', 'The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.')
if PathLog.getLevel(PathLog.thisModule()) == 4:
self.tmpGrp = FreeCAD.ActiveDocument.addObject('App::DocumentObjectGroup', 'tmpDebugGrp')
tmpGrpNm = self.tmpGrp.Name
self.JOB = PathUtils.findParentJob(obj)
self.offsetExtra = abs(obj.OffsetExtra.Value)
if obj.UseComp:
self.useComp = True
self.ofstRadius = self.radius + self.offsetExtra
self.commandlist.append(Path.Command("(Compensated Tool Path. Diameter: " + str(self.radius * 2) + ")"))
else:
self.useComp = False
self.ofstRadius = self.offsetExtra
self.commandlist.append(Path.Command("(Uncompensated Tool Path)"))
shapes = []
if obj.Base:
basewires = []
zMin = None
for b in obj.Base:
edgelist = []
for sub in b[1]:
edgelist.append(getattr(b[0].Shape, sub))
basewires.append((b[0], DraftGeomUtils.findWires(edgelist)))
if zMin is None or b[0].Shape.BoundBox.ZMin < zMin:
zMin = b[0].Shape.BoundBox.ZMin
PathLog.debug('PathProfileEdges areaOpShapes():: len(basewires) is {}'.format(len(basewires)))
for base, wires in basewires:
for wire in wires:
if wire.isClosed() is True:
# f = Part.makeFace(wire, 'Part::FaceMakerSimple')
# if planar error, Comment out previous line, uncomment the next two
(origWire, flatWire) = self._flattenWire(obj, wire, obj.FinalDepth.Value)
f = origWire.Wires[0]
if f is not False:
# shift the compound to the bottom of the base object for proper sectioning
zShift = zMin - f.BoundBox.ZMin
newPlace = FreeCAD.Placement(FreeCAD.Vector(0, 0, zShift), f.Placement.Rotation)
f.Placement = newPlace
env = PathUtils.getEnvelope(base.Shape, subshape=f, depthparams=self.depthparams)
shapes.append((env, False))
else:
PathLog.error(inaccessible)
else:
if self.JOB.GeometryTolerance.Value == 0.0:
msg = self.JOB.Label + '.GeometryTolerance = 0.0.'
msg += translate('PathProfileEdges', 'Please set to an acceptable value greater than zero.')
PathLog.error(msg)
else:
cutWireObjs = False
flattened = self._flattenWire(obj, wire, obj.FinalDepth.Value)
if flattened:
(origWire, flatWire) = flattened
if PathLog.getLevel(PathLog.thisModule()) == 4:
os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpFlatWire')
os.Shape = flatWire
os.purgeTouched()
self.tmpGrp.addObject(os)
cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire)
if cutShp is not False:
cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp)
if cutWireObjs is not False:
for cW in cutWireObjs:
shapes.append((cW, False))
self.profileEdgesIsOpen = True
else:
PathLog.error(inaccessible)
else:
PathLog.error(inaccessible)
# Delete the temporary objects
if PathLog.getLevel(PathLog.thisModule()) == 4:
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False
self.tmpGrp.purgeTouched()
return shapes
def _flattenWire(self, obj, wire, trgtDep):
'''_flattenWire(obj, wire)... Return a flattened version of the wire'''
PathLog.debug('_flattenWire()')
wBB = wire.BoundBox
if wBB.ZLength > 0.0:
PathLog.debug('Wire is not horizontally co-planar. Flattening it.')
# Extrude non-horizontal wire
extFwdLen = wBB.ZLength * 2.2
mbbEXT = wire.extrude(FreeCAD.Vector(0, 0, extFwdLen))
# Create cross-section of shape and translate
sliceZ = wire.BoundBox.ZMin + (extFwdLen / 2)
crsectFaceShp = self._makeCrossSection(mbbEXT, sliceZ, trgtDep)
if crsectFaceShp is not False:
return (wire, crsectFaceShp)
else:
return False
else:
srtWire = Part.Wire(Part.__sortEdges__(wire.Edges))
srtWire.translate(FreeCAD.Vector(0, 0, trgtDep - srtWire.BoundBox.ZMin))
return (wire, srtWire)
# Open-edges methods
def _getCutAreaCrossSection(self, obj, base, origWire, flatWire):
PathLog.debug('_getCutAreaCrossSection()')
FCAD = FreeCAD.ActiveDocument
tolerance = self.JOB.GeometryTolerance.Value
toolDiam = 2 * self.radius # self.radius defined in PathAreaOp or PathProfileBase modules
minBfr = toolDiam * 1.25
bbBfr = (self.ofstRadius * 2) * 1.25
if bbBfr < minBfr:
bbBfr = minBfr
fwBB = flatWire.BoundBox
wBB = origWire.BoundBox
minArea = (self.ofstRadius - tolerance)**2 * math.pi
useWire = origWire.Wires[0]
numOrigEdges = len(useWire.Edges)
sdv = wBB.ZMax
fdv = obj.FinalDepth.Value
extLenFwd = sdv - fdv
if extLenFwd <= 0.0:
msg = translate('PathProfile',
'For open edges, select top edge and set Final Depth manually.')
FreeCAD.Console.PrintError(msg + '\n')
return False
WIRE = flatWire.Wires[0]
numEdges = len(WIRE.Edges)
# Identify first/last edges and first/last vertex on wire
begE = WIRE.Edges[0] # beginning edge
endE = WIRE.Edges[numEdges - 1] # ending edge
blen = begE.Length
elen = endE.Length
Vb = begE.Vertexes[0] # first vertex of wire
Ve = endE.Vertexes[1] # last vertex of wire
pb = FreeCAD.Vector(Vb.X, Vb.Y, fdv)
pe = FreeCAD.Vector(Ve.X, Ve.Y, fdv)
# Identify endpoints connecting circle center and diameter
vectDist = pe.sub(pb)
diam = vectDist.Length
cntr = vectDist.multiply(0.5).add(pb)
R = diam / 2
pl = FreeCAD.Placement()
pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0)
pl.Base = FreeCAD.Vector(0, 0, 0)
# Obtain beginning point perpendicular points
if blen > 0.1:
bcp = begE.valueAt(begE.getParameterByLength(0.1)) # point returned 0.1 mm along edge
else:
bcp = FreeCAD.Vector(begE.Vertexes[1].X, begE.Vertexes[1].Y, fdv)
if elen > 0.1:
ecp = endE.valueAt(endE.getParameterByLength(elen - 0.1)) # point returned 0.1 mm along edge
else:
ecp = FreeCAD.Vector(endE.Vertexes[1].X, endE.Vertexes[1].Y, fdv)
# Create intersection tags for determining which side of wire to cut
(begInt, begExt, iTAG, eTAG) = self._makeIntersectionTags(useWire, numOrigEdges, fdv)
if not begInt or not begExt:
return False
self.iTAG = iTAG
self.eTAG = eTAG
# Create extended wire boundbox, and extrude
extBndbox = self._makeExtendedBoundBox(wBB, bbBfr, fdv)
extBndboxEXT = extBndbox.extrude(FreeCAD.Vector(0, 0, extLenFwd))
# Cut model(selected edges) from extended edges boundbox
cutArea = extBndboxEXT.cut(base.Shape)
if PathLog.getLevel(PathLog.thisModule()) == 4:
CA = FCAD.addObject('Part::Feature', 'tmpCutArea')
CA.Shape = cutArea
CA.recompute()
CA.purgeTouched()
self.tmpGrp.addObject(CA)
# Get top and bottom faces of cut area (CA), and combine faces when necessary
topFc = list()
botFc = list()
bbZMax = cutArea.BoundBox.ZMax
bbZMin = cutArea.BoundBox.ZMin
for f in range(0, len(cutArea.Faces)):
FcBB = cutArea.Faces[f].BoundBox
if abs(FcBB.ZMax - bbZMax) < tolerance and abs(FcBB.ZMin - bbZMax) < tolerance:
topFc.append(f)
if abs(FcBB.ZMax - bbZMin) < tolerance and abs(FcBB.ZMin - bbZMin) < tolerance:
botFc.append(f)
if len(topFc) == 0:
PathLog.error('Failed to identify top faces of cut area.')
return False
topComp = Part.makeCompound([cutArea.Faces[f] for f in topFc])
topComp.translate(FreeCAD.Vector(0, 0, fdv - topComp.BoundBox.ZMin)) # Translate face to final depth
if len(botFc) > 1:
PathLog.debug('len(botFc) > 1')
bndboxFace = Part.Face(extBndbox.Wires[0])
tmpFace = Part.Face(extBndbox.Wires[0])
for f in botFc:
Q = tmpFace.cut(cutArea.Faces[f])
tmpFace = Q
botComp = bndboxFace.cut(tmpFace)
else:
botComp = Part.makeCompound([cutArea.Faces[f] for f in botFc]) # Part.makeCompound([CA.Shape.Faces[f] for f in botFc])
botComp.translate(FreeCAD.Vector(0, 0, fdv - botComp.BoundBox.ZMin)) # Translate face to final depth
# Make common of the two
comFC = topComp.common(botComp)
# Determine with which set of intersection tags the model intersects
(cmnIntArea, cmnExtArea) = self._checkTagIntersection(iTAG, eTAG, 'QRY', comFC)
if cmnExtArea > cmnIntArea:
PathLog.debug('Cutting on Ext side.')
self.cutSide = 'E'
self.cutSideTags = eTAG
tagCOM = begExt.CenterOfMass
else:
PathLog.debug('Cutting on Int side.')
self.cutSide = 'I'
self.cutSideTags = iTAG
tagCOM = begInt.CenterOfMass
# Make two beginning style(oriented) 'L' shape stops
begStop = self._makeStop('BEG', bcp, pb, 'BegStop')
altBegStop = self._makeStop('END', bcp, pb, 'BegStop')
# Identify to which style 'L' stop the beginning intersection tag is closest,
# and create partner end 'L' stop geometry, and save for application later
lenBS_extETag = begStop.CenterOfMass.sub(tagCOM).Length
lenABS_extETag = altBegStop.CenterOfMass.sub(tagCOM).Length
if lenBS_extETag < lenABS_extETag:
endStop = self._makeStop('END', ecp, pe, 'EndStop')
pathStops = Part.makeCompound([begStop, endStop])
else:
altEndStop = self._makeStop('BEG', ecp, pe, 'EndStop')
pathStops = Part.makeCompound([altBegStop, altEndStop])
pathStops.translate(FreeCAD.Vector(0, 0, fdv - pathStops.BoundBox.ZMin))
# Identify closed wire in cross-section that corresponds to user-selected edge(s)
workShp = comFC
fcShp = workShp
wire = origWire
WS = workShp.Wires
lenWS = len(WS)
if lenWS < 3:
wi = 0
else:
wi = None
for wvt in wire.Vertexes:
for w in range(0, lenWS):
twr = WS[w]
for v in range(0, len(twr.Vertexes)):
V = twr.Vertexes[v]
if abs(V.X - wvt.X) < tolerance:
if abs(V.Y - wvt.Y) < tolerance:
# Same vertex found. This wire to be used for offset
wi = w
break
# Efor
if wi is None:
PathLog.error('The cut area cross-section wire does not coincide with selected edge. Wires[] index is None.')
return False
else:
PathLog.debug('Cross-section Wires[] index is {}.'.format(wi))
nWire = Part.Wire(Part.__sortEdges__(workShp.Wires[wi].Edges))
fcShp = Part.Face(nWire)
fcShp.translate(FreeCAD.Vector(0, 0, fdv - workShp.BoundBox.ZMin))
# Eif
# verify that wire chosen is not inside the physical model
if wi > 0: # and isInterior is False:
PathLog.debug('Multiple wires in cut area. First choice is not 0. Testing.')
testArea = fcShp.cut(base.Shape)
isReady = self._checkTagIntersection(iTAG, eTAG, self.cutSide, testArea)
PathLog.debug('isReady {}.'.format(isReady))
if isReady is False:
PathLog.debug('Using wire index {}.'.format(wi - 1))
pWire = Part.Wire(Part.__sortEdges__(workShp.Wires[wi - 1].Edges))
pfcShp = Part.Face(pWire)
pfcShp.translate(FreeCAD.Vector(0, 0, fdv - workShp.BoundBox.ZMin))
workShp = pfcShp.cut(fcShp)
if testArea.Area < minArea:
PathLog.debug('offset area is less than minArea of {}.'.format(minArea))
PathLog.debug('Using wire index {}.'.format(wi - 1))
pWire = Part.Wire(Part.__sortEdges__(workShp.Wires[wi - 1].Edges))
pfcShp = Part.Face(pWire)
pfcShp.translate(FreeCAD.Vector(0, 0, fdv - workShp.BoundBox.ZMin))
workShp = pfcShp.cut(fcShp)
# Eif
# Add path stops at ends of wire
cutShp = workShp.cut(pathStops)
return cutShp
def _checkTagIntersection(self, iTAG, eTAG, cutSide, tstObj):
# Identify intersection of Common area and Interior Tags
intCmn = tstObj.common(iTAG)
# Identify intersection of Common area and Exterior Tags
extCmn = tstObj.common(eTAG)
# Calculate common intersection (solid model side, or the non-cut side) area with tags, to determine physical cut side
cmnIntArea = intCmn.Area
cmnExtArea = extCmn.Area
if cutSide == 'QRY':
return (cmnIntArea, cmnExtArea)
if cmnExtArea > cmnIntArea:
PathLog.debug('Cutting on Ext side.')
if cutSide == 'E':
return True
else:
PathLog.debug('Cutting on Int side.')
if cutSide == 'I':
return True
return False
def _extractPathWire(self, obj, base, flatWire, cutShp):
PathLog.debug('_extractPathWire()')
subLoops = list()
rtnWIRES = list()
osWrIdxs = list()
subDistFactor = 1.0 # Raise to include sub wires at greater distance from original
fdv = obj.FinalDepth.Value
wire = flatWire
lstVrtIdx = len(wire.Vertexes) - 1
lstVrt = wire.Vertexes[lstVrtIdx]
frstVrt = wire.Vertexes[0]
cent0 = FreeCAD.Vector(frstVrt.X, frstVrt.Y, fdv)
cent1 = FreeCAD.Vector(lstVrt.X, lstVrt.Y, fdv)
pl = FreeCAD.Placement()
pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0)
pl.Base = FreeCAD.Vector(0, 0, 0)
# Calculate offset shape, containing cut region
ofstShp = self._extractFaceOffset(obj, cutShp, False)
# CHECK for ZERO area of offset shape
try:
osArea = ofstShp.Area
except Exception as ee:
PathLog.error('No area to offset shape returned.')
return False
if PathLog.getLevel(PathLog.thisModule()) == 4:
os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOffsetShape')
os.Shape = ofstShp
os.recompute()
os.purgeTouched()
self.tmpGrp.addObject(os)
numOSWires = len(ofstShp.Wires)
for w in range(0, numOSWires):
osWrIdxs.append(w)
# Identify two vertexes for dividing offset loop
NEAR0 = self._findNearestVertex(ofstShp, cent0)
min0i = 0
min0 = NEAR0[0][4]
for n in range(0, len(NEAR0)):
N = NEAR0[n]
if N[4] < min0:
min0 = N[4]
min0i = n
(w0, vi0, pnt0, vrt0, d0) = NEAR0[0] # min0i
if PathLog.getLevel(PathLog.thisModule()) == 4:
near0 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear0')
near0.Shape = Part.makeLine(cent0, pnt0)
near0.recompute()
near0.purgeTouched()
self.tmpGrp.addObject(near0)
NEAR1 = self._findNearestVertex(ofstShp, cent1)
min1i = 0
min1 = NEAR1[0][4]
for n in range(0, len(NEAR1)):
N = NEAR1[n]
if N[4] < min1:
min1 = N[4]
min1i = n
(w1, vi1, pnt1, vrt1, d1) = NEAR1[0] # min1i
if PathLog.getLevel(PathLog.thisModule()) == 4:
near1 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear1')
near1.Shape = Part.makeLine(cent1, pnt1)
near1.recompute()
near1.purgeTouched()
self.tmpGrp.addObject(near1)
if w0 != w1:
PathLog.warning('Offset wire endpoint indexes are not equal - w0, w1: {}, {}'.format(w0, w1))
if PathLog.getLevel(PathLog.thisModule()) == 4:
PathLog.debug('min0i is {}.'.format(min0i))
PathLog.debug('min1i is {}.'.format(min1i))
PathLog.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0]))
PathLog.debug('NEAR1[{}] is {}.'.format(w1, NEAR1[w1]))
PathLog.debug('NEAR0 is {}.'.format(NEAR0))
PathLog.debug('NEAR1 is {}.'.format(NEAR1))
mainWire = ofstShp.Wires[w0]
# Check for additional closed loops in offset wire by checking distance to iTAG or eTAG elements
if numOSWires > 1:
# check all wires for proximity(children) to intersection tags
tagsComList = list()
for T in self.cutSideTags.Faces:
tcom = T.CenterOfMass
tv = FreeCAD.Vector(tcom.x, tcom.y, 0.0)
tagsComList.append(tv)
subDist = self.ofstRadius * subDistFactor
for w in osWrIdxs:
if w != w0:
cutSub = False
VTXS = ofstShp.Wires[w].Vertexes
for V in VTXS:
v = FreeCAD.Vector(V.X, V.Y, 0.0)
for t in tagsComList:
if t.sub(v).Length < subDist:
cutSub = True
break
if cutSub is True:
break
if cutSub is True:
sub = Part.Wire(Part.__sortEdges__(ofstShp.Wires[w].Edges))
subLoops.append(sub)
# Eif
# Break offset loop into two wires - one of which is the desired profile path wire.
(edgeIdxs0, edgeIdxs1) = self._separateWireAtVertexes(mainWire, mainWire.Vertexes[vi0], mainWire.Vertexes[vi1])
edgs0 = list()
edgs1 = list()
for e in edgeIdxs0:
edgs0.append(mainWire.Edges[e])
for e in edgeIdxs1:
edgs1.append(mainWire.Edges[e])
part0 = Part.Wire(Part.__sortEdges__(edgs0))
part1 = Part.Wire(Part.__sortEdges__(edgs1))
# Determine which part is nearest original edge(s)
distToPart0 = self._distMidToMid(wire.Wires[0], part0.Wires[0])
distToPart1 = self._distMidToMid(wire.Wires[0], part1.Wires[0])
if distToPart0 < distToPart1:
rtnWIRES.append(part0)
else:
rtnWIRES.append(part1)
rtnWIRES.extend(subLoops)
return rtnWIRES
def _extractFaceOffset(self, obj, fcShape, isHole):
'''_extractFaceOffset(obj, fcShape, isHole) ... internal function.
Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
Adjustments made based on notes by @sliptonic - https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
PathLog.debug('_extractFaceOffset()')
areaParams = {}
JOB = PathUtils.findParentJob(obj)
tolrnc = JOB.GeometryTolerance.Value
if self.useComp is True:
offset = self.ofstRadius # + tolrnc
else:
offset = self.offsetExtra # + tolrnc
if isHole is False:
offset = 0 - offset
areaParams['Offset'] = offset
areaParams['Fill'] = 1
areaParams['Coplanar'] = 0
areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
areaParams['Reorient'] = True
areaParams['OpenMode'] = 0
areaParams['MaxArcPoints'] = 400 # 400
areaParams['Project'] = True
# areaParams['JoinType'] = 1
area = Path.Area() # Create instance of Area() class object
area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane
area.add(fcShape) # obj.Shape to use for extracting offset
area.setParams(**areaParams) # set parameters
return area.getShape()
def _findNearestVertex(self, shape, point):
PathLog.debug('_findNearestVertex()')
PT = FreeCAD.Vector(point.x, point.y, 0.0)
def sortDist(tup):
return tup[4]
PNTS = list()
for w in range(0, len(shape.Wires)):
WR = shape.Wires[w]
V = WR.Vertexes[0]
P = FreeCAD.Vector(V.X, V.Y, 0.0)
dist = P.sub(PT).Length
vi = 0
pnt = P
vrt = V
for v in range(0, len(WR.Vertexes)):
V = WR.Vertexes[v]
P = FreeCAD.Vector(V.X, V.Y, 0.0)
d = P.sub(PT).Length
if d < dist:
dist = d
vi = v
pnt = P
vrt = V
PNTS.append((w, vi, pnt, vrt, dist))
PNTS.sort(key=sortDist)
return PNTS
def _separateWireAtVertexes(self, wire, VV1, VV2):
PathLog.debug('_separateWireAtVertexes()')
tolerance = self.JOB.GeometryTolerance.Value
grps = [[], []]
wireIdxs = [[], []]
V1 = FreeCAD.Vector(VV1.X, VV1.Y, VV1.Z)
V2 = FreeCAD.Vector(VV2.X, VV2.Y, VV2.Z)
lenE = len(wire.Edges)
FLGS = list()
for e in range(0, lenE):
FLGS.append(0)
chk4 = False
for e in range(0, lenE):
v = 0
E = wire.Edges[e]
fv0 = FreeCAD.Vector(E.Vertexes[0].X, E.Vertexes[0].Y, E.Vertexes[0].Z)
fv1 = FreeCAD.Vector(E.Vertexes[1].X, E.Vertexes[1].Y, E.Vertexes[1].Z)
if fv0.sub(V1).Length < tolerance:
v = 1
if fv1.sub(V2).Length < tolerance:
v += 3
chk4 = True
elif fv1.sub(V1).Length < tolerance:
v = 1
if fv0.sub(V2).Length < tolerance:
v += 3
chk4 = True
if fv0.sub(V2).Length < tolerance:
v = 3
if fv1.sub(V1).Length < tolerance:
v += 1
chk4 = True
elif fv1.sub(V2).Length < tolerance:
v = 3
if fv0.sub(V1).Length < tolerance:
v += 1
chk4 = True
FLGS[e] += v
# Efor
PathLog.debug('_separateWireAtVertexes() FLGS: \n{}'.format(FLGS))
PRE = list()
POST = list()
IDXS = list()
IDX1 = list()
IDX2 = list()
for e in range(0, lenE):
f = FLGS[e]
PRE.append(f)
POST.append(f)
IDXS.append(e)
IDX1.append(e)
IDX2.append(e)
PRE.extend(FLGS)
PRE.extend(POST)
lenFULL = len(PRE)
IDXS.extend(IDX1)
IDXS.extend(IDX2)
if chk4 is True:
# find beginning 1 edge
begIdx = None
begFlg = False
for e in range(0, lenFULL):
f = PRE[e]
i = IDXS[e]
if f == 4:
begIdx = e
grps[0].append(f)
wireIdxs[0].append(i)
break
# find first 3 edge
endIdx = None
for e in range(begIdx + 1, lenE + begIdx):
f = PRE[e]
i = IDXS[e]
grps[1].append(f)
wireIdxs[1].append(i)
else:
# find beginning 1 edge
begIdx = None
begFlg = False
for e in range(0, lenFULL):
f = PRE[e]
if f == 1:
if begFlg is False:
begFlg = True
else:
begIdx = e
break
# find first 3 edge and group all first wire edges
endIdx = None
for e in range(begIdx, lenE + begIdx):
f = PRE[e]
i = IDXS[e]
if f == 3:
grps[0].append(f)
wireIdxs[0].append(i)
endIdx = e
break
else:
grps[0].append(f)
wireIdxs[0].append(i)
# Collect remaining edges
for e in range(endIdx + 1, lenFULL):
f = PRE[e]
i = IDXS[e]
if f == 1:
grps[1].append(f)
wireIdxs[1].append(i)
break
else:
wireIdxs[1].append(i)
grps[1].append(f)
# Efor
# Eif
if PathLog.getLevel(PathLog.thisModule()) != 4:
PathLog.debug('grps[0]: {}'.format(grps[0]))
PathLog.debug('grps[1]: {}'.format(grps[1]))
PathLog.debug('wireIdxs[0]: {}'.format(wireIdxs[0]))
PathLog.debug('wireIdxs[1]: {}'.format(wireIdxs[1]))
PathLog.debug('PRE: {}'.format(PRE))
PathLog.debug('IDXS: {}'.format(IDXS))
return (wireIdxs[0], wireIdxs[1])
def _makeCrossSection(self, shape, sliceZ, zHghtTrgt=False):
'''_makeCrossSection(shape, sliceZ, zHghtTrgt=None)...
Creates cross-section objectc from shape. Translates cross-section to zHghtTrgt if available.
Makes face shape from cross-section object. Returns face shape at zHghtTrgt.'''
# Create cross-section of shape and translate
wires = list()
slcs = shape.slice(FreeCAD.Vector(0, 0, 1), sliceZ)
if len(slcs) > 0:
for i in slcs:
wires.append(i)
comp = Part.Compound(wires)
if zHghtTrgt is not False:
comp.translate(FreeCAD.Vector(0, 0, zHghtTrgt - comp.BoundBox.ZMin))
return comp
return False
def _makeExtendedBoundBox(self, wBB, bbBfr, zDep):
p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep)
p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep)
p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep)
p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep)
L1 = Part.makeLine(p1, p2)
L2 = Part.makeLine(p2, p3)
L3 = Part.makeLine(p3, p4)
L4 = Part.makeLine(p4, p1)
return Part.Face(Part.Wire([L1, L2, L3, L4]))
def _makeIntersectionTags(self, useWire, numOrigEdges, fdv):
# Create circular probe tags around perimiter of wire
extTags = list()
intTags = list()
tagRad = (self.radius / 2)
tagCnt = 0
begInt = False
begExt = False
for e in range(0, numOrigEdges):
E = useWire.Edges[e]
LE = E.Length
if LE > (self.radius * 2):
nt = math.ceil(LE / (tagRad * math.pi)) # (tagRad * 2 * math.pi) is circumference
else:
nt = 4 # desired + 1
mid = LE / nt
spc = self.radius / 10
for i in range(0, nt):
if i == 0:
if e == 0:
if LE > 0.2:
aspc = 0.1
else:
aspc = LE * 0.75
cp1 = E.valueAt(E.getParameterByLength(0))
cp2 = E.valueAt(E.getParameterByLength(aspc))
(intTObj, extTObj) = self._makeOffsetCircleTag(cp1, cp2, tagRad, fdv, 'BeginEdge[{}]_'.format(e))
if intTObj and extTObj:
begInt = intTObj
begExt = extTObj
else:
d = i * mid
cp1 = E.valueAt(E.getParameterByLength(d - spc))
cp2 = E.valueAt(E.getParameterByLength(d + spc))
(intTObj, extTObj) = self._makeOffsetCircleTag(cp1, cp2, tagRad, fdv, 'Edge[{}]_'.format(e))
if intTObj and extTObj:
tagCnt += nt
intTags.append(intTObj)
extTags.append(extTObj)
tagArea = math.pi * tagRad**2 * tagCnt
iTAG = Part.makeCompound(intTags)
eTAG = Part.makeCompound(extTags)
return (begInt, begExt, iTAG, eTAG)
def _makeOffsetCircleTag(self, p1, p2, cutterRad, depth, lbl, reverse=False):
pb = FreeCAD.Vector(p1.x, p1.y, 0.0)
pe = FreeCAD.Vector(p2.x, p2.y, 0.0)
toMid = pe.sub(pb).multiply(0.5)
lenToMid = toMid.Length
if lenToMid == 0.0:
# Probably a vertical line segment
return (False, False)
cutFactor = (cutterRad / 2.1) / lenToMid # = 2 is tangent to wire; > 2 allows tag to overlap wire; < 2 pulls tag away from wire
perpE = FreeCAD.Vector(-1 * toMid.y, toMid.x, 0.0).multiply(-1 * cutFactor) # exterior tag
extPnt = pb.add(toMid.add(perpE))
pl = FreeCAD.Placement()
pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0)
# make exterior tag
eCntr = extPnt.add(FreeCAD.Vector(0, 0, depth))
ecw = Part.Wire(Part.makeCircle((cutterRad / 2), eCntr).Edges[0])
extTag = Part.Face(ecw)
# make interior tag
perpI = FreeCAD.Vector(-1 * toMid.y, toMid.x, 0.0).multiply(cutFactor) # interior tag
intPnt = pb.add(toMid.add(perpI))
iCntr = intPnt.add(FreeCAD.Vector(0, 0, depth))
icw = Part.Wire(Part.makeCircle((cutterRad / 2), iCntr).Edges[0])
intTag = Part.Face(icw)
return (intTag, extTag)
def _makeStop(self, sType, pA, pB, lbl):
rad = self.radius
ofstRad = self.ofstRadius
extra = self.radius / 10
pl = FreeCAD.Placement()
pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0)
pl.Base = FreeCAD.Vector(0, 0, 0)
E = FreeCAD.Vector(pB.x, pB.y, 0) # endpoint
C = FreeCAD.Vector(pA.x, pA.y, 0) # checkpoint
lenEC = E.sub(C).Length
if self.useComp is True or (self.useComp is False and self.offsetExtra != 0):
# 'L' stop shape and edge legend
# --1--
# | |
# 2 6
# | |
# | ----5----|
# | 4
# -----3-------|
# positive dist in _makePerp2DVector() is CCW rotation
p1 = E
if sType == 'BEG':
p2 = self._makePerp2DVector(C, E, -0.25) # E1
p3 = self._makePerp2DVector(p1, p2, ofstRad + 1 + extra) # E2
p4 = self._makePerp2DVector(p2, p3, 0.25 + ofstRad + extra) # E3
p5 = self._makePerp2DVector(p3, p4, 1 + extra) # E4
p6 = self._makePerp2DVector(p4, p5, ofstRad + extra) # E5
elif sType == 'END':
p2 = self._makePerp2DVector(C, E, 0.25) # E1
p3 = self._makePerp2DVector(p1, p2, -1 * (ofstRad + 1 + extra)) # E2
p4 = self._makePerp2DVector(p2, p3, -1 * (0.25 + ofstRad + extra)) # E3
p5 = self._makePerp2DVector(p3, p4, -1 * (1 + extra)) # E4
p6 = self._makePerp2DVector(p4, p5, -1 * (ofstRad + extra)) # E5
p7 = E # E6
L1 = Part.makeLine(p1, p2)
L2 = Part.makeLine(p2, p3)
L3 = Part.makeLine(p3, p4)
L4 = Part.makeLine(p4, p5)
L5 = Part.makeLine(p5, p6)
L6 = Part.makeLine(p6, p7)
wire = Part.Wire([L1, L2, L3, L4, L5, L6])
else:
# 'L' stop shape and edge legend
# :
# |----2-------|
# 3 1
# |-----4------|
# positive dist in _makePerp2DVector() is CCW rotation
p1 = E
if sType == 'BEG':
p2 = self._makePerp2DVector(C, E, -1 * (0.25 + abs(self.offsetExtra))) # left, 0.25
p3 = self._makePerp2DVector(p1, p2, 0.25 + abs(self.offsetExtra))
p4 = self._makePerp2DVector(p2, p3, (0.5 + abs(self.offsetExtra))) # FIRST POINT
p5 = self._makePerp2DVector(p3, p4, 0.25 + abs(self.offsetExtra)) # E1 SECOND
elif sType == 'END':
p2 = self._makePerp2DVector(C, E, (0.25 + abs(self.offsetExtra))) # left, 0.25
p3 = self._makePerp2DVector(p1, p2, -1 * (0.25 + abs(self.offsetExtra)))
p4 = self._makePerp2DVector(p2, p3, -1 * (0.5 + abs(self.offsetExtra))) # FIRST POINT
p5 = self._makePerp2DVector(p3, p4, -1 * (0.25 + abs(self.offsetExtra))) # E1 SECOND
p6 = p1 # E4
L1 = Part.makeLine(p1, p2)
L2 = Part.makeLine(p2, p3)
L3 = Part.makeLine(p3, p4)
L4 = Part.makeLine(p4, p5)
L5 = Part.makeLine(p5, p6)
wire = Part.Wire([L1, L2, L3, L4, L5])
# Eif
face = Part.Face(wire)
if PathLog.getLevel(PathLog.thisModule()) == 4:
os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp' + lbl)
os.Shape = face
os.recompute()
os.purgeTouched()
self.tmpGrp.addObject(os)
return face
def _makePerp2DVector(self, v1, v2, dist):
p1 = FreeCAD.Vector(v1.x, v1.y, 0.0)
p2 = FreeCAD.Vector(v2.x, v2.y, 0.0)
toEnd = p2.sub(p1)
factor = dist / toEnd.Length
perp = FreeCAD.Vector(-1 * toEnd.y, toEnd.x, 0.0).multiply(factor)
return p1.add(toEnd.add(perp))
def _distMidToMid(self, wireA, wireB):
mpA = self._findWireMidpoint(wireA)
mpB = self._findWireMidpoint(wireB)
return mpA.sub(mpB).Length
def _findWireMidpoint(self, wire):
midPnt = None
dist = 0.0
wL = wire.Length
midW = wL / 2
for e in range(0, len(wire.Edges)):
E = wire.Edges[e]
elen = E.Length
d_ = dist + elen
if dist < midW and midW <= d_:
dtm = midW - dist
midPnt = E.valueAt(E.getParameterByLength(dtm))
break
else:
dist += elen
return midPnt
class ObjectProfile(PathProfile.ObjectProfile):
'''Psuedo class for Profile operation,
allowing for backward compatibility with pre-existing "Profile Edges" operations.'''
pass
# Eclass
def SetupProperties():
return PathProfileBase.SetupProperties()
return PathProfile.SetupProperties()
def Create(name, obj = None):
'''Create(name) ... Creates and returns a Profile based on edges operation.'''
def Create(name, obj=None):
'''Create(name) ... Creates and returns a Profile operation.'''
if obj is None:
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
obj.Proxy = ObjectProfile(obj, name)

View File

@@ -21,33 +21,34 @@
# * USA *
# * *
# ***************************************************************************
# * Major modifications: 2020 Russell Johnson <russ4262@gmail.com> *
import FreeCAD
import PathScripts.PathOpGui as PathOpGui
import PathScripts.PathProfileBaseGui as PathProfileBaseGui
import PathScripts.PathProfileEdges as PathProfileEdges
import PathScripts.PathProfile as PathProfile
import PathScripts.PathProfileGui as PathProfileGui
from PySide import QtCore
__title__ = "Path Profile based on edges Operation UI"
__title__ = "Path Profile Edges Operation UI (depreciated)"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Profile based on edges operation page controller and command implementation."
__doc__ = "Profile Edges operation page controller and command implementation (depreciated)."
class TaskPanelOpPage(PathProfileBaseGui.TaskPanelOpPage):
'''Page controller for profile based on edges operation.'''
def profileFeatures(self):
'''profileFeatures() ... return FeatureSide
See PathProfileBaseGui.py for details.'''
return PathProfileBaseGui.FeatureSide
class TaskPanelOpPage(PathProfileGui.TaskPanelOpPage):
'''Psuedo page controller class for Profile operation,
allowing for backward compatibility with pre-existing "Profile Edges" operations.'''
pass
# Eclass
Command = PathOpGui.SetupOperation('Profile Edges',
PathProfileEdges.Create,
Command = PathOpGui.SetupOperation('Profile',
PathProfile.Create,
TaskPanelOpPage,
'Path-Profile-Edges',
QtCore.QT_TRANSLATE_NOOP("PathProfile", "Edge Profile"),
QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile based on edges"),
PathProfileEdges.SetupProperties)
'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 PathProfileEdgesGui... done\n")

View File

@@ -22,328 +22,32 @@
# * USA *
# * *
# ***************************************************************************
# * Major modifications: 2020 Russell Johnson <russ4262@gmail.com> *
import FreeCAD
import Path
import PathScripts.PathLog as PathLog
import PathScripts.PathOp as PathOp
import PathScripts.PathProfileBase as PathProfileBase
import PathScripts.PathUtils as PathUtils
import numpy
import PathScripts.PathProfile as PathProfile
from PySide import QtCore
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel')
Part = LazyLoader('Part', globals(), 'Part')
__title__ = "Path Profile Faces Operation"
__author__ = "sliptonic (Brad Collette), Schildkroet"
__title__ = "Path Profile Faces Operation (depreciated)"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Path Profile operation based on faces."
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
__doc__ = "Path Profile operation based on faces (depreciated)."
__contributors__ = "Schildkroet"
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
class ObjectProfile(PathProfileBase.ObjectProfile):
'''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):
'''baseObject() ... returns super of receiver
Used to call base implementation in overwritten functions.'''
# return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels | PathOp.FeatureRotation
return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels
def initAreaOp(self, obj):
'''initAreaOp(obj) ... adds properties for hole, circle and perimeter processing.'''
# Face specific Properties
obj.addProperty("App::PropertyBool", "processHoles", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile holes as well as the outline"))
obj.addProperty("App::PropertyBool", "processPerimeter", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the outline"))
obj.addProperty("App::PropertyBool", "processCircles", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile round holes"))
if not hasattr(obj, 'HandleMultipleFeatures'):
obj.addProperty('App::PropertyEnumeration', 'HandleMultipleFeatures', 'Profile', QtCore.QT_TRANSLATE_NOOP('PathPocket', 'Choose how to process multiple Base Geometry features.'))
obj.HandleMultipleFeatures = ['Collectively', 'Individually']
self.initRotationOp(obj)
self.baseObject().initAreaOp(obj)
def initRotationOp(self, obj):
'''initRotationOp(obj) ... setup receiver for rotation'''
if not hasattr(obj, 'ReverseDirection'):
obj.addProperty('App::PropertyBool', 'ReverseDirection', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Reverse direction of pocket operation.'))
if not hasattr(obj, 'InverseAngle'):
obj.addProperty('App::PropertyBool', 'InverseAngle', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Inverse the angle. Example: -22.5 -> 22.5 degrees.'))
if not hasattr(obj, 'AttemptInverseAngle'):
obj.addProperty('App::PropertyBool', 'AttemptInverseAngle', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Attempt the inverse angle for face access if original rotation fails.'))
if not hasattr(obj, 'LimitDepthToFace'):
obj.addProperty('App::PropertyBool', 'LimitDepthToFace', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Enforce the Z-depth of the selected face as the lowest value for final depth. Higher user values will be observed.'))
def extraOpOnChanged(self, obj, prop):
'''extraOpOnChanged(obj, porp) ... process operation specific changes to properties.'''
if prop == 'EnableRotation':
self.setOpEditorProperties(obj)
def setOpEditorProperties(self, obj):
if obj.EnableRotation == 'Off':
obj.setEditorMode('ReverseDirection', 2)
obj.setEditorMode('InverseAngle', 2)
obj.setEditorMode('AttemptInverseAngle', 2)
obj.setEditorMode('LimitDepthToFace', 2)
else:
obj.setEditorMode('ReverseDirection', 0)
obj.setEditorMode('InverseAngle', 0)
obj.setEditorMode('AttemptInverseAngle', 0)
obj.setEditorMode('LimitDepthToFace', 0)
def areaOpShapes(self, obj):
'''areaOpShapes(obj) ... returns envelope for all base shapes or wires for Arch.Panels.'''
PathLog.track()
if obj.UseComp:
self.commandlist.append(Path.Command("(Compensated Tool Path. Diameter: " + str(self.radius * 2) + ")"))
else:
self.commandlist.append(Path.Command("(Uncompensated Tool Path)"))
shapes = []
self.profileshape = [] # pylint: disable=attribute-defined-outside-init
baseSubsTuples = []
subCount = 0
allTuples = []
if obj.Base: # The user has selected subobjects from the base. Process each.
if obj.EnableRotation != 'Off':
for p in range(0, len(obj.Base)):
(base, subsList) = obj.Base[p]
for sub in subsList:
subCount += 1
shape = getattr(base.Shape, sub)
if isinstance(shape, Part.Face):
rtn = False
(norm, surf) = self.getFaceNormAndSurf(shape)
(rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable
PathLog.debug("initial faceRotationAnalysis: {}".format(praInfo))
if rtn is True:
(clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount)
# Verify faces are correctly oriented - InverseAngle might be necessary
faceIA = getattr(clnBase.Shape, sub)
(norm, surf) = self.getFaceNormAndSurf(faceIA)
(rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable
PathLog.debug("follow-up faceRotationAnalysis: {}".format(praInfo2))
if abs(praAngle) == 180.0:
rtn = False
if self.isFaceUp(clnBase, faceIA) is False:
PathLog.debug('isFaceUp 1 is False')
angle -= 180.0
if rtn is True:
PathLog.debug(translate("Path", "Face appears misaligned after initial rotation."))
if obj.InverseAngle is False:
if obj.AttemptInverseAngle is True:
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.")
PathLog.warning(msg)
if self.isFaceUp(clnBase, faceIA) is False:
PathLog.debug('isFaceUp 2 is False')
angle += 180.0
else:
PathLog.debug(' isFaceUp')
else:
PathLog.debug("Face appears to be oriented correctly.")
if angle < 0.0:
angle += 360.0
tup = clnBase, sub, tag, angle, axis, clnStock
else:
if self.warnDisabledAxis(obj, axis) is False:
PathLog.debug(str(sub) + ": No rotation used")
axis = 'X'
angle = 0.0
tag = base.Name + '_' + axis + str(angle).replace('.', '_')
stock = PathUtils.findParentJob(obj).Stock
tup = base, sub, tag, angle, axis, stock
allTuples.append(tup)
if subCount > 1:
msg = translate('Path', "Multiple faces in Base Geometry.") + " "
msg += translate('Path', "Depth settings will be applied to all faces.")
PathLog.warning(msg)
(Tags, Grps) = self.sortTuplesByIndex(allTuples, 2) # return (TagList, GroupList)
subList = []
for o in range(0, len(Tags)):
subList = []
for (base, sub, tag, angle, axis, stock) in Grps[o]:
subList.append(sub)
pair = base, subList, angle, axis, stock
baseSubsTuples.append(pair)
# Efor
else:
PathLog.debug(translate("Path", "EnableRotation property is 'Off'."))
stock = PathUtils.findParentJob(obj).Stock
for (base, subList) in obj.Base:
baseSubsTuples.append((base, subList, 0.0, 'X', stock))
# for base in obj.Base:
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
for (base, subsList, angle, axis, stock) in baseSubsTuples:
holes = []
faces = []
faceDepths = []
startDepths = []
for sub in subsList:
shape = getattr(base.Shape, sub)
if isinstance(shape, Part.Face):
faces.append(shape)
if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face
for wire in shape.Wires[1:]:
holes.append((base.Shape, wire))
# Add face depth to list
faceDepths.append(shape.BoundBox.ZMin)
else:
ignoreSub = base.Name + '.' + sub
msg = translate('Path', "Found a selected object which is not a face. Ignoring: {}".format(ignoreSub))
PathLog.error(msg)
FreeCAD.Console.PrintWarning(msg)
# Set initial Start and Final Depths and recalculate depthparams
finDep = obj.FinalDepth.Value
strDep = obj.StartDepth.Value
if strDep > stock.Shape.BoundBox.ZMax:
strDep = stock.Shape.BoundBox.ZMax
startDepths.append(strDep)
self.depthparams = self._customDepthParams(obj, strDep, finDep)
for shape, wire in holes:
f = Part.makeFace(wire, 'Part::FaceMakerSimple')
drillable = PathUtils.isDrillable(shape, wire)
if (drillable and obj.processCircles) or (not drillable and obj.processHoles):
env = PathUtils.getEnvelope(shape, subshape=f, depthparams=self.depthparams)
tup = env, True, 'pathProfileFaces', angle, axis, strDep, finDep
shapes.append(tup)
if len(faces) > 0:
profileshape = Part.makeCompound(faces)
self.profileshape.append(profileshape)
if obj.processPerimeter:
if obj.HandleMultipleFeatures == 'Collectively':
custDepthparams = self.depthparams
if obj.LimitDepthToFace is True and obj.EnableRotation != 'Off':
if profileshape.BoundBox.ZMin > obj.FinalDepth.Value:
finDep = profileshape.BoundBox.ZMin
envDepthparams = self._customDepthParams(obj, strDep + 0.5, finDep) # only an envelope
try:
# env = PathUtils.getEnvelope(base.Shape, subshape=profileshape, depthparams=envDepthparams)
env = PathUtils.getEnvelope(profileshape, depthparams=envDepthparams)
except Exception: # pylint: disable=broad-except
# PathUtils.getEnvelope() failed to return an object.
PathLog.error(translate('Path', 'Unable to create path for face(s).'))
else:
tup = env, False, 'pathProfileFaces', angle, axis, strDep, finDep
shapes.append(tup)
elif obj.HandleMultipleFeatures == 'Individually':
for shape in faces:
# profShape = Part.makeCompound([shape])
finalDep = obj.FinalDepth.Value
custDepthparams = self.depthparams
if obj.Side == 'Inside':
if finalDep < shape.BoundBox.ZMin:
# Recalculate depthparams
finalDep = shape.BoundBox.ZMin
custDepthparams = self._customDepthParams(obj, strDep + 0.5, finalDep)
# env = PathUtils.getEnvelope(base.Shape, subshape=profShape, depthparams=custDepthparams)
env = PathUtils.getEnvelope(shape, depthparams=custDepthparams)
tup = env, False, 'pathProfileFaces', angle, axis, strDep, finalDep
shapes.append(tup)
# Lower high Start Depth to top of Stock
startDepth = max(startDepths)
if obj.StartDepth.Value > startDepth:
obj.StartDepth.Value = startDepth
else: # Try to build targets from the job base
if 1 == len(self.model):
if hasattr(self.model[0], "Proxy"):
PathLog.debug("hasattr() Proxy")
if isinstance(self.model[0].Proxy, ArchPanel.PanelSheet): # process the sheet
if obj.processCircles or obj.processHoles:
for shape in self.model[0].Proxy.getHoles(self.model[0], transform=True):
for wire in shape.Wires:
drillable = PathUtils.isDrillable(self.model[0].Proxy, wire)
if (drillable and obj.processCircles) or (not drillable and obj.processHoles):
f = Part.makeFace(wire, 'Part::FaceMakerSimple')
env = PathUtils.getEnvelope(self.model[0].Shape, subshape=f, depthparams=self.depthparams)
tup = env, True, 'pathProfileFaces', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value
shapes.append(tup)
if obj.processPerimeter:
for shape in self.model[0].Proxy.getOutlines(self.model[0], transform=True):
for wire in shape.Wires:
f = Part.makeFace(wire, 'Part::FaceMakerSimple')
env = PathUtils.getEnvelope(self.model[0].Shape, subshape=f, depthparams=self.depthparams)
tup = env, False, 'pathProfileFaces', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value
shapes.append(tup)
self.removalshapes = shapes # pylint: disable=attribute-defined-outside-init
PathLog.debug("%d shapes" % len(shapes))
return shapes
def areaOpSetDefaultValues(self, obj, job):
'''areaOpSetDefaultValues(obj, job) ... sets default values for hole, circle and perimeter processing.'''
self.baseObject().areaOpSetDefaultValues(obj, job)
obj.processHoles = False
obj.processCircles = False
obj.processPerimeter = True
obj.ReverseDirection = False
obj.InverseAngle = False
obj.AttemptInverseAngle = True
obj.LimitDepthToFace = True
obj.HandleMultipleFeatures = 'Individually'
class ObjectProfile(PathProfile.ObjectProfile):
'''Psuedo class for Profile operation,
allowing for backward compatibility with pre-existing "Profile Faces" operations.'''
pass
# Eclass
def SetupProperties():
setup = PathProfileBase.SetupProperties()
setup.append("processHoles")
setup.append("processPerimeter")
setup.append("processCircles")
setup.append("ReverseDirection")
setup.append("InverseAngle")
setup.append("AttemptInverseAngle")
setup.append("HandleMultipleFeatures")
return setup
return PathProfile.SetupProperties()
def Create(name, obj=None):
'''Create(name) ... Creates and returns a Profile based on faces operation.'''
'''Create(name) ... Creates and returns a Profile operation.'''
if obj is None:
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
obj.Proxy = ObjectProfile(obj, name)

View File

@@ -21,33 +21,34 @@
# * USA *
# * *
# ***************************************************************************
# * Major modifications: 2020 Russell Johnson <russ4262@gmail.com> *
import FreeCAD
import PathScripts.PathOpGui as PathOpGui
import PathScripts.PathProfileBaseGui as PathProfileBaseGui
import PathScripts.PathProfileFaces as PathProfileFaces
import PathScripts.PathProfile as PathProfile
import PathScripts.PathProfileGui as PathProfileGui
from PySide import QtCore
__title__ = "Path Profile based on faces Operation UI"
__title__ = "Path Profile Faces Operation UI (depreciated)"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Profile based on faces operation page controller and command implementation."
__doc__ = "Profile Faces operation page controller and command implementation (depreciated)."
class TaskPanelOpPage(PathProfileBaseGui.TaskPanelOpPage):
'''Page controller for profile based on faces operation.'''
def profileFeatures(self):
'''profileFeatures() ... return FeatureSide | FeatureProcessing.
See PathProfileBaseGui.py for details.'''
return PathProfileBaseGui.FeatureSide | PathProfileBaseGui.FeatureProcessing
class TaskPanelOpPage(PathProfileGui.TaskPanelOpPage):
'''Psuedo page controller class for Profile operation,
allowing for backward compatibility with pre-existing "Profile Faces" operations.'''
pass
# Eclass
Command = PathOpGui.SetupOperation('Profile Faces',
PathProfileFaces.Create,
Command = PathOpGui.SetupOperation('Profile',
PathProfile.Create,
TaskPanelOpPage,
'Path-Profile-Face',
QtCore.QT_TRANSLATE_NOOP("PathProfile", "Face Profile"),
QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile based on face or faces"),
PathProfileFaces.SetupProperties)
'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")

View File

@@ -26,112 +26,162 @@ 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 Base UI"
__title__ = "Path Profile Operation UI"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Base page controller for profile operations."
__doc__ = "Profile operation page controller and command implementation."
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
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
support
'''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:
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
Must be overwritten by subclasses.'''
.'''
return FeatureSide | FeatureProcessing
def getForm(self):
'''getForm() ... returns UI customized according to profileFeatures()'''
form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpProfileFullEdit.ui")
if not FeatureSide & self.profileFeatures():
form.cutSide.hide()
form.cutSideLabel.hide()
if not FeatureProcessing & self.profileFeatures():
form.processCircles.hide()
form.processHoles.hide()
form.processPerimeter.hide()
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.Direction != str(self.form.direction.currentText()):
obj.Direction = str(self.form.direction.currentText())
if obj.EnableRotation != str(self.form.enableRotation.currentText()):
obj.EnableRotation = str(self.form.enableRotation.currentText())
self.updateToolController(obj, self.form.toolController)
self.updateCoolant(obj, self.form.coolantController)
if FeatureSide & self.profileFeatures():
if obj.Side != str(self.form.cutSide.currentText()):
obj.Side = str(self.form.cutSide.currentText())
if FeatureProcessing & self.profileFeatures():
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()
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.form.extraOffset.setText(FreeCAD.Units.Quantity(obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString)
self.form.useCompensation.setChecked(obj.UseComp)
self.form.useStartPoint.setChecked(obj.UseStartPoint)
self.selectInComboBox(obj.Direction, self.form.direction)
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)
if FeatureSide & self.profileFeatures():
self.selectInComboBox(obj.Side, self.form.cutSide)
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)
if FeatureProcessing & self.profileFeatures():
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.direction.currentIndexChanged)
signals.append(self.form.useCompensation.clicked)
signals.append(self.form.useStartPoint.clicked)
signals.append(self.form.extraOffset.editingFinished)
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)
if FeatureSide & self.profileFeatures():
signals.append(self.form.cutSide.currentIndexChanged)
if FeatureProcessing & self.profileFeatures():
signals.append(self.form.processHoles.clicked)
signals.append(self.form.processPerimeter.clicked)
signals.append(self.form.processCircles.clicked)
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")

View File

@@ -102,8 +102,29 @@ class DRILLGate(PathBaseGate):
return False
class PROFILEGate(PathBaseGate):
class FACEGate(PathBaseGate): # formerly PROFILEGate class using allow_ORIG method as allow()
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
profileable = False
try:
obj = obj.Shape
except Exception: # pylint: disable=broad-except
return False
if obj.ShapeType == 'Compound':
if sub and sub[0:4] == 'Face':
profileable = True
elif obj.ShapeType == 'Face': # 3D Face, not flat, planar?
profileable = True # Was False
elif obj.ShapeType == 'Solid':
if sub and sub[0:4] == 'Face':
profileable = True
return profileable
def allow_ORIG(self, doc, obj, sub): # pylint: disable=unused-argument
profileable = False
try:
@@ -137,6 +158,33 @@ class PROFILEGate(PathBaseGate):
return profileable
class PROFILEGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
if sub and sub[0:4] == 'Edge':
return True
try:
obj = obj.Shape
except Exception: # pylint: disable=broad-except
return False
if obj.ShapeType == 'Compound':
if sub and sub[0:4] == 'Face':
return True
elif obj.ShapeType == 'Face':
return True
elif obj.ShapeType == 'Solid':
if sub and sub[0:4] == 'Face':
return True
elif obj.ShapeType == 'Wire':
return True
return False
class POCKETGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
@@ -179,10 +227,12 @@ class CONTOURGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
pass
class PROBEGate:
def allow(self, doc, obj, sub):
pass
def contourselect():
FreeCADGui.Selection.addSelectionGate(CONTOURGate())
FreeCAD.Console.PrintWarning("Contour Select Mode\n")
@@ -203,6 +253,11 @@ def engraveselect():
FreeCAD.Console.PrintWarning("Engraving Select Mode\n")
def fselect():
FreeCADGui.Selection.addSelectionGate(FACEGate()) # Was PROFILEGate()
FreeCAD.Console.PrintWarning("Profiling Select Mode\n")
def chamferselect():
FreeCADGui.Selection.addSelectionGate(CHAMFERGate())
FreeCAD.Console.PrintWarning("Deburr Select Mode\n")
@@ -224,21 +279,21 @@ def adaptiveselect():
def surfaceselect():
if(MESHGate() is True or PROFILEGate() is True):
FreeCADGui.Selection.addSelectionGate(True)
else:
FreeCADGui.Selection.addSelectionGate(False)
# FreeCADGui.Selection.addSelectionGate(MESHGate())
# FreeCADGui.Selection.addSelectionGate(PROFILEGate()) # Added for face selection
gate = False
if(MESHGate() or FACEGate()):
gate = True
FreeCADGui.Selection.addSelectionGate(gate)
FreeCAD.Console.PrintWarning("Surfacing Select Mode\n")
def probeselect():
FreeCADGui.Selection.addSelectionGate(PROBEGate())
FreeCAD.Console.PrintWarning("Probe Select Mode\n")
def select(op):
opsel = {}
opsel['Contour'] = contourselect
opsel['Contour'] = contourselect # (depreciated)
opsel['Deburr'] = chamferselect
opsel['Drilling'] = drillselect
opsel['Engrave'] = engraveselect
@@ -247,8 +302,9 @@ def select(op):
opsel['Pocket'] = pocketselect
opsel['Pocket 3D'] = pocketselect
opsel['Pocket Shape'] = pocketselect
opsel['Profile Edges'] = eselect
opsel['Profile Faces'] = profileselect
opsel['Profile Edges'] = eselect # (depreciated)
opsel['Profile Faces'] = fselect # (depreciated)
opsel['Profile'] = profileselect
opsel['Surface'] = surfaceselect
opsel['Waterline'] = surfaceselect
opsel['Adaptive'] = adaptiveselect