Merge branch 'master' of github.com:FreeCAD/FreeCAD
This commit is contained in:
@@ -119,8 +119,10 @@ def make_label(target_point=App.Vector(0, 0, 0),
|
||||
- `'Area'` will show the `Area` of the target object's `Shape`,
|
||||
or of the indicated `'FaceN'` in `target`.
|
||||
|
||||
custom_text: str, optional
|
||||
custom_text: str, or list of str, optional
|
||||
It defaults to `'Label'`.
|
||||
If it is a list, each element in the list represents a new text line.
|
||||
|
||||
It is the text that will be displayed by the label when
|
||||
`label_type` is `'Custom'`.
|
||||
|
||||
@@ -280,9 +282,14 @@ def make_label(target_point=App.Vector(0, 0, 0),
|
||||
if not custom_text:
|
||||
custom_text = "Label"
|
||||
try:
|
||||
utils.type_check([(custom_text, str)], name=_name)
|
||||
utils.type_check([(custom_text, (str, list))], name=_name)
|
||||
except TypeError:
|
||||
_err(translate("draft","Wrong input: must be a string."))
|
||||
_err(translate("draft","Wrong input: must be a list of strings or a single string."))
|
||||
return None
|
||||
|
||||
if (type(custom_text) is list
|
||||
and not all(isinstance(element, str) for element in custom_text)):
|
||||
_err(translate("draft","Wrong input: must be a list of strings or a single string."))
|
||||
return None
|
||||
|
||||
_msg("direction: {}".format(direction))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
; Graphite
|
||||
; Uwe Stöhr, LGPL
|
||||
; Uwe Stöhr, LGPL
|
||||
; information about the content of such cards can be found on the wiki:
|
||||
; https://www.freecadweb.org/wiki/Material
|
||||
; https://www.freecadweb.org/wiki/Material
|
||||
; file created by FreeCAD 0.20.24516 (Git)
|
||||
|
||||
[General]
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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 & B</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -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><html><head/><body><p>The amount of extra material left by this operation in relation to the target shape.</p></body></html></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><html><head/><body><p>The amount of extra material left by this operation in relation to the target shape.</p></body></html></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 & B</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user