Merge pull request #3443 from Russ4262/UnifiedProfile
Path: Combine Contour, Profile Faces and Profile Edges into unified Profile operation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
1426
src/Mod/Path/PathScripts/PathProfile.py
Normal file
1426
src/Mod/Path/PathScripts/PathProfile.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user