Path: Remove first-draft rotational code Path

Remove all first-draft rotational-related code from Path workbench to make way for more stable, long-term integration of a rotational indexing capability.
This commit is contained in:
Russell Johnson
2021-05-27 10:01:19 -05:00
parent 4b7a5d2e98
commit 03cce806f3
15 changed files with 232 additions and 2514 deletions

View File

@@ -73,33 +73,6 @@
</property>
</widget>
</item>
<item row="1" column="4">
<widget class="QLabel" name="peckDepthLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Depth</string>
</property>
</widget>
</item>
<item row="3" column="4">
<widget class="QLabel" name="retractLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Retract</string>
</property>
</widget>
</item>
<item row="1" column="6">
<widget class="Gui::QuantitySpinBox" name="peckDepth">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QCheckBox" name="peckEnabled">
<property name="text">
@@ -107,34 +80,6 @@
</property>
</widget>
</item>
<item row="5" column="1" rowspan="2" colspan="2">
<widget class="QCheckBox" name="dwellEnabled">
<property name="text">
<string>Dwell</string>
</property>
</widget>
</item>
<item row="3" column="6">
<widget class="Gui::QuantitySpinBox" name="peckRetractHeight">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="6" column="6">
<widget class="Gui::QuantitySpinBox" name="dwellTime">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="7" column="4">
<widget class="QLabel" name="Offsetlabel">
<property name="text">
<string>Extend Depth</string>
</property>
</widget>
</item>
<item row="7" column="6">
<widget class="QComboBox" name="ExtraOffset">
<item>
@@ -154,34 +99,58 @@
</item>
</widget>
</item>
<item row="8" column="6">
<widget class="QComboBox" name="enableRotation">
<item>
<property name="text">
<string>Off</string>
</property>
</item>
<item>
<property name="text">
<string>A(x)</string>
</property>
</item>
<item>
<property name="text">
<string>B(y)</string>
</property>
</item>
<item>
<property name="text">
<string>A &amp; B</string>
</property>
</item>
<item row="3" column="4">
<widget class="QLabel" name="retractLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Retract</string>
</property>
</widget>
</item>
<item row="8" column="4">
<widget class="QLabel" name="label">
<item row="3" column="6">
<widget class="Gui::QuantitySpinBox" name="peckRetractHeight">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="6" column="6">
<widget class="Gui::QuantitySpinBox" name="dwellTime">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="6">
<widget class="Gui::QuantitySpinBox" name="peckDepth">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="1" rowspan="2" colspan="2">
<widget class="QCheckBox" name="dwellEnabled">
<property name="text">
<string>Enable Rotation</string>
<string>Dwell</string>
</property>
</widget>
</item>
<item row="1" column="4">
<widget class="QLabel" name="peckDepthLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Depth</string>
</property>
</widget>
</item>
<item row="7" column="4">
<widget class="QLabel" name="Offsetlabel">
<property name="text">
<string>Extend Depth</string>
</property>
</widget>
</item>

View File

@@ -230,37 +230,6 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="enableRotation_label">
<property name="text">
<string>Enable Rotation</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="enableRotation">
<item>
<property name="text">
<string>Off</string>
</property>
</item>
<item>
<property name="text">
<string>A(x)</string>
</property>
</item>
<item>
<property name="text">
<string>B(y)</string>
</property>
</item>
<item>
<property name="text">
<string>A &amp; B</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -57,10 +57,10 @@
<item row="1" column="0">
<widget class="QWidget" name="widget" native="true">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="cutSideLabel">
<item row="1" column="0">
<widget class="QLabel" name="directionLabel">
<property name="text">
<string>Cut Side</string>
<string>Direction</string>
</property>
</widget>
</item>
@@ -81,10 +81,16 @@
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="directionLabel">
<property name="text">
<string>Direction</string>
<item row="2" column="1">
<widget class="Gui::InputField" name="extraOffset">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The amount of extra material left by this operation in relation to the target shape.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
@@ -105,6 +111,13 @@
</item>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="cutSideLabel">
<property name="text">
<string>Cut Side</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="extraOffsetLabel">
<property name="text">
@@ -112,50 +125,6 @@
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="Gui::InputField" name="extraOffset">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The amount of extra material left by this operation in relation to the target shape.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Enable Rotation</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="enableRotation">
<item>
<property name="text">
<string>Off</string>
</property>
</item>
<item>
<property name="text">
<string>A(x)</string>
</property>
</item>
<item>
<property name="text">
<string>B(y)</string>
</property>
</item>
<item>
<property name="text">
<string>A &amp; B</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -41,12 +41,11 @@ __title__ = "Base class for PathArea based operations."
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Base class and properties for Path.Area based operations."
__contributors__ = "russ4262 (Russell Johnson)"
LOGLEVEL = PathLog.Level.INFO
PathLog.setLevel(LOGLEVEL, PathLog.thisModule())
if LOGLEVEL is PathLog.Level.DEBUG:
PathLog.trackModule()
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# PathLog.trackModule(PathLog.thisModule())
# Qt translation handling
@@ -66,7 +65,9 @@ class ObjectOp(PathOp.ObjectOp):
'''opFeatures(obj) ... returns the base features supported by all Path.Area based operations.
The standard feature list is OR'ed with the return value of areaOpFeatures().
Do not overwrite, implement areaOpFeatures(obj) instead.'''
return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureStepDown | PathOp.FeatureHeights | PathOp.FeatureStartPoint | self.areaOpFeatures(obj) | PathOp.FeatureCoolant
return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureStepDown \
| PathOp.FeatureHeights | PathOp.FeatureStartPoint \
| self.areaOpFeatures(obj) | PathOp.FeatureCoolant
def areaOpFeatures(self, obj):
'''areaOpFeatures(obj) ... overwrite to add operation specific features.
@@ -79,9 +80,6 @@ class ObjectOp(PathOp.ObjectOp):
Do not overwrite, overwrite initAreaOp(obj) instead.'''
PathLog.track()
# These are static while document is open, if it contains a 3D Surface Op
self.initWithRotation = False
# Debugging
obj.addProperty("App::PropertyString", "AreaParams", "Path")
obj.setEditorMode('AreaParams', 2) # hide
@@ -89,16 +87,9 @@ class ObjectOp(PathOp.ObjectOp):
obj.setEditorMode('PathParams', 2) # hide
obj.addProperty("Part::PropertyPartShape", "removalshape", "Path")
obj.setEditorMode('removalshape', 2) # hide
# obj.Proxy = self
self.setupAdditionalProperties(obj)
self.initAreaOp(obj)
def setupAdditionalProperties(self, obj):
if not hasattr(obj, 'EnableRotation'):
obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis."))
obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B']
def initAreaOp(self, obj):
'''initAreaOp(obj) ... overwrite if the receiver class needs initialisation.
Can safely be overwritten by subclasses.'''
@@ -150,7 +141,6 @@ class ObjectOp(PathOp.ObjectOp):
if hasattr(obj, prop):
obj.setEditorMode(prop, 2)
self.setupAdditionalProperties(obj)
self.areaOpOnDocumentRestored(obj)
def areaOpOnDocumentRestored(self, obj):
@@ -164,14 +154,6 @@ class ObjectOp(PathOp.ObjectOp):
Do not overwrite, overwrite areaOpSetDefaultValues(obj, job) instead.'''
PathLog.debug("opSetDefaultValues(%s, %s)" % (obj.Label, job.Label))
# Initial setting for EnableRotation is taken from Job settings/SetupSheet
# User may override on per-operation basis as needed.
if hasattr(job.SetupSheet, 'SetupEnableRotation'):
obj.EnableRotation = job.SetupSheet.SetupEnableRotation
else:
obj.EnableRotation = 'Off'
PathLog.debug("opSetDefaultValues(): Enable Rotation: {}".format(obj.EnableRotation))
if PathOp.FeatureDepths & self.opFeatures(obj):
try:
shape = self.areaOpShapeForDepths(obj, job)
@@ -189,26 +171,8 @@ class ObjectOp(PathOp.ObjectOp):
startDepth = bb.ZMax
finalDepth = bb.ZMin
# Adjust start and final depths if rotation is enabled
if obj.EnableRotation != 'Off':
self.initWithRotation = True
self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init
# Calculate rotational distances/radii
opHeights = self.opDetermineRotationRadii(obj) # return is list with tuples [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfset)]
(xRotRad, yRotRad, zRotRad) = opHeights[0] # pylint: disable=unused-variable
PathLog.debug("opHeights[0]: " + str(opHeights[0]))
PathLog.debug("opHeights[1]: " + str(opHeights[1]))
if obj.EnableRotation == 'A(x)':
startDepth = xRotRad
if obj.EnableRotation == 'B(y)':
startDepth = yRotRad
else:
startDepth = max(xRotRad, yRotRad)
finalDepth = -1 * startDepth
obj.StartDepth.Value = startDepth
obj.FinalDepth.Value = finalDepth
# obj.StartDepth.Value = startDepth
# obj.FinalDepth.Value = finalDepth
obj.OpStartDepth.Value = startDepth
obj.OpFinalDepth.Value = finalDepth
@@ -338,49 +302,9 @@ class ObjectOp(PathOp.ObjectOp):
# Instantiate class variables for operation reference
self.endVector = None # pylint: disable=attribute-defined-outside-init
self.rotateFlag = False # pylint: disable=attribute-defined-outside-init
self.leadIn = 2.0 # pylint: disable=attribute-defined-outside-init
self.cloneNames = [] # pylint: disable=attribute-defined-outside-init
self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init
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.rotStartDepth = None # pylint: disable=attribute-defined-outside-init
start_depth = obj.StartDepth.Value
final_depth = obj.FinalDepth.Value
if obj.EnableRotation != 'Off':
# Calculate operation heights based upon rotation radii
opHeights = self.opDetermineRotationRadii(obj)
(self.xRotRad, self.yRotRad, self.zRotRad) = opHeights[0] # pylint: disable=attribute-defined-outside-init
(self.clrOfset, self.safOfst) = opHeights[1] # pylint: disable=attribute-defined-outside-init
# Set clearance and safe heights based upon rotation radii
if obj.EnableRotation == 'A(x)':
start_depth = self.xRotRad
elif obj.EnableRotation == 'B(y)':
start_depth = self.yRotRad
else:
start_depth = max(self.xRotRad, self.yRotRad)
final_depth = -1 * start_depth
self.rotStartDepth = start_depth
# The next two lines are improper code.
# The ClearanceHeight and SafeHeight need to be set in opSetDefaultValues() method.
# They should not be redefined here, so this entire `if...:` statement needs relocated.
obj.ClearanceHeight.Value = start_depth + self.clrOfset
obj.SafeHeight.Value = start_depth + self.safOfst
# Create visual axes when debugging.
if PathLog.getLevel(PathLog.thisModule()) == 4:
self.visualAxis()
# Set axial feed rates based upon horizontal feed rates
safeCircum = 2 * math.pi * obj.SafeHeight.Value
self.axialFeed = 360 / safeCircum * self.horizFeed # pylint: disable=attribute-defined-outside-init
self.axialRapid = 360 / safeCircum * self.horizRapid # pylint: disable=attribute-defined-outside-init
# Initiate depthparams and calculate operation heights for rotational operation
# Initiate depthparams and calculate operation heights for operation
self.depthparams = self._customDepthParams(obj, obj.StartDepth.Value, obj.FinalDepth.Value)
# Set start point
@@ -391,13 +315,13 @@ class ObjectOp(PathOp.ObjectOp):
aOS = self.areaOpShapes(obj) # pylint: disable=assignment-from-no-return
# Adjust tuples length received from other PathWB tools/operations beside PathPocketShape
# Adjust tuples length received from other PathWB tools/operations
shapes = []
for shp in aOS:
if len(shp) == 2:
(fc, iH) = shp
# fc, iH, sub, angle, axis, strtDep, finDep
tup = fc, iH, 'otherOp', 0.0, 'S', start_depth, final_depth
# fc, iH, sub or description
tup = fc, iH, 'otherOp'
shapes.append(tup)
else:
shapes.append(shp)
@@ -420,23 +344,15 @@ class ObjectOp(PathOp.ObjectOp):
shapes = [j['shape'] for j in jobs]
sims = []
numShapes = len(shapes)
for ns in range(0, numShapes):
for shape, isHole, sub in shapes:
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:
nextAxis = 'L'
self.depthparams = self._customDepthParams(obj, strDep, finDep)
try:
if profileEdgesIsOpen:
(pp, sim) = self._buildProfileOpenEdges(obj, shape, isHole, start, getsim)
@@ -450,28 +366,6 @@ class ObjectOp(PathOp.ObjectOp):
ppCmds = pp
else:
ppCmds = pp.Commands
if obj.EnableRotation != 'Off' and self.rotateFlag is True:
# Rotate model to index for cut
if axis == 'X':
axisOfRot = 'A'
elif axis == 'Y':
axisOfRot = 'B'
elif axis == 'Z':
axisOfRot = 'C'
else:
axisOfRot = 'A'
# Rotate Model to correct angle
ppCmds.insert(0, Path.Command('G0', {axisOfRot: angle, 'F': self.axialRapid}))
# Raise cutter to safe height
ppCmds.insert(0, Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
# Return index to starting position if axis of rotation changes.
if numShapes > 1:
if ns != numShapes - 1:
if axis != nextAxis:
ppCmds.append(Path.Command('G0', {axisOfRot: 0.0, 'F': self.axialRapid}))
# Eif
# Save gcode commands to object command list
self.commandlist.extend(ppCmds)
@@ -482,51 +376,6 @@ class ObjectOp(PathOp.ObjectOp):
self.endVector[2] = obj.ClearanceHeight.Value
self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
# Raise cutter to safe height and rotate back to original orientation
# based on next rotational operation in job
if self.rotateFlag is True:
resetAxis = False
lastJobOp = None
nextJobOp = None
opIdx = 0
JOB = PathUtils.findParentJob(obj)
jobOps = JOB.Operations.Group
numJobOps = len(jobOps)
for joi in range(0, numJobOps):
jo = jobOps[joi]
if jo.Name == obj.Name:
opIdx = joi
lastOpIdx = opIdx - 1
nextOpIdx = opIdx + 1
if lastOpIdx > -1:
lastJobOp = jobOps[lastOpIdx]
if nextOpIdx < numJobOps:
nextJobOp = jobOps[nextOpIdx]
if lastJobOp is not None:
if hasattr(lastJobOp, 'EnableRotation'):
PathLog.debug('Last Op, {}, has `EnableRotation` set to {}'.format(lastJobOp.Label, lastJobOp.EnableRotation))
if lastJobOp.EnableRotation != obj.EnableRotation:
resetAxis = True
# if ns == numShapes - 1: # If last shape, check next op EnableRotation setting
if nextJobOp is not None:
if hasattr(nextJobOp, 'EnableRotation'):
PathLog.debug('Next Op, {}, has `EnableRotation` set to {}'.format(nextJobOp.Label, nextJobOp.EnableRotation))
if nextJobOp.EnableRotation != obj.EnableRotation:
resetAxis = True
# Raise to safe height if rotation activated
self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
# reset rotational axes if necessary
if resetAxis is True:
self.commandlist.append(Path.Command('G0', {'A': 0.0, 'F': self.axialRapid}))
self.commandlist.append(Path.Command('G0', {'B': 0.0, 'F': self.axialRapid}))
self.useTempJobClones('Delete') # Delete temp job clone group and contents
self.guiMessage('title', None, show=True) # Process GUI messages to user
for ton in self.tempObjectNames: # remove temporary objects by name
FreeCAD.ActiveDocument.removeObject(ton)
PathLog.debug("obj.Name: " + str(obj.Name) + "\n\n")
return sims
@@ -561,461 +410,7 @@ class ObjectOp(PathOp.ObjectOp):
# pylint: disable=unused-argument
return False
# Rotation-related methods
def opDetermineRotationRadii(self, obj):
'''opDetermineRotationRadii(obj)
Determine rotational radii for 4th-axis rotations, for clearance/safe heights '''
parentJob = PathUtils.findParentJob(obj)
xlim = 0.0
ylim = 0.0
# Determine boundbox radius based upon xzy limits data
if math.fabs(self.stockBB.ZMin) > math.fabs(self.stockBB.ZMax):
zlim = self.stockBB.ZMin
else:
zlim = self.stockBB.ZMax
if obj.EnableRotation != 'B(y)':
# Rotation is around X-axis, cutter moves along same axis
if math.fabs(self.stockBB.YMin) > math.fabs(self.stockBB.YMax):
ylim = self.stockBB.YMin
else:
ylim = self.stockBB.YMax
if obj.EnableRotation != 'A(x)':
# Rotation is around Y-axis, cutter moves along same axis
if math.fabs(self.stockBB.XMin) > math.fabs(self.stockBB.XMax):
xlim = self.stockBB.XMin
else:
xlim = self.stockBB.XMax
xRotRad = math.sqrt(ylim**2 + zlim**2)
yRotRad = math.sqrt(xlim**2 + zlim**2)
zRotRad = math.sqrt(xlim**2 + ylim**2)
clrOfst = parentJob.SetupSheet.ClearanceHeightOffset.Value
safOfst = parentJob.SetupSheet.SafeHeightOffset.Value
return [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfst)]
def faceRotationAnalysis(self, obj, norm, surf):
'''faceRotationAnalysis(obj, norm, surf)
Determine X and Y independent rotation necessary to make normalAt = Z=1 (0,0,1) '''
PathLog.track()
praInfo = "faceRotationAnalysis()"
rtn = True
orientation = 'X'
angle = 500.0
precision = 6
for i in range(0, 13):
if PathGeom.Tolerance * (i * 10) == 1.0:
precision = i
break
def roundRoughValues(precision, val):
# Convert VALxe-15 numbers to zero
if PathGeom.isRoughly(0.0, val) is True:
return 0.0
# Convert VAL.99999999 to next integer
elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance:
return round(val)
else:
return round(val, precision)
nX = roundRoughValues(precision, norm.x)
nY = roundRoughValues(precision, norm.y)
nZ = roundRoughValues(precision, norm.z)
praInfo += "\n -normalAt(0,0): " + str(nX) + ", " + str(nY) + ", " + str(nZ)
saX = roundRoughValues(precision, surf.x)
saY = roundRoughValues(precision, surf.y)
saZ = roundRoughValues(precision, surf.z)
praInfo += "\n -Surface.Axis: " + str(saX) + ", " + str(saY) + ", " + str(saZ)
# Determine rotation needed and current orientation
if saX == 0.0:
if saY == 0.0:
orientation = "Z"
if saZ == 1.0:
angle = 0.0
elif saZ == -1.0:
angle = -180.0
else:
praInfo += "_else_X" + str(saZ)
elif saY == 1.0:
orientation = "Y"
angle = 90.0
elif saY == -1.0:
orientation = "Y"
angle = -90.0
else:
if saZ != 0.0:
angle = math.degrees(math.atan(saY / saZ))
orientation = "Y"
elif saY == 0.0:
if saZ == 0.0:
orientation = "X"
if saX == 1.0:
angle = -90.0
elif saX == -1.0:
angle = 90.0
else:
praInfo += "_else_X" + str(saX)
else:
orientation = "X"
ratio = saX / saZ
angle = math.degrees(math.atan(ratio))
if ratio < 0.0:
praInfo += " NEG-ratio"
# angle -= 90
else:
praInfo += " POS-ratio"
angle = -1 * angle
if saX < 0.0:
angle = angle + 180.0
elif saZ == 0.0:
# if saY != 0.0:
angle = math.degrees(math.atan(saX / saY))
orientation = "Y"
if saX + nX == 0.0:
angle = -1 * angle
if saY + nY == 0.0:
angle = -1 * angle
if saZ + nZ == 0.0:
angle = -1 * angle
if saY == -1.0 or saY == 1.0:
if nX != 0.0:
angle = -1 * angle
# Enforce enabled rotation in settings
praInfo += "\n -Initial orientation: {}".format(orientation)
if orientation == 'Y':
axis = 'X'
if obj.EnableRotation == 'B(y)': # Required axis disabled
if angle == 180.0 or angle == -180.0:
axis = 'Y'
else:
rtn = False
elif orientation == 'X':
axis = 'Y'
if obj.EnableRotation == 'A(x)': # Required axis disabled
if angle == 180.0 or angle == -180.0:
axis = 'X'
else:
rtn = False
elif orientation == 'Z':
axis = 'X'
if math.fabs(angle) == 0.0:
angle = 0.0
rtn = False
if angle == 500.0:
angle = 0.0
rtn = False
if rtn is False:
if orientation == 'Z' and angle == 0.0 and obj.ReverseDirection is True:
if obj.EnableRotation == 'B(y)':
axis = 'Y'
rtn = True
if rtn is True:
self.rotateFlag = True # pylint: disable=attribute-defined-outside-init
if obj.ReverseDirection is True:
if angle < 180.0:
angle = angle + 180.0
else:
angle = angle - 180.0
angle = round(angle, precision)
praInfo += "\n -Rotation analysis: angle: " + str(angle) + ", axis: " + str(axis)
if rtn is True:
praInfo += "\n - ... rotation triggered"
else:
praInfo += "\n - ... NO rotation triggered"
return (rtn, angle, axis, praInfo)
def guiMessage(self, title, msg, show=False):
'''guiMessage(title, msg, show=False)
Handle op related GUI messages to user'''
if msg is not None:
self.guiMsgs.append((title, msg))
if show is True:
if len(self.guiMsgs) > 0:
if FreeCAD.GuiUp:
from PySide.QtGui import QMessageBox
for entry in self.guiMsgs:
(title, msg) = entry
QMessageBox.warning(None, title, msg)
self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init
return True
else:
for entry in self.guiMsgs:
(title, msg) = entry
PathLog.warning("{}:: {}".format(title, msg))
self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init
return True
return False
def visualAxis(self):
'''visualAxis()
Create visual X & Y axis for use in orientation of rotational operations
Triggered only for PathLog.debug'''
if not FreeCAD.ActiveDocument.getObject('xAxCyl'):
xAx = 'xAxCyl'
yAx = 'yAxCyl'
# zAx = 'zAxCyl'
VA = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "visualAxis")
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.getObject('visualAxis').Visibility = False
vaGrp = FreeCAD.ActiveDocument.getObject("visualAxis")
FreeCAD.ActiveDocument.addObject("Part::Cylinder", xAx)
cyl = FreeCAD.ActiveDocument.getObject(xAx)
cyl.Label = xAx
cyl.Radius = self.xRotRad
cyl.Height = 0.01
cyl.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), 90))
cyl.purgeTouched()
if FreeCAD.GuiUp:
cylGui = FreeCADGui.ActiveDocument.getObject(xAx)
cylGui.ShapeColor = (0.667, 0.000, 0.000)
cylGui.Transparency = 85
cylGui.Visibility = False
vaGrp.addObject(cyl)
FreeCAD.ActiveDocument.addObject("Part::Cylinder", yAx)
cyl = FreeCAD.ActiveDocument.getObject(yAx)
cyl.Label = yAx
cyl.Radius = self.yRotRad
cyl.Height = 0.01
cyl.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), 90))
cyl.purgeTouched()
if FreeCAD.GuiUp:
cylGui = FreeCADGui.ActiveDocument.getObject(yAx)
cylGui.ShapeColor = (0.000, 0.667, 0.000)
cylGui.Transparency = 85
cylGui.Visibility = False
vaGrp.addObject(cyl)
VA.purgeTouched()
def useTempJobClones(self, cloneName):
'''useTempJobClones(cloneName)
Manage use of temporary model clones for rotational operation calculations.
Clones are stored in 'rotJobClones' group.'''
if FreeCAD.ActiveDocument.getObject('rotJobClones'):
if cloneName == 'Start':
if PathLog.getLevel(PathLog.thisModule()) < 4:
for cln in FreeCAD.ActiveDocument.getObject('rotJobClones').Group:
FreeCAD.ActiveDocument.removeObject(cln.Name)
elif cloneName == 'Delete':
if PathLog.getLevel(PathLog.thisModule()) < 4:
for cln in FreeCAD.ActiveDocument.getObject('rotJobClones').Group:
FreeCAD.ActiveDocument.removeObject(cln.Name)
FreeCAD.ActiveDocument.removeObject('rotJobClones')
else:
FreeCAD.ActiveDocument.getObject('rotJobClones').purgeTouched()
else:
FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "rotJobClones")
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.getObject('rotJobClones').Visibility = False
if cloneName != 'Start' and cloneName != 'Delete':
FreeCAD.ActiveDocument.getObject('rotJobClones').addObject(FreeCAD.ActiveDocument.getObject(cloneName))
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.getObject(cloneName).Visibility = False
def cloneBaseAndStock(self, obj, base, angle, axis, subCount):
'''cloneBaseAndStock(obj, base, angle, axis, subCount)
Method called to create a temporary clone of the base and parent Job stock.
Clones are destroyed after usage for calculations related to rotational operations.'''
# Create a temporary clone and stock of model for rotational use.
rndAng = round(angle, 8)
if rndAng < 0.0: # neg sign is converted to underscore in clone name creation.
tag = axis + '_' + axis + '_' + str(math.fabs(rndAng)).replace('.', '_')
else:
tag = axis + str(rndAng).replace('.', '_')
clnNm = obj.Name + '_base_' + '_' + str(subCount) + '_' + tag
stckClnNm = obj.Name + '_stock_' + '_' + str(subCount) + '_' + tag
if clnNm not in self.cloneNames:
self.cloneNames.append(clnNm)
self.cloneNames.append(stckClnNm)
if FreeCAD.ActiveDocument.getObject(clnNm):
FreeCAD.ActiveDocument.getObject(clnNm).Shape = base.Shape
else:
FreeCAD.ActiveDocument.addObject('Part::Feature', clnNm).Shape = base.Shape
self.useTempJobClones(clnNm)
if FreeCAD.ActiveDocument.getObject(stckClnNm):
FreeCAD.ActiveDocument.getObject(stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape
else:
FreeCAD.ActiveDocument.addObject('Part::Feature', stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape
self.useTempJobClones(stckClnNm)
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.getObject(stckClnNm).Transparency = 90
FreeCADGui.ActiveDocument.getObject(clnNm).ShapeColor = (1.000, 0.667, 0.000)
clnBase = FreeCAD.ActiveDocument.getObject(clnNm)
clnStock = FreeCAD.ActiveDocument.getObject(stckClnNm)
tag = base.Name + '_' + tag
return (clnBase, clnStock, tag)
def getFaceNormAndSurf(self, face):
'''getFaceNormAndSurf(face)
Return face.normalAt(0,0) or face.normal(0,0) and face.Surface.Axis vectors
'''
norm = FreeCAD.Vector(0.0, 0.0, 0.0)
surf = FreeCAD.Vector(0.0, 0.0, 0.0)
if hasattr(face, 'normalAt'):
n = face.normalAt(0, 0)
elif hasattr(face, 'normal'):
n = face.normal(0, 0)
if hasattr(face.Surface, 'Axis'):
s = face.Surface.Axis
else:
s = n
norm.x = n.x
norm.y = n.y
norm.z = n.z
surf.x = s.x
surf.y = s.y
surf.z = s.z
return (norm, surf)
def applyRotationalAnalysis(self, obj, base, angle, axis, subCount):
'''applyRotationalAnalysis(obj, base, angle, axis, subCount)
Create temp clone and stock and apply rotation to both.
Return new rotated clones
'''
if axis == 'X':
vect = FreeCAD.Vector(1, 0, 0)
elif axis == 'Y':
vect = FreeCAD.Vector(0, 1, 0)
# Commented out to fix PocketShape InverseAngle rotation problem
# if obj.InverseAngle is True:
# angle = -1 * angle
# if math.fabs(angle) == 0.0:
# angle = 0.0
# Create a temporary clone of model for rotational use.
(clnBase, clnStock, tag) = self.cloneBaseAndStock(obj, base, angle, axis, subCount)
# Rotate base to such that Surface.Axis of pocket bottom is Z=1
clnBase = Draft.rotate(clnBase, angle, center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False)
clnStock = Draft.rotate(clnStock, angle, center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False)
clnBase.purgeTouched()
clnStock.purgeTouched()
return (clnBase, angle, clnStock, tag)
def applyInverseAngle(self, obj, clnBase, clnStock, axis, angle):
'''applyInverseAngle(obj, clnBase, clnStock, axis, angle)
Apply rotations to incoming base and stock objects.'''
if axis == 'X':
vect = FreeCAD.Vector(1, 0, 0)
elif axis == 'Y':
vect = FreeCAD.Vector(0, 1, 0)
# Rotate base to inverse of original angle
clnBase = Draft.rotate(clnBase, (-2 * angle), center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False)
clnStock = Draft.rotate(clnStock, (-2 * angle), center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False)
clnBase.purgeTouched()
clnStock.purgeTouched()
# Update property and angle values
obj.InverseAngle = True
# obj.AttemptInverseAngle = False
angle = -1 * angle
PathLog.debug(translate("Path", "Rotated to inverse angle."))
return (clnBase, clnStock, angle)
def sortTuplesByIndex(self, TupleList, tagIdx):
'''sortTuplesByIndex(TupleList, tagIdx)
sort list of tuples based on tag index provided
return (TagList, GroupList)
'''
# Separate elements, regroup by orientation (axis_angle combination)
TagList = ['X34.2']
GroupList = [[(2.3, 3.4, 'X')]]
for tup in TupleList:
if tup[tagIdx] in TagList:
# Determine index of found string
i = 0
for orn in TagList:
if orn == tup[4]:
break
i += 1
GroupList[i].append(tup)
else:
TagList.append(tup[4]) # add orientation entry
GroupList.append([tup]) # add orientation entry
# Remove temp elements
TagList.pop(0)
GroupList.pop(0)
return (TagList, GroupList)
def warnDisabledAxis(self, obj, axis, sub=''):
'''warnDisabledAxis(self, obj, axis)
Provide user feedback if required axis is disabled'''
if axis == 'X' and obj.EnableRotation == 'B(y)':
msg = translate('Path', "{}:: {} is inaccessible.".format(obj.Name, sub)) + " "
msg += translate('Path', "Selected feature(s) require 'Enable Rotation: A(x)' for access.")
PathLog.warning(msg)
return True
elif axis == 'Y' and obj.EnableRotation == 'A(x)':
msg = translate('Path', "{}:: {} is inaccessible.".format(obj.Name, sub)) + " "
msg += translate('Path', "Selected feature(s) require 'Enable Rotation: B(y)' for access.")
PathLog.warning(msg)
return True
else:
return False
def isFaceUp(self, base, face):
'''isFaceUp(base, face) ...
When passed a base object and face shape, returns True if face is up.
This method is used to identify correct rotation of a model.
'''
# verify face is normal to Z+-
(norm, surf) = self.getFaceNormAndSurf(face)
if round(abs(norm.z), 8) != 1.0 or round(abs(surf.z), 8) != 1.0:
PathLog.debug('isFaceUp - face not oriented normal to Z+-')
return False
up = face.extrude(FreeCAD.Vector(0.0, 0.0, 5.0))
dwn = face.extrude(FreeCAD.Vector(0.0, 0.0, -5.0))
upCmn = base.Shape.common(up)
dwnCmn = base.Shape.common(dwn)
# Identify orientation based on volumes of common() results
if len(upCmn.Edges) > 0:
PathLog.debug('isFaceUp - HAS up edges\n')
if len(dwnCmn.Edges) > 0:
PathLog.debug('isFaceUp - up and dwn edges\n')
dVol = round(dwnCmn.Volume, 6)
uVol = round(upCmn.Volume, 6)
if uVol > dVol:
return False
return True
else:
if round(upCmn.Volume, 6) == 0.0:
return True
return False
elif len(dwnCmn.Edges) > 0:
PathLog.debug('isFaceUp - HAS dwn edges only\n')
dVol = round(dwnCmn.Volume, 6)
if dVol == 0.0:
return False
return True
PathLog.debug('isFaceUp - exit True\n')
return True
# Support methods
def _customDepthParams(self, obj, strDep, finDep):
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
cdp = PathUtils.depth_params(
@@ -1030,5 +425,5 @@ class ObjectOp(PathOp.ObjectOp):
# Eclass
def SetupProperties():
setup = ['EnableRotation']
setup = []
return setup

View File

@@ -26,7 +26,6 @@ import PathScripts.PathOp as PathOp
import PathScripts.PathUtils as PathUtils
from PySide import QtCore
import PathScripts.PathGeom as PathGeom
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
@@ -35,15 +34,11 @@ Draft = LazyLoader('Draft', globals(), 'Draft')
Part = LazyLoader('Part', globals(), 'Part')
DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils')
import math
if FreeCAD.GuiUp:
import FreeCADGui
__title__ = "Path Circular Holes Base Operation"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Base class an implementation for operations on circular holes."
__contributors__ = "russ4262 (Russell Johnson)"
# Qt translation handling
@@ -57,39 +52,32 @@ PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
class ObjectOp(PathOp.ObjectOp):
'''Base class for proxy objects of all operations on circular holes.'''
# These are static while document is open, if it contains a CircularHole Op
initOpFinalDepth = None
initOpStartDepth = None
initWithRotation = False
defValsSet = False
docRestored = False
def opFeatures(self, obj):
'''opFeatures(obj) ... calls circularHoleFeatures(obj) and ORs in the standard features required for processing circular holes.
Do not overwrite, implement circularHoleFeatures(obj) instead'''
return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureBaseFaces | self.circularHoleFeatures(obj) | PathOp.FeatureCoolant
return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights \
| PathOp.FeatureBaseFaces | self.circularHoleFeatures(obj) \
| PathOp.FeatureCoolant
def circularHoleFeatures(self, obj):
'''circularHoleFeatures(obj) ... overwrite to add operations specific features.
Can safely be overwritten by subclasses.'''
# pylint: disable=unused-argument
return 0
def initOperation(self, obj):
'''initOperation(obj) ... adds Disabled properties and calls initCircularHoleOperation(obj).
Do not overwrite, implement initCircularHoleOperation(obj) instead.'''
obj.addProperty("App::PropertyStringList", "Disabled", "Base", QtCore.QT_TRANSLATE_NOOP("Path", "List of disabled features"))
self.initCircularHoleOperation(obj)
def initCircularHoleOperation(self, obj):
'''initCircularHoleOperation(obj) ... overwrite if the subclass needs initialisation.
Can safely be overwritten by subclasses.'''
pass # pylint: disable=unnecessary-pass
pass
def baseIsArchPanel(self, obj, base):
'''baseIsArchPanel(obj, base) ... return true if op deals with an Arch.Panel.'''
# pylint: disable=unused-argument
return hasattr(base, "Proxy") and isinstance(base.Proxy, ArchPanel.PanelSheet)
def getArchPanelEdge(self, obj, base, sub):
@@ -101,7 +89,6 @@ class ObjectOp(PathOp.ObjectOp):
Obviously this is as fragile as can be, but currently the best we can do while the panel sheets
hide the actual features from Path and they can't be referenced directly.
'''
# pylint: disable=unused-argument
ids = sub.split(".")
holeId = int(ids[0])
wireId = int(ids[1])
@@ -136,7 +123,8 @@ class ObjectOp(PathOp.ObjectOp):
shape.Edges[i].Curve.Radius * 2 > shape.BoundBox.XLength*0.9):
return shape.Edges[i].Curve.Radius * 2
# for all other shapes the diameter is just the dimension in X. This may be inaccurate as the BoundBox is calculated on the tessellated geometry
# for all other shapes the diameter is just the dimension in X.
# This may be inaccurate as the BoundBox is calculated on the tessellated geometry
PathLog.warning(translate("Path", "Hole diameter may be inaccurate due to tessellation on face. Consider selecting hole edge."))
return shape.BoundBox.XLength
except Part.OCCError as e:
@@ -185,143 +173,34 @@ class ObjectOp(PathOp.ObjectOp):
Do not overwrite, implement circularHoleExecute(obj, holes) instead.'''
PathLog.track()
holes = []
baseSubsTuples = []
subCount = 0
allTuples = []
self.cloneNames = [] # pylint: disable=attribute-defined-outside-init
self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init
self.rotateFlag = False # pylint: disable=attribute-defined-outside-init
self.useTempJobClones('Delete') # pylint: disable=attribute-defined-outside-init
self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init
self.clearHeight = obj.ClearanceHeight.Value # pylint: disable=attribute-defined-outside-init
self.safeHeight = obj.SafeHeight.Value # pylint: disable=attribute-defined-outside-init
self.axialFeed = 0.0 # pylint: disable=attribute-defined-outside-init
self.axialRapid = 0.0 # pylint: disable=attribute-defined-outside-init
def haveLocations(self, obj):
if PathOp.FeatureLocations & self.opFeatures(obj):
return len(obj.Locations) != 0
return False
if obj.EnableRotation == 'Off':
strDep = obj.StartDepth.Value
finDep = obj.FinalDepth.Value
else:
# Calculate operation heights based upon rotation radii
opHeights = self.opDetermineRotationRadii(obj)
(self.xRotRad, self.yRotRad, self.zRotRad) = opHeights[0] # pylint: disable=attribute-defined-outside-init
(clrOfset, safOfst) = opHeights[1]
PathLog.debug("Exec. opHeights[0]: " + str(opHeights[0]))
PathLog.debug("Exec. opHeights[1]: " + str(opHeights[1]))
# Set clearance and safe heights based upon rotation radii
if obj.EnableRotation == 'A(x)':
strDep = self.xRotRad
elif obj.EnableRotation == 'B(y)':
strDep = self.yRotRad
else:
strDep = max(self.xRotRad, self.yRotRad)
finDep = -1 * strDep
obj.ClearanceHeight.Value = strDep + clrOfset
obj.SafeHeight.Value = strDep + safOfst
# Create visual axes when debugging.
if PathLog.getLevel(PathLog.thisModule()) == 4:
self.visualAxis()
# Set axial feed rates based upon horizontal feed rates
safeCircum = 2 * math.pi * obj.SafeHeight.Value
self.axialFeed = 360 / safeCircum * self.horizFeed # pylint: disable=attribute-defined-outside-init
self.axialRapid = 360 / safeCircum * self.horizRapid # pylint: disable=attribute-defined-outside-init
# Complete rotational analysis and temp clone creation as needed
if obj.EnableRotation == 'Off':
PathLog.debug("Enable Rotation setting is 'Off' for {}.".format(obj.Name))
stock = PathUtils.findParentJob(obj).Stock
for (base, subList) in obj.Base:
baseSubsTuples.append((base, subList, 0.0, 'A', stock))
else:
for p in range(0, len(obj.Base)):
(bst, at) = self.process_base_geometry_with_rotation(obj, p, subCount)
allTuples.extend(at)
baseSubsTuples.extend(bst)
for base, subs, angle, axis, stock in baseSubsTuples:
# rotate shorter angle in opposite direction
if angle > 180:
angle -= 360
elif angle < -180:
angle += 360
# Re-analyze rotated model for drillable holes
if obj.EnableRotation != 'Off':
rotated_features = self.findHoles(obj, base)
holes = []
for base, subs in obj.Base:
for sub in subs:
PathLog.debug('sub, angle, axis: {}, {}, {}'.format(sub, angle, axis))
PathLog.debug('processing {} in {}'.format(sub, base.Name))
if self.isHoleEnabled(obj, base, sub):
pos = self.holePosition(obj, base, sub)
if pos:
# Identify face to which edge belongs
sub_shape = base.Shape.getElement(sub)
holes.append({'x': pos.x, 'y': pos.y, 'r': self.holeDiameter(obj, base, sub)})
# Default is to treat selection as 'Face' shape
holeBtm = sub_shape.BoundBox.ZMin
if obj.EnableRotation != 'Off':
# Update Start and Final depths due to rotation, if auto defaults are active
parent_face = self._find_parent_face_of_edge(rotated_features, sub_shape)
if parent_face:
PathLog.debug('parent_face found')
holeBtm = parent_face.BoundBox.ZMin
if obj.OpStartDepth == obj.StartDepth:
obj.StartDepth.Value = parent_face.BoundBox.ZMax
PathLog.debug('new StartDepth: {}'.format(obj.StartDepth.Value))
if obj.OpFinalDepth == obj.FinalDepth:
obj.FinalDepth.Value = holeBtm
PathLog.debug('new FinalDepth: {}'.format(holeBtm))
else:
PathLog.debug('NO parent_face identified')
if base.Shape.getElement(sub).ShapeType == 'Edge':
msg = translate("Path", "Verify Final Depth of holes based on edges. {} depth is: {} mm".format(sub, round(holeBtm, 4))) + " "
msg += translate("Path", "Always select the bottom edge of the hole when using an edge.")
PathLog.warning(msg)
# Warn user if Final Depth set lower than bottom of hole
if obj.FinalDepth.Value < holeBtm:
msg = translate("Path", "Final Depth setting is below the hole bottom for {}.".format(sub)) + ' '
msg += translate("Path", "{} depth is calculated at {} mm".format(sub, round(holeBtm, 4)))
PathLog.warning(msg)
holes.append({'x': pos.x, 'y': pos.y, 'r': self.holeDiameter(obj, base, sub),
'angle': angle, 'axis': axis, 'trgtDep': obj.FinalDepth.Value,
'stkTop': stock.Shape.BoundBox.ZMax})
# haveLocations are populated from user-provided (x, y) coordinates
# provided by the user in the Base Locations tab of the Task Editor window
if haveLocations(self, obj):
for location in obj.Locations:
# holes.append({'x': location.x, 'y': location.y, 'r': 0, 'angle': 0.0, 'axis': 'X', 'holeBtm': obj.FinalDepth.Value})
holes.append({'x': location.x, 'y': location.y, 'r': 0,
'angle': 0.0, 'axis': 'X', 'trgtDep': obj.FinalDepth.Value,
'stkTop': PathUtils.findParentJob(obj).Stock.Shape.BoundBox.ZMax})
holes.append({'x': location.x, 'y': location.y, 'r': 0})
if len(holes) > 0:
self.circularHoleExecute(obj, holes) # circularHoleExecute() located in PathDrilling.py
self.useTempJobClones('Delete') # Delete temp job clone group and contents
self.guiMessage('title', None, show=True) # Process GUI messages to user
PathLog.debug("obj.Name: " + str(obj.Name))
self.circularHoleExecute(obj, holes)
def circularHoleExecute(self, obj, holes):
'''circularHoleExecute(obj, holes) ... implement processing of holes.
holes is a list of dictionaries with 'x', 'y' and 'r' specified for each hole.
Note that for Vertexes, non-circular Edges and Locations r=0.
Must be overwritten by subclasses.'''
pass # pylint: disable=unnecessary-pass
pass
def findAllHoles(self, obj):
'''findAllHoles(obj) ... find all holes of all base models and assign as features.'''
@@ -390,615 +269,4 @@ class ObjectOp(PathOp.ObjectOp):
PathLog.debug("Found hole feature %s.%s" % (baseobject.Label, candidateFaceName))
PathLog.debug("holes found: {}".format(holelist))
return features
# Rotation-related methods
def opDetermineRotationRadii(self, obj):
'''opDetermineRotationRadii(obj)
Determine rotational radii for 4th-axis rotations, for clearance/safe heights '''
parentJob = PathUtils.findParentJob(obj)
xlim = 0.0
ylim = 0.0
# Determine boundbox radius based upon xzy limits data
if math.fabs(self.stockBB.ZMin) > math.fabs(self.stockBB.ZMax):
zlim = self.stockBB.ZMin
else:
zlim = self.stockBB.ZMax
if obj.EnableRotation != 'B(y)':
# Rotation is around X-axis, cutter moves along same axis
if math.fabs(self.stockBB.YMin) > math.fabs(self.stockBB.YMax):
ylim = self.stockBB.YMin
else:
ylim = self.stockBB.YMax
if obj.EnableRotation != 'A(x)':
# Rotation is around Y-axis, cutter moves along same axis
if math.fabs(self.stockBB.XMin) > math.fabs(self.stockBB.XMax):
xlim = self.stockBB.XMin
else:
xlim = self.stockBB.XMax
xRotRad = math.sqrt(ylim**2 + zlim**2)
yRotRad = math.sqrt(xlim**2 + zlim**2)
zRotRad = math.sqrt(xlim**2 + ylim**2)
clrOfst = parentJob.SetupSheet.ClearanceHeightOffset.Value
safOfst = parentJob.SetupSheet.SafeHeightOffset.Value
return [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfst)]
def faceRotationAnalysis(self, obj, norm, surf):
'''faceRotationAnalysis(obj, norm, surf)
Determine X and Y independent rotation necessary to make normalAt = Z=1 (0,0,1) '''
PathLog.track()
praInfo = "faceRotationAnalysis(): "
rtn = True
orientation = 'X'
angle = 500.0
precision = 6
for i in range(0, 13):
if PathGeom.Tolerance * (i * 10) == 1.0:
precision = i
break
def roundRoughValues(precision, val):
# Convert VALxe-15 numbers to zero
if PathGeom.isRoughly(0.0, val) is True:
return 0.0
# Convert VAL.99999999 to next integer
elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance:
return round(val)
else:
return round(val, precision)
nX = roundRoughValues(precision, norm.x)
nY = roundRoughValues(precision, norm.y)
nZ = roundRoughValues(precision, norm.z)
praInfo += "\n -normalAt(0,0): " + str(nX) + ", " + str(nY) + ", " + str(nZ)
saX = roundRoughValues(precision, surf.x)
saY = roundRoughValues(precision, surf.y)
saZ = roundRoughValues(precision, surf.z)
praInfo += "\n -Surface.Axis: " + str(saX) + ", " + str(saY) + ", " + str(saZ)
# Determine rotation needed and current orientation
if saX == 0.0:
if saY == 0.0:
orientation = "Z"
if saZ == 1.0:
angle = 0.0
elif saZ == -1.0:
angle = -180.0
else:
praInfo += "_else_X" + str(saZ)
elif saY == 1.0:
orientation = "Y"
angle = 90.0
elif saY == -1.0:
orientation = "Y"
angle = -90.0
else:
if saZ != 0.0:
angle = math.degrees(math.atan(saY / saZ))
orientation = "Y"
elif saY == 0.0:
if saZ == 0.0:
orientation = "X"
if saX == 1.0:
angle = -90.0
elif saX == -1.0:
angle = 90.0
else:
praInfo += "_else_X" + str(saX)
else:
orientation = "X"
ratio = saX / saZ
angle = math.degrees(math.atan(ratio))
if ratio < 0.0:
praInfo += " NEG-ratio"
# angle -= 90
else:
praInfo += " POS-ratio"
angle = -1 * angle
if saX < 0.0:
angle = angle + 180.0
elif saZ == 0.0:
# if saY != 0.0:
angle = math.degrees(math.atan(saX / saY))
orientation = "Y"
if saX + nX == 0.0:
angle = -1 * angle
if saY + nY == 0.0:
angle = -1 * angle
if saZ + nZ == 0.0:
angle = -1 * angle
if saY == -1.0 or saY == 1.0:
if nX != 0.0:
angle = -1 * angle
# Enforce enabled rotation in settings
praInfo += "\n -Initial orientation: {}".format(orientation)
if orientation == 'Y':
axis = 'X'
if obj.EnableRotation == 'B(y)': # Required axis disabled
if angle == 180.0 or angle == -180.0:
axis = 'Y'
else:
rtn = False
elif orientation == 'X':
axis = 'Y'
if obj.EnableRotation == 'A(x)': # Required axis disabled
if angle == 180.0 or angle == -180.0:
axis = 'X'
else:
rtn = False
elif orientation == 'Z':
axis = 'X'
if math.fabs(angle) == 0.0:
angle = 0.0
rtn = False
if angle == 500.0:
angle = 0.0
rtn = False
if rtn is False:
if orientation == 'Z' and angle == 0.0 and obj.ReverseDirection is True:
if obj.EnableRotation == 'B(y)':
axis = 'Y'
rtn = True
if rtn:
self.rotateFlag = True # pylint: disable=attribute-defined-outside-init
if obj.ReverseDirection is True:
if angle < 180.0:
angle = angle + 180.0
else:
angle = angle - 180.0
angle = round(angle, precision)
praInfo += "\n -Rotation analysis: angle: " + str(angle) + ", axis: " + str(axis)
if rtn is True:
praInfo += "\n - ... rotation triggered"
else:
praInfo += "\n - ... NO rotation triggered"
return (rtn, angle, axis, praInfo)
def guiMessage(self, title, msg, show=False):
'''guiMessage(title, msg, show=False)
Handle op related GUI messages to user'''
if msg is not None:
self.guiMsgs.append((title, msg))
if show is True:
if len(self.guiMsgs) > 0:
if FreeCAD.GuiUp:
from PySide.QtGui import QMessageBox
for entry in self.guiMsgs:
(title, msg) = entry
QMessageBox.warning(None, title, msg)
self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init
return True
else:
for entry in self.guiMsgs:
(title, msg) = entry
PathLog.warning("{}:: {}".format(title, msg))
self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init
return True
return False
def visualAxis(self):
'''visualAxis()
Create visual X & Y axis for use in orientation of rotational operations
Triggered only for PathLog.debug'''
fcad = FreeCAD.ActiveDocument
if not fcad.getObject('xAxCyl'):
xAx = 'xAxCyl'
yAx = 'yAxCyl'
# zAx = 'zAxCyl'
visual_axis_obj = fcad.addObject("App::DocumentObjectGroup", "visualAxis")
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.getObject('visualAxis').Visibility = False
vaGrp = fcad.getObject("visualAxis")
fcad.addObject("Part::Cylinder", xAx)
cyl = fcad.getObject(xAx)
cyl.Label = xAx
cyl.Radius = self.xRotRad
cyl.Height = 0.01
cyl.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), 90))
cyl.purgeTouched()
if FreeCAD.GuiUp:
cylGui = FreeCADGui.ActiveDocument.getObject(xAx)
cylGui.ShapeColor = (0.667, 0.000, 0.000)
cylGui.Transparency = 85
cylGui.Visibility = False
vaGrp.addObject(cyl)
fcad.addObject("Part::Cylinder", yAx)
cyl = fcad.getObject(yAx)
cyl.Label = yAx
cyl.Radius = self.yRotRad
cyl.Height = 0.01
cyl.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), 90))
cyl.purgeTouched()
if FreeCAD.GuiUp:
cylGui = FreeCADGui.ActiveDocument.getObject(yAx)
cylGui.ShapeColor = (0.000, 0.667, 0.000)
cylGui.Transparency = 85
cylGui.Visibility = False
vaGrp.addObject(cyl)
visual_axis_obj.purgeTouched()
def useTempJobClones(self, cloneName):
'''useTempJobClones(cloneName)
Manage use of temporary model clones for rotational operation calculations.
Clones are stored in 'rotJobClones' group.'''
fcad = FreeCAD.ActiveDocument
if fcad.getObject('rotJobClones'):
if cloneName == 'Start':
if PathLog.getLevel(PathLog.thisModule()) < 4:
for cln in fcad.getObject('rotJobClones').Group:
fcad.removeObject(cln.Name)
elif cloneName == 'Delete':
if PathLog.getLevel(PathLog.thisModule()) < 4:
for cln in fcad.getObject('rotJobClones').Group:
fcad.removeObject(cln.Name)
fcad.removeObject('rotJobClones')
else:
fcad.getObject('rotJobClones').purgeTouched()
else:
fcad.addObject("App::DocumentObjectGroup", "rotJobClones")
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.getObject('rotJobClones').Visibility = False
if cloneName != 'Start' and cloneName != 'Delete':
fcad.getObject('rotJobClones').addObject(fcad.getObject(cloneName))
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.getObject(cloneName).Visibility = False
def cloneBaseAndStock(self, obj, base, angle, axis, subCount):
'''cloneBaseAndStock(obj, base, angle, axis, subCount)
Method called to create a temporary clone of the base and parent Job stock.
Clones are destroyed after usage for calculations related to rotational operations.'''
# Create a temporary clone and stock of model for rotational use.
fcad = FreeCAD.ActiveDocument
rndAng = round(angle, 8)
if rndAng < 0.0: # neg sign is converted to underscore in clone name creation.
tag = axis + '_' + axis + '_' + str(math.fabs(rndAng)).replace('.', '_')
else:
tag = axis + str(rndAng).replace('.', '_')
clnNm = obj.Name + '_base_' + '_' + str(subCount) + '_' + tag
stckClnNm = obj.Name + '_stock_' + '_' + str(subCount) + '_' + tag
if clnNm not in self.cloneNames:
self.cloneNames.append(clnNm)
self.cloneNames.append(stckClnNm)
if fcad.getObject(clnNm):
fcad.getObject(clnNm).Shape = base.Shape
else:
fcad.addObject('Part::Feature', clnNm).Shape = base.Shape
self.useTempJobClones(clnNm)
if fcad.getObject(stckClnNm):
fcad.getObject(stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape
else:
fcad.addObject('Part::Feature', stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape
self.useTempJobClones(stckClnNm)
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.getObject(stckClnNm).Transparency = 90
FreeCADGui.ActiveDocument.getObject(clnNm).ShapeColor = (1.000, 0.667, 0.000)
clnBase = fcad.getObject(clnNm)
clnStock = fcad.getObject(stckClnNm)
tag = base.Name + '_' + tag
return (clnBase, clnStock, tag)
def getFaceNormAndSurf(self, face):
'''getFaceNormAndSurf(face)
Return face.normalAt(0,0) or face.normal(0,0) and face.Surface.Axis vectors
'''
norm = FreeCAD.Vector(0.0, 0.0, 0.0)
surf = FreeCAD.Vector(0.0, 0.0, 0.0)
if hasattr(face, 'normalAt'):
n = face.normalAt(0, 0)
elif hasattr(face, 'normal'):
n = face.normal(0, 0)
if hasattr(face.Surface, 'Axis'):
s = face.Surface.Axis
else:
s = n
norm.x = n.x
norm.y = n.y
norm.z = n.z
surf.x = s.x
surf.y = s.y
surf.z = s.z
return (norm, surf)
def applyRotationalAnalysis(self, obj, base, angle, axis, subCount):
'''applyRotationalAnalysis(obj, base, angle, axis, subCount)
Create temp clone and stock and apply rotation to both.
Return new rotated clones
'''
if axis == 'X':
vect = FreeCAD.Vector(1, 0, 0)
elif axis == 'Y':
vect = FreeCAD.Vector(0, 1, 0)
# Create a temporary clone of model for rotational use.
(clnBase, clnStock, tag) = self.cloneBaseAndStock(obj, base, angle, axis, subCount)
# Rotate base to such that Surface.Axis of pocket bottom is Z=1
clnBase = Draft.rotate(clnBase, angle, center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False)
clnStock = Draft.rotate(clnStock, angle, center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False)
clnBase.purgeTouched()
clnStock.purgeTouched()
return (clnBase, angle, clnStock, tag)
def applyInverseAngle(self, obj, clnBase, clnStock, axis, angle):
'''applyInverseAngle(obj, clnBase, clnStock, axis, angle)
Apply rotations to incoming base and stock objects.'''
if axis == 'X':
vect = FreeCAD.Vector(1, 0, 0)
elif axis == 'Y':
vect = FreeCAD.Vector(0, 1, 0)
# Rotate base to inverse of original angle
clnBase = Draft.rotate(clnBase, (-2 * angle), center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False)
clnStock = Draft.rotate(clnStock, (-2 * angle), center=FreeCAD.Vector(0.0, 0.0, 0.0), axis=vect, copy=False)
clnBase.purgeTouched()
clnStock.purgeTouched()
# Update property and angle values
obj.InverseAngle = True
# obj.AttemptInverseAngle = False
angle = -1 * angle
PathLog.debug(translate("Path", "Rotated to inverse angle."))
return (clnBase, clnStock, angle)
def calculateStartFinalDepths(self, obj, shape, stock):
'''calculateStartFinalDepths(obj, shape, stock)
Calculate correct start and final depths for the shape(face) object provided.'''
finDep = max(obj.FinalDepth.Value, shape.BoundBox.ZMin)
stockTop = stock.Shape.BoundBox.ZMax
if obj.EnableRotation == 'Off':
strDep = obj.StartDepth.Value
if strDep <= finDep:
strDep = stockTop
else:
strDep = min(obj.StartDepth.Value, stockTop)
if strDep <= finDep:
strDep = stockTop
msg = translate('Path', "Start depth <= face depth.\nIncreased to stock top.")
PathLog.error(msg)
return (strDep, finDep)
def sortTuplesByIndex(self, TupleList, tagIdx):
'''sortTuplesByIndex(TupleList, tagIdx)
sort list of tuples based on tag index provided
return (TagList, GroupList)
'''
# Separate elements, regroup by orientation (axis_angle combination)
TagList = ['X34.2']
GroupList = [[(2.3, 3.4, 'X')]]
for tup in TupleList:
if tup[tagIdx] in TagList:
# Determine index of found string
i = 0
for orn in TagList:
if orn == tup[4]:
break
i += 1
GroupList[i].append(tup)
else:
TagList.append(tup[4]) # add orientation entry
GroupList.append([tup]) # add orientation entry
# Remove temp elements
TagList.pop(0)
GroupList.pop(0)
return (TagList, GroupList)
def warnDisabledAxis(self, obj, axis, sub=''):
'''warnDisabledAxis(self, obj, axis)
Provide user feedback if required axis is disabled'''
if axis == 'X' and obj.EnableRotation == 'B(y)':
msg = translate('Path', "{}:: {} is inaccessible.".format(obj.Name, sub)) + " "
msg += translate('Path', "Selected feature(s) require 'Enable Rotation: A(x)' for access.")
PathLog.warning(msg)
return True
elif axis == 'Y' and obj.EnableRotation == 'A(x)':
msg = translate('Path', "{}:: {} is inaccessible.".format(obj.Name, sub)) + " "
msg += translate('Path', "Selected feature(s) require 'Enable Rotation: B(y)' for access.")
PathLog.warning(msg)
return True
else:
return False
def isFaceUp(self, base, face):
'''isFaceUp(base, face) ...
When passed a base object and face shape, returns True if face is up.
This method is used to identify correct rotation of a model.
'''
# verify face is normal to Z+-
(norm, surf) = self.getFaceNormAndSurf(face)
if round(abs(norm.z), 8) != 1.0 or round(abs(surf.z), 8) != 1.0:
PathLog.debug('isFaceUp - face not oriented normal to Z+-')
return False
curve = face.OuterWire.Edges[0].Curve
if curve.TypeId == "Part::GeomCircle":
center = curve.Center
radius = curve.Radius * 1.
face = Part.Face(Part.Wire(Part.makeCircle(radius, center)))
up = face.extrude(FreeCAD.Vector(0.0, 0.0, 5.0))
dwn = face.extrude(FreeCAD.Vector(0.0, 0.0, -5.0))
upCmn = base.Shape.common(up)
dwnCmn = base.Shape.common(dwn)
# Identify orientation based on volumes of common() results
if len(upCmn.Edges) > 0:
PathLog.debug('isFaceUp - HAS up edges\n')
if len(dwnCmn.Edges) > 0:
PathLog.debug('isFaceUp - up and dwn edges\n')
dVol = round(dwnCmn.Volume, 6)
uVol = round(upCmn.Volume, 6)
if uVol > dVol:
return False
return True
else:
if round(upCmn.Volume, 6) == 0.0:
return True
return False
elif len(dwnCmn.Edges) > 0:
PathLog.debug('isFaceUp - HAS dwn edges only\n')
dVol = round(dwnCmn.Volume, 6)
if dVol == 0.0:
return False
return True
PathLog.debug('isFaceUp - exit True')
return True
def process_base_geometry_with_rotation(self, obj, p, subCount):
'''process_base_geometry_with_rotation(obj, p, subCount)...
This method is the control method for analyzing the selected features,
determining their rotational needs, and creating clones as needed
for rotational access for the pocketing operation.
Requires the object, obj.Base index (p), and subCount reference arguments.
Returns two lists of tuples for continued processing into paths.
'''
baseSubsTuples = []
allTuples = []
(base, subsList) = obj.Base[p]
PathLog.debug(translate('Path', "Processing subs individually ..."))
for sub in subsList:
subCount += 1
tup = self.process_nonloop_sublist(obj, base, sub)
if tup:
allTuples.append(tup)
baseSubsTuples.append(tup)
return (baseSubsTuples, allTuples)
def process_nonloop_sublist(self, obj, base, sub):
'''process_nonloop_sublist(obj, sub)...
Process sublist with non-looped set of features when rotation is enabled.
'''
rtn = False
face = base.Shape.getElement(sub)
if sub[:4] != 'Face':
if face.ShapeType == 'Edge':
edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([face])))
face = edgToFace
else:
ignoreSub = base.Name + '.' + sub
PathLog.error(translate('Path', "Selected feature is not a Face. Ignoring: {}".format(ignoreSub)))
return False
(norm, surf) = self.getFaceNormAndSurf(face)
(rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable
PathLog.debug("initial rotational analysis: {}".format(praInfo))
clnBase = base
faceIA = clnBase.Shape.getElement(sub)
if faceIA.ShapeType == 'Edge':
edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([faceIA])))
faceIA = edgToFace
if rtn is True:
faceNum = sub.replace('Face', '')
PathLog.debug("initial applyRotationalAnalysis")
(clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNum)
# Verify faces are correctly oriented - InverseAngle might be necessary
faceIA = clnBase.Shape.getElement(sub)
if faceIA.ShapeType == 'Edge':
edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([faceIA])))
faceIA = edgToFace
(norm, surf) = self.getFaceNormAndSurf(faceIA)
(rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable
PathLog.debug("follow-up rotational analysis: {}".format(praInfo2))
isFaceUp = self.isFaceUp(clnBase, faceIA)
PathLog.debug('... initial isFaceUp: {}'.format(isFaceUp))
if isFaceUp:
rtn = False
PathLog.debug('returning analysis: {}, {}'.format(praAngle, praAxis))
return (clnBase, [sub], angle, axis, clnStock)
if round(abs(praAngle), 8) == 180.0:
rtn = False
if not isFaceUp:
PathLog.debug('initial isFaceUp is False')
angle = 0.0
# Eif
if rtn:
# initial rotation failed, attempt inverse rotation if user requests it
PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.") + ' 2')
if obj.AttemptInverseAngle:
PathLog.debug(translate("Path", "Applying inverse angle automatically."))
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
if obj.InverseAngle:
PathLog.debug(translate("Path", "Applying inverse angle manually."))
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.")
PathLog.warning(msg)
faceIA = clnBase.Shape.getElement(sub)
if faceIA.ShapeType == 'Edge':
edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([faceIA])))
faceIA = edgToFace
if not self.isFaceUp(clnBase, faceIA):
angle += 180.0
# Normalize rotation angle
if angle < 0.0:
angle += 360.0
elif angle > 360.0:
angle -= 360.0
return (clnBase, [sub], angle, axis, clnStock)
if not self.warnDisabledAxis(obj, axis):
PathLog.debug(str(sub) + ": No rotation used")
axis = 'X'
angle = 0.0
stock = PathUtils.findParentJob(obj).Stock
return (base, [sub], angle, axis, stock)
def _find_parent_face_of_edge(self, rotated_features, test_shape):
'''_find_parent_face_of_edge(rotated_features, test_shape)...
Compare test_shape with each within rotated_features to identify
and return the parent face of the test_shape, if it exists.'''
for (base, sub) in rotated_features:
sub_shape = base.Shape.getElement(sub)
if test_shape.isSame(sub_shape):
return sub_shape
elif test_shape.isEqual(sub_shape):
return sub_shape
else:
for e in sub_shape.Edges:
if test_shape.isSame(e):
return sub_shape
elif test_shape.isEqual(e):
return sub_shape
return False
return features

View File

@@ -36,7 +36,7 @@ __title__ = "Path Drilling Operation"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Path Drilling operation."
__contributors__ = "russ4262 (Russell Johnson)"
__contributors__ = "IMBack!"
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
@@ -53,7 +53,6 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
def circularHoleFeatures(self, obj):
'''circularHoleFeatures(obj) ... drilling works on anything, turn on all Base geometries and Locations.'''
# return PathOp.FeatureBaseGeometry | PathOp.FeatureLocations | PathOp.FeatureRotation
return PathOp.FeatureBaseGeometry | PathOp.FeatureLocations | PathOp.FeatureCoolant
def initCircularHoleOperation(self, obj):
@@ -64,30 +63,15 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
obj.addProperty("App::PropertyBool", "DwellEnabled", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable dwell"))
obj.addProperty("App::PropertyBool", "AddTipLength", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Calculate the tip length and subtract from final depth"))
obj.addProperty("App::PropertyEnumeration", "ReturnLevel", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Controls how tool retracts Default=G99"))
obj.ReturnLevel = ['G99', 'G98'] # Canned Cycle Return Level
obj.addProperty("App::PropertyDistance", "RetractHeight", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "The height where feed starts and height during retract tool when path is finished while in a peck operation"))
obj.addProperty("App::PropertyEnumeration", "ExtraOffset", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "How far the drill depth is extended"))
obj.ExtraOffset = ['None', 'Drill Tip', '2x Drill Tip'] # Canned Cycle Return Level
# Rotation related properties
if not hasattr(obj, 'EnableRotation'):
obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis."))
obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B']
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.'))
obj.ReturnLevel = ['G99', 'G98'] # Canned Cycle Return Level
obj.ExtraOffset = ['None', 'Drill Tip', '2x Drill Tip'] # Canned Cycle Return Level
def circularHoleExecute(self, obj, holes):
'''circularHoleExecute(obj, holes) ... generate drill operation for each hole in holes.'''
PathLog.track()
PathLog.debug("\ncircularHoleExecute() in PathDrilling.py")
lastAxis = None
lastAngle = 0.0
parentJob = PathUtils.findParentJob(obj)
self.commandlist.append(Path.Command("(Begin Drilling)"))
@@ -104,57 +88,30 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
self.commandlist.append(Path.Command('G90'))
self.commandlist.append(Path.Command(obj.ReturnLevel))
for p in holes:
cmd = "G81"
cmdParams = {}
cmdParams['Z'] = p['trgtDep'] - tiplength
cmdParams['F'] = self.vertFeed
cmdParams['R'] = obj.RetractHeight.Value
if obj.PeckEnabled and obj.PeckDepth.Value > 0:
cmd = "G83"
cmdParams['Q'] = obj.PeckDepth.Value
elif obj.DwellEnabled and obj.DwellTime > 0:
cmd = "G82"
cmdParams['P'] = obj.DwellTime
cmd = "G81"
cmdParams = {}
cmdParams['Z'] = obj.FinalDepth.Value - tiplength
cmdParams['F'] = self.vertFeed
cmdParams['R'] = obj.RetractHeight.Value
if obj.PeckEnabled and obj.PeckDepth.Value > 0:
cmd = "G83"
cmdParams['Q'] = obj.PeckDepth.Value
elif obj.DwellEnabled and obj.DwellTime > 0:
cmd = "G82"
cmdParams['P'] = obj.DwellTime
# parentJob = PathUtils.findParentJob(obj)
# startHeight = obj.StartDepth.Value + parentJob.SetupSheet.SafeHeightOffset.Value
startHeight = obj.StartDepth.Value + self.job.SetupSheet.SafeHeightOffset.Value
for p in holes:
params = {}
params['X'] = p['x']
params['Y'] = p['y']
if obj.EnableRotation != 'Off':
angle = p['angle']
axis = p['axis']
# Rotate model to index for hole
if axis == 'X':
axisOfRot = 'A'
elif axis == 'Y':
axisOfRot = 'B'
elif axis == 'Z':
axisOfRot = 'C'
else:
axisOfRot = 'A'
# Set initial values for last axis and angle
if lastAxis is None:
lastAxis = axisOfRot
lastAngle = angle
# Handle axial and angular transitions
if axisOfRot != lastAxis:
self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
self.commandlist.append(Path.Command('G0', {lastAxis: 0.0, 'F': self.axialRapid}))
elif angle != lastAngle:
self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
# Prepare for drilling cycle
self.commandlist.append(Path.Command('G0', {axisOfRot: angle, 'F': self.axialRapid}))
# Update retract height due to rotation
self.opSetDefaultRetractHeight(obj)
cmdParams['R'] = obj.RetractHeight.Value
# move to hole location
self.commandlist.append(Path.Command('G0', {'X': p['x'], 'Y': p['y'], 'F': self.horizRapid}))
startHeight = obj.StartDepth.Value + parentJob.SetupSheet.SafeHeightOffset.Value
self.commandlist.append(Path.Command('G0', {'Z': startHeight, 'F': self.vertRapid}))
self.commandlist.append(Path.Command('G1', {'Z': obj.StartDepth.Value, 'F': self.vertFeed}))
@@ -168,36 +125,18 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
self.commandlist.append(Path.Command('G80'))
self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value}))
# shift axis and angle values
if obj.EnableRotation != 'Off':
lastAxis = axisOfRot
lastAngle = angle
if obj.EnableRotation != 'Off':
self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
self.commandlist.append(Path.Command('G0', {lastAxis: 0.0, 'F': self.axialRapid}))
def opSetDefaultRetractHeight(self, obj, job=None):
'''opSetDefaultRetractHeight(obj, job) ... set default Retract Height value'''
has_job = True
if not job:
job = PathUtils.findParentJob(obj)
has_job = False
def opSetDefaultValues(self, obj, job):
'''opSetDefaultValues(obj, job) ... set default value for RetractHeight'''
obj.ExtraOffset = "None"
if hasattr(job.SetupSheet, 'RetractHeight'):
obj.RetractHeight = job.SetupSheet.RetractHeight
elif self.applyExpression(obj, 'RetractHeight', 'StartDepth+SetupSheet.SafeHeightOffset'):
if has_job:
if not job:
obj.RetractHeight = 10
else:
obj.RetractHeight.Value = obj.StartDepth.Value + 1.0
def opSetDefaultValues(self, obj, job):
'''opSetDefaultValues(obj, job) ... Set default property values'''
self.opSetDefaultRetractHeight(obj, job)
if hasattr(job.SetupSheet, 'PeckDepth'):
obj.PeckDepth = job.SetupSheet.PeckDepth
elif self.applyExpression(obj, 'PeckDepth', 'OpToolDiameter*0.75'):
@@ -208,19 +147,6 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
else:
obj.DwellTime = 1
obj.ReverseDirection = False
obj.InverseAngle = False
obj.AttemptInverseAngle = False
obj.ExtraOffset = "None"
# Initial setting for EnableRotation is taken from Job SetupSheet
# User may override on per-operation basis as needed.
if hasattr(job.SetupSheet, 'SetupEnableRotation'):
obj.EnableRotation = job.SetupSheet.SetupEnableRotation
else:
obj.EnableRotation = 'Off'
def SetupProperties():
setup = []
setup.append("PeckDepth")
@@ -231,14 +157,9 @@ def SetupProperties():
setup.append("ReturnLevel")
setup.append("ExtraOffset")
setup.append("RetractHeight")
setup.append("EnableRotation")
setup.append("ReverseDirection")
setup.append("InverseAngle")
setup.append("AttemptInverseAngle")
return setup
def Create(name, obj=None):
def Create(name, obj = None):
'''Create(name) ... Creates and returns a Drilling operation.'''
if obj is None:
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
@@ -247,4 +168,4 @@ def Create(name, obj=None):
if obj.Proxy:
obj.Proxy.findAllHoles(obj)
return obj
return obj

View File

@@ -98,8 +98,6 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
obj.PeckEnabled = self.form.peckEnabled.isChecked()
if obj.ExtraOffset != str(self.form.ExtraOffset.currentText()):
obj.ExtraOffset = str(self.form.ExtraOffset.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)
@@ -123,7 +121,6 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
self.setupToolController(obj, self.form.toolController)
self.setupCoolant(obj, self.form.coolantController)
self.selectInComboBox(obj.EnableRotation, self.form.enableRotation)
def getSignalsForUpdate(self, obj):
@@ -138,7 +135,6 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
signals.append(self.form.toolController.currentIndexChanged)
signals.append(self.form.coolantController.currentIndexChanged)
signals.append(self.form.ExtraOffset.currentIndexChanged)
signals.append(self.form.enableRotation.currentIndexChanged)
return signals

View File

@@ -64,11 +64,6 @@ class ObjectHelix(PathCircularHoleBase.ObjectOp):
obj.addProperty("App::PropertyLength", "StepOver", "Helix Drill", translate("PathHelix", "Radius increment (must be smaller than tool diameter)"))
obj.addProperty("App::PropertyLength", "StartRadius", "Helix Drill", translate("PathHelix", "Starting Radius"))
# Rotation related properties
if not hasattr(obj, 'EnableRotation'):
obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis."))
obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B']
def opOnDocumentRestored(self, obj):
if not hasattr(obj, 'StartRadius'):
obj.addProperty("App::PropertyLength", "StartRadius", "Helix Drill", translate("PathHelix", "Starting Radius"))
@@ -209,21 +204,12 @@ class ObjectHelix(PathCircularHoleBase.ObjectOp):
obj.StartSide = "Inside"
obj.StepOver = 100
# Initial setting for EnableRotation is taken from Job SetupSheet
# User may override on per-operation basis as needed.
parentJob = findParentJob(obj) # PathUtils.findParentJob(obj)
if hasattr(parentJob.SetupSheet, 'SetupEnableRotation'):
obj.EnableRotation = parentJob.SetupSheet.SetupEnableRotation
else:
obj.EnableRotation = 'Off'
def SetupProperties():
setup = []
setup.append("Direction")
setup.append("StartSide")
setup.append("StepOver")
setup.append("EnableRotation")
setup.append("StartRadius")
return setup

View File

@@ -210,10 +210,10 @@ class ObjectFace(PathPocketBase.ObjectPocket):
PathLog.debug("Processing holes and face ...")
holeEnv = PathUtils.getEnvelope(partshape=holeShape, depthparams=self.depthparams)
newEnv = env.cut(holeEnv)
tup = newEnv, False, 'pathMillFace', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value
tup = newEnv, False, 'pathMillFace'
else:
PathLog.debug("Processing solid face ...")
tup = env, False, 'pathMillFace', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value
tup = env, False, 'pathMillFace'
self.removalshapes.append(tup)
obj.removalshape = self.removalshapes[0][0] # save removal shape

View File

@@ -103,8 +103,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
obj.StepOver = self.form.stepOverPercent.value()
if obj.OffsetPattern != str(self.form.offsetPattern.currentText()):
obj.OffsetPattern = str(self.form.offsetPattern.currentText())
if obj.EnableRotation != str(self.form.enableRotation.currentText()):
obj.EnableRotation = str(self.form.enableRotation.currentText())
PathGui.updateInputField(obj, 'ExtraOffset', self.form.extraOffset)
self.updateToolController(obj, self.form.toolController)
@@ -144,7 +142,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
self.selectInComboBox(obj.CutMode, self.form.cutMode)
self.setupToolController(obj, self.form.toolController)
self.setupCoolant(obj, self.form.coolantController)
self.selectInComboBox(obj.EnableRotation, self.form.enableRotation)
if FeatureFacing & self.pocketFeatures():
self.selectInComboBox(obj.BoundaryShape, self.form.boundaryShape)
@@ -164,7 +161,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
signals.append(self.form.useOutline.clicked)
signals.append(self.form.minTravel.clicked)
signals.append(self.form.coolantController.currentIndexChanged)
signals.append(self.form.enableRotation.currentIndexChanged)
if FeatureFacing & self.pocketFeatures():
signals.append(self.form.boundaryShape.currentIndexChanged)

View File

@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2020 Schildkroet *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
@@ -26,22 +25,22 @@ import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathOp as PathOp
import PathScripts.PathPocketBase as PathPocketBase
import PathScripts.PathUtils as PathUtils
import math
from PySide import QtCore
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
Draft = LazyLoader('Draft', globals(), 'Draft')
Part = LazyLoader('Part', globals(), 'Part')
TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw')
math = LazyLoader('math', globals(), 'math')
from PySide import QtCore
__title__ = "Path Pocket Shape Operation"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Class and implementation of shape based Pocket operation."
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# PathLog.trackModule(PathLog.thisModule())
@@ -62,7 +61,6 @@ def endPoints(edgeOrWire):
cnt = len([p2 for p2 in pts if PathGeom.pointsCoincide(p, p2)])
if 1 == cnt:
unique.append(p)
return unique
pfirst = edgeOrWire.valueAt(edgeOrWire.FirstParameter)
@@ -78,7 +76,6 @@ def includesPoint(p, pts):
for pt in pts:
if PathGeom.pointsCoincide(p, pt):
return True
return False
@@ -87,12 +84,10 @@ def selectOffsetWire(feature, wires):
closest = None
for w in wires:
dist = feature.distToShape(w)[0]
if closest is None or dist > closest[0]: # pylint: disable=unsubscriptable-object
if closest is None or dist > closest[0]:
closest = (dist, w)
if closest is not None:
if not closest is None:
return closest[1]
return None
@@ -104,6 +99,7 @@ def extendWire(feature, wire, length):
off2D = wire.makeOffset2D(length)
except FreeCAD.Base.FreeCADError:
return None
endPts = endPoints(wire)
if endPts:
edges = [e for e in off2D.Edges if Part.Circle != type(e.Curve) or not includesPoint(e.Curve.Center, endPts)]
@@ -122,15 +118,14 @@ def extendWire(feature, wire, length):
edges.append(Part.Edge(Part.LineSegment(endPts[1], ePts[0])))
edges.extend(offset.Edges)
edges.append(Part.Edge(Part.LineSegment(endPts[0], ePts[1])))
return Part.Wire(edges)
return None
class Extension(object):
DirectionNormal = 0
DirectionX = 1
DirectionY = 2
DirectionNormal = 0
DirectionX = 1
DirectionY = 2
def __init__(self, obj, feature, sub, length, direction):
PathLog.debug("Extension(%s, %s, %s, %.2f, %s" % (obj.Label, feature, sub, length, direction))
@@ -141,8 +136,6 @@ class Extension(object):
self.direction = direction
self.extFaces = list()
self.wire = None
def getSubLink(self):
return "%s:%s" % (self.feature, self.sub)
@@ -158,7 +151,6 @@ class Extension(object):
wire = Part.Wire([e0, e1, e2, e3])
self.wire = wire
return wire
return extendWire(feature, Part.Wire([e0]), self.length.Value)
def _getEdgeNumbers(self):
@@ -166,8 +158,8 @@ class Extension(object):
numbers = [nr for nr in self.sub[5:-1].split(',')]
else:
numbers = [self.sub[4:]]
PathLog.debug("_getEdgeNumbers() -> %s" % numbers)
return numbers
def _getEdgeNames(self):
@@ -181,10 +173,8 @@ class Extension(object):
poffMinus = p0 - 0.01 * normal
if not self.obj.Shape.isInside(poffPlus, 0.005, True):
return normal
if not self.obj.Shape.isInside(poffMinus, 0.005, True):
return normal.negative()
return None
def _getDirection(self, wire):
@@ -193,6 +183,7 @@ class Extension(object):
tangent = e0.tangentAt(midparam)
PathLog.track('tangent', tangent, self.feature, self.sub)
normal = tangent.cross(FreeCAD.Vector(0, 0, 1))
if PathGeom.pointsCoincide(normal, FreeCAD.Vector(0, 0, 0)):
return None
@@ -239,7 +230,6 @@ class Extension(object):
r = circle.Radius - self.length.Value
else:
r = circle.Radius + self.length.Value
# assuming the offset produces a valid circle - go for it
if r > 0:
e3 = Part.makeCircle(r, circle.Center, circle.Axis, edge.FirstParameter * 180 / math.pi, edge.LastParameter * 180 / math.pi)
@@ -261,7 +251,6 @@ class Extension(object):
e0 = Part.makeLine(center, edge.valueAt(edge.FirstParameter))
e2 = Part.makeLine(edge.valueAt(edge.LastParameter), center)
return Part.Wire([e0, edge, e2])
PathLog.track()
return Part.Wire([edge])
@@ -270,10 +259,8 @@ class Extension(object):
direction = self._getDirection(sub)
if direction is None:
return None
# return self._extendEdge(feature, edge, direction)
return self._extendEdge(feature, edges[0], direction)
return extendWire(feature, sub, self.length.Value)
def _makeCircularExtFace(self, edge, extWire):
@@ -300,12 +287,13 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
'''Proxy object for Pocket operation.'''
def areaOpFeatures(self, obj):
return super(ObjectPocket, self).areaOpFeatures(obj) | PathOp.FeatureLocations
return super(self.__class__, self).areaOpFeatures(obj) | PathOp.FeatureLocations
def initPocketOp(self, obj):
'''initPocketOp(obj) ... setup receiver'''
if not hasattr(obj, 'UseOutline'):
obj.addProperty('App::PropertyBool', 'UseOutline', 'Pocket', QtCore.QT_TRANSLATE_NOOP('PathPocketShape', 'Uses the outline of the base geometry.'))
obj.UseOutline = False
if not hasattr(obj, 'ExtensionLengthDefault'):
obj.addProperty('App::PropertyDistance', 'ExtensionLengthDefault', 'Extension', QtCore.QT_TRANSLATE_NOOP('PathPocketShape', 'Default length of extensions.'))
if not hasattr(obj, 'ExtensionFeature'):
@@ -315,40 +303,10 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
obj.ExtensionCorners = True
obj.setEditorMode('ExtensionFeature', 2)
self.initRotationOp(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 areaOpOnChanged(self, obj, prop):
'''areaOpOnChanged(obj, porp) ... process operation specific changes to properties.'''
if prop == 'EnableRotation':
self.setEditorProperties(obj)
def setEditorProperties(self, obj):
obj.setEditorMode('ReverseDirection', 2)
if obj.EnableRotation == 'Off':
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 areaOpOnDocumentRestored(self, obj):
'''opOnDocumentRestored(obj) ... adds the UseOutline property, others, if they doesn't exist.'''
'''opOnDocumentRestored(obj) ... adds the UseOutline property if it doesn't exist.'''
self.initPocketOp(obj)
self.setEditorProperties(obj)
def pocketInvertExtraOffset(self):
return False
@@ -356,184 +314,106 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
def areaOpShapes(self, obj):
'''areaOpShapes(obj) ... return shapes representing the solids to be removed.'''
PathLog.track()
PathLog.debug("----- areaOpShapes() in PathPocketShape.py")
self.isDebug = True if PathLog.getLevel(PathLog.thisModule()) == 4 else False
baseSubsTuples = []
allTuples = []
subCount = 0
if obj.Base:
PathLog.debug('Processing obj.Base')
self.removalshapes = [] # pylint: disable=attribute-defined-outside-init
if obj.EnableRotation == 'Off':
stock = PathUtils.findParentJob(obj).Stock
for (base, subList) in obj.Base:
tup = (base, subList, 0.0, 'X', stock)
baseSubsTuples.append(tup)
else:
PathLog.debug('... Rotation is active')
# method call here
for p in range(0, len(obj.Base)):
(bst, at) = self.process_base_geometry_with_rotation(obj, p, subCount)
allTuples.extend(at)
baseSubsTuples.extend(bst)
for o in baseSubsTuples:
self.horiz = [] # pylint: disable=attribute-defined-outside-init
self.vert = [] # pylint: disable=attribute-defined-outside-init
subBase = o[0]
subsList = o[1]
angle = o[2]
axis = o[3]
# stock = o[4]
for sub in subsList:
PathLog.debug('base items exist. Processing...')
self.removalshapes = []
self.horiz = []
vertical = []
for o in obj.Base:
PathLog.debug('Base item: {}'.format(o))
base = o[0]
for sub in o[1]:
if 'Face' in sub:
if not self.clasifySub(subBase, sub):
PathLog.error(translate('PathPocket', 'Pocket does not support shape %s.%s') % (subBase.Label, sub))
if obj.EnableRotation != 'Off':
PathLog.warning(translate('PathPocket', 'Face might not be within rotation accessibility limits.'))
# Determine final depth as highest value of bottom boundbox of vertical face,
# in case of uneven faces on bottom
if len(self.vert) > 0:
vFinDep = self.vert[0].BoundBox.ZMin
for vFace in self.vert:
if vFace.BoundBox.ZMin > vFinDep:
vFinDep = vFace.BoundBox.ZMin
# Determine if vertical faces for a loop: Extract planar loop wire as new horizontal face.
self.vertical = PathGeom.combineConnectedShapes(self.vert) # pylint: disable=attribute-defined-outside-init
self.vWires = [TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) for shape in self.vertical] # pylint: disable=attribute-defined-outside-init
for wire in self.vWires:
w = PathGeom.removeDuplicateEdges(wire)
face = Part.Face(w)
# face.tessellate(0.1)
if PathGeom.isRoughly(face.Area, 0):
msg = translate('PathPocket', 'Vertical faces do not form a loop - ignoring')
PathLog.error(msg)
else:
face.translate(FreeCAD.Vector(0, 0, vFinDep - face.BoundBox.ZMin))
face = base.Shape.getElement(sub)
if type(face.Surface) == Part.Plane and PathGeom.isVertical(face.Surface.Axis):
# it's a flat horizontal face
self.horiz.append(face)
# add faces for extensions
self.exts = [] # pylint: disable=attribute-defined-outside-init
for ext in self.getExtensions(obj):
wire = ext.getWire()
if wire:
for face in ext.getExtensionFaces(wire):
self.horiz.append(face)
self.exts.append(face)
# Place all self.horiz faces into same working plane
for h in self.horiz:
h.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - h.BoundBox.ZMin))
# check all faces and see if they are touching/overlapping and combine those into a compound
self.horizontal = [] # pylint: disable=attribute-defined-outside-init
for shape in PathGeom.combineConnectedShapes(self.horiz):
shape.sewShape()
# shape.tessellate(0.1)
shpZMin = shape.BoundBox.ZMin
PathLog.debug('PathGeom.combineConnectedShapes shape.BoundBox.ZMin: {}'.format(shape.BoundBox.ZMin))
if obj.UseOutline:
wire = TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1))
wFace = Part.Face(wire)
if wFace.BoundBox.ZMin != shpZMin:
wFace.translate(FreeCAD.Vector(0, 0, shpZMin - wFace.BoundBox.ZMin))
self.horizontal.append(wFace)
PathLog.debug('PathGeom.combineConnectedShapes shape.BoundBox.ZMin: {}'.format(wFace.BoundBox.ZMin))
else:
self.horizontal.append(shape)
# move all horizontal faces to FinalDepth
# extrude all faces up to StartDepth and those are the removal shapes
start_dep = obj.StartDepth.Value
clrnc = 0.5
# self._addDebugObject('subBase', subBase.Shape)
for face in self.horizontal:
isFaceUp = True
invZ = 0.0
useAngle = angle
faceZMin = face.BoundBox.ZMin
adj_final_dep = obj.FinalDepth.Value
trans = obj.FinalDepth.Value - face.BoundBox.ZMin
PathLog.debug('face.BoundBox.ZMin: {}'.format(face.BoundBox.ZMin))
if obj.EnableRotation != 'Off':
PathLog.debug('... running isFaceUp()')
isFaceUp = self.isFaceUp(subBase, face)
# Determine if face is really oriented toward Z+ (rotational purposes)
# ignore for cylindrical faces
if not isFaceUp:
PathLog.debug('... NOT isFaceUp')
useAngle += 180.0
invZ = (-2 * face.BoundBox.ZMin)
face.translate(FreeCAD.Vector(0.0, 0.0, invZ))
faceZMin = face.BoundBox.ZMin # reset faceZMin
PathLog.debug('... face.BoundBox.ZMin: {}'.format(face.BoundBox.ZMin))
elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical(face.Surface.Axis):
# vertical cylinder wall
if any(e.isClosed() for e in face.Edges):
# complete cylinder
circle = Part.makeCircle(face.Surface.Radius, face.Surface.Center)
disk = Part.Face(Part.Wire(circle))
self.horiz.append(disk)
else:
# partial cylinder wall
vertical.append(face)
elif type(face.Surface) == Part.Plane and PathGeom.isHorizontal(face.Surface.Axis):
vertical.append(face)
else:
PathLog.debug('... isFaceUp')
if useAngle > 180.0:
useAngle -= 360.0
PathLog.error(translate('PathPocket', 'Pocket does not support shape %s.%s') % (base.Label, sub))
# Apply LimitDepthToFace property for rotational operations
if obj.LimitDepthToFace:
if obj.FinalDepth.Value < face.BoundBox.ZMin:
PathLog.debug('obj.FinalDepth.Value < face.BoundBox.ZMin')
# Raise FinalDepth to face depth
adj_final_dep = faceZMin # face.BoundBox.ZMin # faceZMin
# Ensure StartDepth is above FinalDepth
if start_dep <= adj_final_dep:
start_dep = adj_final_dep + 1.0
msg = translate('PathPocketShape', 'Start Depth is lower than face depth. Setting to:')
PathLog.warning(msg + ' {} mm.'.format(start_dep))
PathLog.debug('LimitDepthToFace adj_final_dep: {}'.format(adj_final_dep))
# Eif
self.vertical = PathGeom.combineConnectedShapes(vertical)
self.vWires = [TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) for shape in self.vertical]
for wire in self.vWires:
w = PathGeom.removeDuplicateEdges(wire)
face = Part.Face(w)
face.tessellate(0.1)
if PathGeom.isRoughly(face.Area, 0):
PathLog.error(translate('PathPocket', 'Vertical faces do not form a loop - ignoring'))
else:
self.horiz.append(face)
face.translate(FreeCAD.Vector(0.0, 0.0, adj_final_dep - faceZMin - clrnc))
zExtVal = start_dep - adj_final_dep + (2 * clrnc)
extShp = face.removeSplitter().extrude(FreeCAD.Vector(0, 0, zExtVal))
self.removalshapes.append((extShp, False, 'pathPocketShape', useAngle, axis, start_dep, adj_final_dep))
PathLog.debug("Extent values are strDep: {}, finDep: {}, extrd: {}".format(start_dep, adj_final_dep, zExtVal))
# Efor face
# Efor
# add faces for extensions
self.exts = [] # pylint: disable=attribute-defined-outside-init
for ext in self.getExtensions(obj):
wire = ext.getWire()
if wire:
for face in ext.getExtensionFaces(wire):
self.horiz.append(face)
self.exts.append(face)
else:
# process the job base object as a whole
PathLog.debug(translate("Path", 'Processing model as a whole ...'))
finDep = obj.FinalDepth.Value
strDep = obj.StartDepth.Value
self.outlines = [Part.Face(TechDraw.findShapeOutline(base.Shape, 1, FreeCAD.Vector(0, 0, 1))) for base in self.model] # pylint: disable=attribute-defined-outside-init
# Place all self.horiz faces into same working plane
for h in self.horiz:
h.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - h.BoundBox.ZMin))
# check all faces and see if they are touching/overlapping and combine those into a compound
self.horizontal = []
for shape in PathGeom.combineConnectedShapes(self.horiz):
shape.sewShape()
shape.tessellate(0.05) # originally 0.1
if obj.UseOutline:
wire = TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1))
wire.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - wire.BoundBox.ZMin))
self.horizontal.append(Part.Face(wire))
else:
self.horizontal.append(shape)
# extrude all faces up to StartDepth and those are the removal shapes
extent = FreeCAD.Vector(0, 0, obj.StartDepth.Value - obj.FinalDepth.Value)
self.removalshapes = [(face.removeSplitter().extrude(extent), False) for face in self.horizontal]
else: # process the job base object as a whole
PathLog.debug("processing the whole job base object")
self.outlines = [Part.Face(TechDraw.findShapeOutline(base.Shape, 1, FreeCAD.Vector(0, 0, 1))) for base in self.model]
stockBB = self.stock.Shape.BoundBox
self.removalshapes = [] # pylint: disable=attribute-defined-outside-init
self.bodies = [] # pylint: disable=attribute-defined-outside-init
self.removalshapes = []
self.bodies = []
for outline in self.outlines:
outline.translate(FreeCAD.Vector(0, 0, stockBB.ZMin - 1))
body = outline.extrude(FreeCAD.Vector(0, 0, stockBB.ZLength + 2))
self.bodies.append(body)
self.removalshapes.append((self.stock.Shape.cut(body), False, 'pathPocketShape', 0.0, 'X', strDep, finDep))
self.removalshapes.append((self.stock.Shape.cut(body), False))
for (shape, hole, sub, angle, axis, strDep, finDep) in self.removalshapes: # pylint: disable=unused-variable
for (shape, hole) in self.removalshapes:
shape.tessellate(0.05) # originally 0.1
if self.removalshapes:
obj.removalshape = self.removalshapes[0][0]
return self.removalshapes
def areaOpSetDefaultValues(self, obj, job):
'''areaOpSetDefaultValues(obj, job) ... set default values'''
obj.StepOver = 100
obj.ZigZagAngle = 45
obj.ExtensionCorners = True
obj.UseOutline = False
obj.ReverseDirection = False
obj.InverseAngle = False
obj.AttemptInverseAngle = True
obj.LimitDepthToFace = True
obj.ExtensionCorners = True
if job and job.Stock:
bb = job.Stock.Shape.BoundBox
obj.OpFinalDepth = bb.ZMin
obj.OpStartDepth = bb.ZMax
obj.setExpression('ExtensionLengthDefault', 'OpToolDiameter / 2')
def createExtension(self, obj, extObj, extFeature, extSub):
@@ -553,468 +433,6 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
PathLog.track(obj.Label, len(extensions))
obj.ExtensionFeature = [(ext.obj, ext.getSubLink()) for ext in extensions]
def checkForFacesLoop(self, base, subsList):
'''checkForFacesLoop(base, subsList)...
Accepts a list of face names for the given base.
Checks to determine if they are looped together.
'''
PathLog.track()
fCnt = 0
go = True
vertLoopFace = None
tempNameList = []
delTempNameList = 0
saSum = FreeCAD.Vector(0.0, 0.0, 0.0)
norm = FreeCAD.Vector(0.0, 0.0, 0.0)
surf = FreeCAD.Vector(0.0, 0.0, 0.0)
precision = 6
def makeTempExtrusion(base, sub, fCnt):
extName = 'tmpExtrude' + str(fCnt)
wireName = 'tmpWire' + str(fCnt)
wr = Part.Wire(Part.__sortEdges__(base.Shape.getElement(sub).Edges))
if wr.isNull():
PathLog.debug('No wire created from {}'.format(sub))
return (False, 0, 0)
else:
tmpWire = FreeCAD.ActiveDocument.addObject('Part::Feature', wireName).Shape = wr
tmpWireObj = FreeCAD.ActiveDocument.getObject(wireName)
tmpExtObj = FreeCAD.ActiveDocument.addObject('Part::Extrusion', extName)
tmpExt = FreeCAD.ActiveDocument.getObject(extName)
tmpExt.Base = tmpWireObj
tmpExt.DirMode = "Normal"
tmpExt.DirLink = None
tmpExt.LengthFwd = 10.0
tmpExt.LengthRev = 0.0
tmpExt.Solid = True
tmpExt.Reversed = False
tmpExt.Symmetric = False
tmpExt.TaperAngle = 0.0
tmpExt.TaperAngleRev = 0.0
tmpExt.recompute()
tmpExt.purgeTouched()
tmpWireObj.purgeTouched()
return (True, tmpWireObj, tmpExt)
def roundValue(precision, val):
# Convert VALxe-15 numbers to zero
if PathGeom.isRoughly(0.0, val) is True:
return 0.0
# Convert VAL.99999999 to next integer
elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance:
return round(val)
else:
return round(val, precision)
# Determine precision from Tolerance
for i in range(0, 13):
if PathGeom.Tolerance * (i * 10) == 1.0:
precision = i
break
# Sub Surface.Axis values of faces
# Vector of (0, 0, 0) will suggests a loop
for sub in subsList:
if 'Face' in sub:
fCnt += 1
saSum = saSum.add(base.Shape.getElement(sub).Surface.Axis)
# Minimim of three faces required for loop to exist
if fCnt < 3:
go = False
# Determine if all faces combined point toward loop center = False
if PathGeom.isRoughly(0, saSum.x):
if PathGeom.isRoughly(0, saSum.y):
if PathGeom.isRoughly(0, saSum.z):
PathLog.debug("Combined subs suggest loop of faces. Checking ...")
go = True
if go is True:
lastExtrusion = None
matchList = []
go = False
# Cycle through subs, extruding to solid for each
for sub in subsList:
if 'Face' in sub:
fCnt += 1
go = False
# Extrude face to solid
(rtn, tmpWire, tmpExt) = makeTempExtrusion(base, sub, fCnt)
# If success, record new temporary objects for deletion
if rtn is True:
tempNameList.append(tmpExt.Name)
tempNameList.append(tmpWire.Name)
delTempNameList += 1
if lastExtrusion is None:
lastExtrusion = tmpExt
rtn = True
else:
go = False
break
# Cycle through faces on each extrusion, looking for common normal faces for rotation analysis
if len(matchList) == 0:
for fc in lastExtrusion.Shape.Faces:
(norm, raw) = self.getFaceNormAndSurf(fc)
rnded = FreeCAD.Vector(roundValue(precision, raw.x), roundValue(precision, raw.y), roundValue(precision, raw.z))
if rnded.x == 0.0 or rnded.y == 0.0 or rnded.z == 0.0:
for fc2 in tmpExt.Shape.Faces:
(norm2, raw2) = self.getFaceNormAndSurf(fc2) # pylint: disable=unused-variable
rnded2 = FreeCAD.Vector(roundValue(precision, raw2.x), roundValue(precision, raw2.y), roundValue(precision, raw2.z))
if rnded == rnded2:
matchList.append(fc2)
go = True
else:
for m in matchList:
(norm, raw) = self.getFaceNormAndSurf(m)
rnded = FreeCAD.Vector(roundValue(precision, raw.x), roundValue(precision, raw.y), roundValue(precision, raw.z))
for fc2 in tmpExt.Shape.Faces:
(norm2, raw2) = self.getFaceNormAndSurf(fc2)
rnded2 = FreeCAD.Vector(roundValue(precision, raw2.x), roundValue(precision, raw2.y), roundValue(precision, raw2.z))
if rnded.x == 0.0 or rnded.y == 0.0 or rnded.z == 0.0:
if rnded == rnded2:
go = True
# Eif
if go is False:
break
# Eif
# Eif 'Face'
# Efor
if go is True:
go = False
if len(matchList) == 2:
saTotal = FreeCAD.Vector(0.0, 0.0, 0.0)
for fc in matchList:
(norm, raw) = self.getFaceNormAndSurf(fc)
rnded = FreeCAD.Vector(roundValue(precision, raw.x), roundValue(precision, raw.y), roundValue(precision, raw.z))
if (rnded.y > 0.0 or rnded.z > 0.0) and vertLoopFace is None:
vertLoopFace = fc
saTotal = saTotal.add(rnded)
if saTotal == FreeCAD.Vector(0.0, 0.0, 0.0):
if vertLoopFace is not None:
go = True
if go is True:
(norm, surf) = self.getFaceNormAndSurf(vertLoopFace)
else:
PathLog.debug(translate('Path', 'Can not identify loop.'))
if delTempNameList > 0:
for tmpNm in tempNameList:
FreeCAD.ActiveDocument.removeObject(tmpNm)
return (go, norm, surf)
def planarFaceFromExtrusionEdges(self, face, trans):
'''planarFaceFromExtrusionEdges(face, trans)...
Use closed edges to create a temporary face for use in the pocketing operation.
'''
useFace = 'useFaceName'
minArea = 0.0
fCnt = 0
clsd = []
planar = False
# Identify closed edges
for edg in face.Edges:
if edg.isClosed():
PathLog.debug(' -e.isClosed()')
clsd.append(edg)
planar = True
# Attempt to create planar faces and select that with smallest area for use as pocket base
if planar is True:
planar = False
for edg in clsd:
fCnt += 1
fName = sub + '_face_' + str(fCnt)
# Create planar face from edge
mFF = Part.Face(Part.Wire(Part.__sortEdges__([edg])))
if mFF.isNull():
PathLog.debug('Face(Part.Wire()) failed')
else:
if trans is True:
mFF.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - mFF.BoundBox.ZMin))
if FreeCAD.ActiveDocument.getObject(fName):
FreeCAD.ActiveDocument.removeObject(fName)
tmpFaceObj = FreeCAD.ActiveDocument.addObject('Part::Feature', fName).Shape = mFF
tmpFace = FreeCAD.ActiveDocument.getObject(fName)
tmpFace.purgeTouched()
if minArea == 0.0:
minArea = tmpFace.Shape.Face1.Area
useFace = fName
planar = True
elif tmpFace.Shape.Face1.Area < minArea:
minArea = tmpFace.Shape.Face1.Area
FreeCAD.ActiveDocument.removeObject(useFace)
useFace = fName
else:
FreeCAD.ActiveDocument.removeObject(fName)
if useFace != 'useFaceName':
self.useTempJobClones(useFace)
return (planar, useFace)
def clasifySub(self, bs, sub):
'''clasifySub(bs, sub)...
Given a base and a sub-feature name, returns True
if the sub-feature is a horizontally oriented flat face.
'''
face = bs.Shape.getElement(sub)
if type(face.Surface) == Part.Plane:
PathLog.debug('type() == Part.Plane')
if PathGeom.isVertical(face.Surface.Axis):
PathLog.debug(' -isVertical()')
# it's a flat horizontal face
self.horiz.append(face)
return True
elif PathGeom.isHorizontal(face.Surface.Axis):
PathLog.debug(' -isHorizontal()')
self.vert.append(face)
return True
else:
return False
elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical(face.Surface.Axis):
PathLog.debug('type() == Part.Cylinder')
# vertical cylinder wall
if any(e.isClosed() for e in face.Edges):
PathLog.debug(' -e.isClosed()')
# complete cylinder
circle = Part.makeCircle(face.Surface.Radius, face.Surface.Center)
disk = Part.Face(Part.Wire(circle))
disk.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - disk.BoundBox.ZMin))
self.horiz.append(disk)
return True
else:
PathLog.debug(' -none isClosed()')
# partial cylinder wall
self.vert.append(face)
return True
elif type(face.Surface) == Part.SurfaceOfExtrusion:
# extrusion wall
PathLog.debug('type() == Part.SurfaceOfExtrusion')
# Attempt to extract planar face from surface of extrusion
(planar, useFace) = self.planarFaceFromExtrusionEdges(face, trans=True)
# Save face object to self.horiz for processing or display error
if planar is True:
uFace = FreeCAD.ActiveDocument.getObject(useFace)
self.horiz.append(uFace.Shape.Faces[0])
msg = translate('Path', "<b>Verify depth of pocket for '{}'.</b>".format(sub))
msg += translate('Path', "\n<br>Pocket is based on extruded surface.")
msg += translate('Path', "\n<br>Bottom of pocket might be non-planar and/or not normal to spindle axis.")
msg += translate('Path', "\n<br>\n<br><i>3D pocket bottom is NOT available in this operation</i>.")
PathLog.warning(msg)
else:
PathLog.error(translate("Path", "Failed to create a planar face from edges in {}.".format(sub)))
else:
PathLog.debug(' -type(face.Surface): {}'.format(type(face.Surface)))
return False
# Process obj.Base with rotation enabled
def process_base_geometry_with_rotation(self, obj, p, subCount):
'''process_base_geometry_with_rotation(obj, p, subCount)...
This method is the control method for analyzing the selected features,
determining their rotational needs, and creating clones as needed
for rotational access for the pocketing operation.
Requires the object, obj.Base index (p), and subCount reference arguments.
Returns two lists of tuples for continued processing into pocket paths.
'''
baseSubsTuples = []
allTuples = []
isLoop = False
(base, subsList) = obj.Base[p]
# First, check all subs collectively for loop of faces
if len(subsList) > 2:
(isLoop, norm, surf) = self.checkForFacesLoop(base, subsList)
if isLoop:
PathLog.debug("Common Surface.Axis or normalAt() value found for loop faces.")
subCount += 1
tup = self.process_looped_sublist(obj, norm, surf)
if tup:
allTuples.append(tup)
baseSubsTuples.append(tup)
# Eif
if not isLoop:
PathLog.debug(translate('Path', "Processing subs individually ..."))
for sub in subsList:
subCount += 1
tup = self.process_nonloop_sublist(obj, base, sub)
if tup:
allTuples.append(tup)
baseSubsTuples.append(tup)
# Eif
return (baseSubsTuples, allTuples)
def process_looped_sublist(self, obj, norm, surf):
'''process_looped_sublist(obj, norm, surf)...
Process set of looped faces when rotation is enabled.
'''
PathLog.debug(translate("Path", "Selected faces form loop. Processing looped faces."))
rtn = False
(rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable
if rtn is True:
faceNums = ""
for f in subsList:
faceNums += '_' + f.replace('Face', '')
(clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNums) # pylint: disable=unused-variable
# Verify faces are correctly oriented - InverseAngle might be necessary
PathLog.debug("Checking if faces are oriented correctly after rotation.")
for sub in subsList:
face = clnBase.Shape.getElement(sub)
if type(face.Surface) == Part.Plane:
if not PathGeom.isHorizontal(face.Surface.Axis):
rtn = False
PathLog.warning(translate("PathPocketShape", "Face appears to NOT be horizontal AFTER rotation applied."))
break
if rtn is False:
PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.") + ' 1')
if obj.InverseAngle:
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
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 angle < 0.0:
angle += 360.0
tup = clnBase, subsList, angle, axis, clnStock
else:
if self.warnDisabledAxis(obj, axis) is False:
PathLog.debug("No rotation used")
axis = 'X'
angle = 0.0
stock = PathUtils.findParentJob(obj).Stock
tup = base, subsList, angle, axis, stock
# Eif
return tup
def process_nonloop_sublist(self, obj, base, sub):
'''process_nonloop_sublist(obj, sub)...
Process sublist with non-looped set of features when rotation is enabled.
'''
if sub[:4] != 'Face':
ignoreSub = base.Name + '.' + sub
PathLog.error(translate('Path', "Selected feature is not a Face. Ignoring: {}".format(ignoreSub)))
return False
rtn = False
face = base.Shape.getElement(sub)
if type(face.Surface) == Part.SurfaceOfExtrusion:
# extrusion wall
PathLog.debug('analyzing type() == Part.SurfaceOfExtrusion')
# Attempt to extract planar face from surface of extrusion
(planar, useFace) = self.planarFaceFromExtrusionEdges(face, trans=False)
# Save face object to self.horiz for processing or display error
if planar is True:
base = FreeCAD.ActiveDocument.getObject(useFace)
sub = 'Face1'
PathLog.debug(' -successful face created: {}'.format(useFace))
else:
PathLog.error(translate("Path", "Failed to create a planar face from edges in {}.".format(sub)))
(norm, surf) = self.getFaceNormAndSurf(face)
(rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable
PathLog.debug("initial {}".format(praInfo))
clnBase = base
faceIA = clnBase.Shape.getElement(sub)
if rtn is True:
faceNum = sub.replace('Face', '')
PathLog.debug("initial applyRotationalAnalysis")
(clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNum)
# Verify faces are correctly oriented - InverseAngle might be necessary
faceIA = clnBase.Shape.getElement(sub)
(norm, surf) = self.getFaceNormAndSurf(faceIA)
(rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable
PathLog.debug("follow-up {}".format(praInfo2))
isFaceUp = self.isFaceUp(clnBase, faceIA)
if isFaceUp:
rtn = False
if round(abs(praAngle), 8) == 180.0:
rtn = False
if not isFaceUp:
PathLog.debug('initial isFaceUp is False')
angle = 0.0
# Eif
if rtn:
# initial rotation failed, attempt inverse rotation if user requests it
PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.") + ' 2')
if obj.AttemptInverseAngle:
PathLog.debug(translate("Path", "Applying inverse angle automatically."))
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
if obj.InverseAngle:
PathLog.debug(translate("Path", "Applying inverse angle manually."))
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.")
PathLog.warning(msg)
faceIA = clnBase.Shape.getElement(sub)
if not self.isFaceUp(clnBase, faceIA):
angle += 180.0
# Normalize rotation angle
if angle < 0.0:
angle += 360.0
elif angle > 360.0:
angle -= 360.0
return (clnBase, [sub], angle, axis, clnStock)
if not self.warnDisabledAxis(obj, axis):
PathLog.debug(str(sub) + ": No rotation used")
axis = 'X'
angle = 0.0
stock = PathUtils.findParentJob(obj).Stock
return (base, [sub], angle, axis, stock)
# Method to add temporary debug object
def _addDebugObject(self, objName, objShape):
'''_addDebugObject(objName, objShape)...
Is passed a desired debug object's desired name and shape.
This method creates a FreeCAD object for debugging purposes.
The created object must be deleted manually from the object tree
by the user.
'''
if self.isDebug:
O = FreeCAD.ActiveDocument.addObject('Part::Feature', 'debug_' + objName)
O.Shape = objShape
O.purgeTouched()
def SetupProperties():
setup = PathPocketBase.SetupProperties()
@@ -1022,10 +440,6 @@ def SetupProperties():
setup.append('ExtensionLengthDefault')
setup.append('ExtensionFeature')
setup.append('ExtensionCorners')
setup.append("ReverseDirection")
setup.append("InverseAngle")
setup.append("AttemptInverseAngle")
setup.append("LimitDepthToFace")
return setup
@@ -1033,5 +447,7 @@ def Create(name, obj=None):
'''Create(name) ... Creates and returns a Pocket operation.'''
if obj is None:
obj = FreeCAD.ActiveDocument.addObject('Path::FeaturePython', name)
obj.Proxy = ObjectPocket(obj, name)
return obj
return obj

View File

@@ -118,15 +118,6 @@ class ObjectProfile(PathAreaOp.ObjectOp):
QtCore.QT_TRANSLATE_NOOP("App::Property", "Side of edge that tool should cut")),
("App::PropertyBool", "UseComp", "Profile",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if using Cutter Radius Compensation")),
("App::PropertyBool", "ReverseDirection", "Rotation",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse direction of pocket operation.")),
("App::PropertyBool", "InverseAngle", "Rotation",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Inverse the angle. Example: -22.5 -> 22.5 degrees.")),
("App::PropertyBool", "AttemptInverseAngle", "Rotation",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Attempt the inverse angle for face access if original rotation fails.")),
("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 areaOpPropertyEnumerations(self):
@@ -144,15 +135,11 @@ class ObjectProfile(PathAreaOp.ObjectOp):
'''areaOpPropertyDefaults(obj, job) ... returns a dictionary of default values
for the operation's properties.'''
return {
'AttemptInverseAngle': True,
'Direction': 'CW',
'HandleMultipleFeatures': 'Collectively',
'InverseAngle': False,
'JoinType': 'Round',
'LimitDepthToFace': True,
'MiterLimit': 0.1,
'OffsetExtra': 0.0,
'ReverseDirection': False,
'Side': 'Outside',
'UseComp': True,
'processCircles': False,
@@ -186,7 +173,6 @@ class ObjectProfile(PathAreaOp.ObjectOp):
'''setOpEditorProperties(obj, porp) ... Process operation-specific changes to properties visibility.'''
fc = 2
# ml = 0 if obj.JoinType == 'Miter' else 2
rotation = 2 if obj.EnableRotation == 'Off' else 0
side = 0 if obj.UseComp else 2
opType = self._getOperationType(obj)
@@ -199,18 +185,12 @@ class ObjectProfile(PathAreaOp.ObjectOp):
obj.setEditorMode('JoinType', 2)
obj.setEditorMode('MiterLimit', 2) # ml
obj.setEditorMode('Side', side)
obj.setEditorMode('HandleMultipleFeatures', fc)
obj.setEditorMode('processCircles', fc)
obj.setEditorMode('processHoles', fc)
obj.setEditorMode('processPerimeter', fc)
obj.setEditorMode('ReverseDirection', rotation)
obj.setEditorMode('InverseAngle', rotation)
obj.setEditorMode('AttemptInverseAngle', rotation)
obj.setEditorMode('LimitDepthToFace', rotation)
def _getOperationType(self, obj):
if len(obj.Base) == 0:
return 'Contour'
@@ -228,7 +208,7 @@ class ObjectProfile(PathAreaOp.ObjectOp):
def areaOpOnChanged(self, obj, prop):
'''areaOpOnChanged(obj, prop) ... updates certain property visibilities depending on changed properties.'''
if prop in ['UseComp', 'JoinType', 'EnableRotation', 'Base']:
if prop in ['UseComp', 'JoinType', 'Base']:
if hasattr(self, 'propertiesReady') and self.propertiesReady:
self.setOpEditorProperties(obj)
@@ -295,10 +275,7 @@ class ObjectProfile(PathAreaOp.ObjectOp):
PathLog.track()
shapes = []
baseSubsTuples = list()
allTuples = list()
remainingObjBaseFeatures = list()
subCount = 0
self.isDebug = True if PathLog.getLevel(PathLog.thisModule()) == 4 else False
self.inaccessibleMsg = translate('PathProfile', 'The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.')
self.offsetExtra = obj.OffsetExtra.Value
@@ -330,39 +307,7 @@ class ObjectProfile(PathAreaOp.ObjectOp):
# Edges were already processed, or whole model targeted.
PathLog.debug("remainingObjBaseFeatures is False")
elif remainingObjBaseFeatures and len(remainingObjBaseFeatures) > 0: # Process remaining features after edges processed above.
if obj.EnableRotation != 'Off':
for p in range(0, len(remainingObjBaseFeatures)):
(base, subsList) = remainingObjBaseFeatures[p]
for sub in subsList:
subCount += 1
shape = getattr(base.Shape, sub)
if isinstance(shape, Part.Face):
tup = self._analyzeFace(obj, base, sub, shape, subCount)
allTuples.append(tup)
if subCount > 1 and obj.HandleMultipleFeatures == 'Collectively':
msg = translate('PathProfile', "Multiple faces in Base Geometry.") + " "
msg += translate('PathProfile', "Depth settings will be applied to all faces.")
FreeCAD.Console.PrintWarning(msg)
(Tags, Grps) = self.sortTuplesByIndex(allTuples, 2) # return (TagList, GroupList)
subList = []
for o in range(0, len(Tags)):
subList = []
for (base, sub, _, angle, axis, stock) in Grps[o]:
subList.append(sub)
pair = base, subList, angle, axis, stock
baseSubsTuples.append(pair)
# Efor
else:
stock = PathUtils.findParentJob(obj).Stock
for (base, subList) in remainingObjBaseFeatures:
baseSubsTuples.append((base, subList, 0.0, 'X', stock))
# Eif
# for base in remainingObjBaseFeatures:
for (base, subsList, angle, axis, stock) in baseSubsTuples:
for (base, subsList) in remainingObjBaseFeatures:
holes = []
faces = []
faceDepths = []
@@ -383,31 +328,24 @@ class ObjectProfile(PathAreaOp.ObjectOp):
msg = translate('PathProfile', "Found a selected object which is not a face. Ignoring:")
PathLog.warning(msg + " {}".format(ignoreSub))
# Identify initial Start and Final Depths
finDep = obj.FinalDepth.Value
strDep = obj.StartDepth.Value
for baseShape, wire in holes:
cont = False
f = Part.makeFace(wire, 'Part::FaceMakerSimple')
drillable = PathUtils.isDrillable(baseShape, wire)
ot = self._openingType(obj, baseShape, f, strDep, finDep)
if obj.processCircles:
if drillable:
if ot < 1:
cont = True
cont = True
if obj.processHoles:
if not drillable:
if ot < 1:
cont = True
cont = True
if cont:
shapeEnv = PathUtils.getEnvelope(baseShape, subshape=f, depthparams=self.depthparams)
if shapeEnv:
self._addDebugObject('HoleShapeEnvelope', shapeEnv)
# env = PathUtils.getEnvelope(baseShape, subshape=f, depthparams=self.depthparams)
tup = shapeEnv, True, 'pathProfile', angle, axis, strDep, finDep
tup = shapeEnv, True, 'pathProfile'
shapes.append(tup)
if faces and obj.processPerimeter:
@@ -416,11 +354,6 @@ class ObjectProfile(PathAreaOp.ObjectOp):
cont = True
profileshape = Part.makeCompound(faces)
if obj.LimitDepthToFace is True and obj.EnableRotation != 'Off':
if profileshape.BoundBox.ZMin > obj.FinalDepth.Value:
finDep = profileshape.BoundBox.ZMin
custDepthparams = self._customDepthParams(obj, strDep + 0.5, finDep) # only an envelope
try:
shapeEnv = PathUtils.getEnvelope(profileshape, depthparams=custDepthparams)
except Exception as ee: # pylint: disable=broad-except
@@ -431,18 +364,17 @@ class ObjectProfile(PathAreaOp.ObjectOp):
if cont:
self._addDebugObject('CollectCutShapeEnv', shapeEnv)
tup = shapeEnv, False, 'pathProfile', angle, axis, strDep, finDep
tup = shapeEnv, False, 'pathProfile'
shapes.append(tup)
elif obj.HandleMultipleFeatures == 'Individually':
for shape in faces:
finalDep = obj.FinalDepth.Value
custDepthparams = self.depthparams
self._addDebugObject('Rotation_Indiv_Shp', shape)
self._addDebugObject('Indiv_Shp', shape)
shapeEnv = PathUtils.getEnvelope(shape, depthparams=custDepthparams)
if shapeEnv:
self._addDebugObject('IndivCutShapeEnv', shapeEnv)
tup = shapeEnv, False, 'pathProfile', angle, axis, strDep, finalDep
tup = shapeEnv, False, 'pathProfile'
shapes.append(tup)
else: # Try to build targets from the job models
@@ -461,7 +393,7 @@ class ObjectProfile(PathAreaOp.ObjectOp):
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, 'pathProfile', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value
tup = env, True, 'pathProfile'
shapes.append(tup)
# Process perimeter if requested by user
@@ -470,7 +402,7 @@ class ObjectProfile(PathAreaOp.ObjectOp):
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, 'pathProfile', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value
tup = env, False, 'pathProfile'
shapes.append(tup)
else:
# shapes.extend([(PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthparams), False) for base in self.model if hasattr(base, 'Shape')])
@@ -492,93 +424,6 @@ class ObjectProfile(PathAreaOp.ObjectOp):
return shapes
# Analyze a face for rotational needs
def _analyzeFace(self, obj, base, sub, shape, subCount):
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:
# Rotational alignment is suggested from analysis
(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))
PathLog.debug("praAngle: {}".format(praAngle))
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.AttemptInverseAngle is True:
PathLog.debug(translate("Path", "Applying inverse angle automatically."))
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
else:
if obj.InverseAngle:
PathLog.debug(translate("Path", "Applying inverse angle manually."))
(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
return tup
def _openingType(self, obj, baseShape, face, strDep, finDep):
# Test if solid geometry above opening
extDistPos = strDep - face.BoundBox.ZMin
if extDistPos > 0:
extFacePos = face.extrude(FreeCAD.Vector(0.0, 0.0, extDistPos))
cmnPos = baseShape.common(extFacePos)
if cmnPos.Volume > 0:
# Signifies solid protrusion above,
# or overhang geometry above opening
return 1
# Test if solid geometry below opening
extDistNeg = finDep - face.BoundBox.ZMin
if extDistNeg < 0:
extFaceNeg = face.extrude(FreeCAD.Vector(0.0, 0.0, extDistNeg))
cmnNeg = baseShape.common(extFaceNeg)
if cmnNeg.Volume == 0:
# No volume below signifies
# an unobstructed/nonconstricted opening through baseShape
return 0
else:
# Could be a pocket,
# or a constricted/narrowing hole through baseShape
return -1
msg = translate('PathProfile', 'failed to return opening type.')
PathLog.debug('_openingType() ' + msg)
return -2
# Method to handle each model as a whole, when no faces are selected
def _processEachModel(self, obj):
shapeTups = list()
@@ -628,7 +473,7 @@ class ObjectProfile(PathAreaOp.ObjectOp):
if f:
shapeEnv = PathUtils.getEnvelope(Part.Face(f), depthparams=self.depthparams)
if shapeEnv:
tup = shapeEnv, False, 'Profile', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value
tup = shapeEnv, False, 'pathProfile'
shapes.append(tup)
else:
PathLog.error(self.inaccessibleMsg)
@@ -662,7 +507,7 @@ class ObjectProfile(PathAreaOp.ObjectOp):
PathLog.error(self.inaccessibleMsg)
if openEdges:
tup = openEdges, False, 'OpenEdge', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value
tup = openEdges, False, 'OpenEdge'
shapes.append(tup)
else:
if zDiff < self.JOB.GeometryTolerance.Value:

View File

@@ -77,8 +77,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
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()
@@ -100,7 +98,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
self.selectInComboBox(obj.Side, self.form.cutSide)
self.selectInComboBox(obj.Direction, self.form.direction)
self.form.extraOffset.setText(FreeCAD.Units.Quantity(obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString)
self.selectInComboBox(obj.EnableRotation, self.form.enableRotation)
self.form.useCompensation.setChecked(obj.UseComp)
self.form.useStartPoint.setChecked(obj.UseStartPoint)
@@ -118,7 +115,6 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
signals.append(self.form.cutSide.currentIndexChanged)
signals.append(self.form.direction.currentIndexChanged)
signals.append(self.form.extraOffset.editingFinished)
signals.append(self.form.enableRotation.currentIndexChanged)
signals.append(self.form.useCompensation.stateChanged)
signals.append(self.form.useStartPoint.stateChanged)
signals.append(self.form.processHoles.stateChanged)

View File

@@ -195,12 +195,6 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
obj.addProperty("App::PropertyLink", "ClearanceOp", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Operation to clear the inside of the thread"))
obj.Direction = self.Directions
# Rotation related properties
if not hasattr(obj, 'EnableRotation'):
obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis."))
obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B']
def threadStartDepth(self, obj):
if obj.ThreadOrientation == self.RightHand:
if obj.Direction == self.DirectionClimb:

View File

@@ -251,8 +251,6 @@ class ObjectWaterline(PathOp.ObjectOp):
# Used to hide inputs in properties list
expMode = G = 0
show = hide = A = B = C = 2
if hasattr(obj, 'EnableRotation'):
obj.setEditorMode('EnableRotation', hide)
obj.setEditorMode('BoundaryEnforcement', hide)
obj.setEditorMode('InternalFeaturesAdjustment', hide)