Merge pull request #2231 from Russ4262/pathpocketshape_fix
Path: PocketShape - Fix repositioning of Job model; ProfileFaces - add 4th-axis feature
This commit is contained in:
@@ -21,22 +21,31 @@
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Additional modifications and contributions beginning 2019 *
|
||||
# * Focus: 4th-axis integration *
|
||||
# * by Russell Johnson <russ4262@gmail.com> *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
# SCRIPT NOTES:
|
||||
# - Need to add "UseRotation" property to task window UI, and attach appropriate onChange event handler
|
||||
# - Consult FC community about wording for "UseRotation" property
|
||||
# - FUTURE: Relocate rotational calculations to Job setup tool, creating a Machine section
|
||||
# with axis & rotation toggles and associated min/max values
|
||||
# with axis & rotation toggles and associated min/max values
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathLog as PathLog
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
import PathScripts.PathGeom as PathGeom
|
||||
import Draft
|
||||
import math
|
||||
|
||||
# from PathScripts.PathUtils import waiting_effects
|
||||
from PySide import QtCore
|
||||
import math
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
|
||||
|
||||
__title__ = "Base class for PathArea based operations."
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
@@ -44,8 +53,8 @@ __url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Base class and properties for Path.Area based operations."
|
||||
__contributors__ = "mlampert [FreeCAD], russ4262 (Russell Johnson)"
|
||||
__createdDate__ = "2017"
|
||||
__scriptVersion__ = "1h testing"
|
||||
__lastModified__ = "2019-05-03 10:52 CST"
|
||||
__scriptVersion__ = "2g testing"
|
||||
__lastModified__ = "2019-06-13 15:37 CST"
|
||||
|
||||
if False:
|
||||
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
|
||||
@@ -53,8 +62,9 @@ if False:
|
||||
else:
|
||||
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
|
||||
|
||||
|
||||
# Qt translation handling
|
||||
|
||||
|
||||
def translate(context, text, disambig=None):
|
||||
return QtCore.QCoreApplication.translate(context, text, disambig)
|
||||
|
||||
@@ -68,15 +78,17 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
operations.'''
|
||||
|
||||
# These are static while document is open, if it contains a 3D Surface Op
|
||||
initFinalDepth = None
|
||||
initOpFinalDepth = None
|
||||
initOpStartDepth = None
|
||||
initWithRotation = False
|
||||
defValsSet = False
|
||||
docRestored = False
|
||||
|
||||
def opFeatures(self, obj):
|
||||
'''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.FeatureRotation
|
||||
return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureStepDown | PathOp.FeatureHeights | PathOp.FeatureStartPoint | self.areaOpFeatures(obj)
|
||||
|
||||
def areaOpFeatures(self, obj):
|
||||
@@ -96,30 +108,32 @@ 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, 'UseRotation'):
|
||||
obj.addProperty("App::PropertyEnumeration", "UseRotation", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Use rotation to gain access to pockets/areas."))
|
||||
obj.UseRotation = ['Off', 'A(x)', 'B(y)', 'A & B']
|
||||
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.'''
|
||||
pass
|
||||
|
||||
def areaOpShapeForDepths(self, obj):
|
||||
def areaOpShapeForDepths(self, obj, job):
|
||||
'''areaOpShapeForDepths(obj) ... returns the shape used to make an initial calculation for the depths being used.
|
||||
The default implementation returns the job's Base.Shape'''
|
||||
job = PathUtils.findParentJob(obj)
|
||||
if job and job.Base:
|
||||
PathLog.debug("job=%s base=%s shape=%s" % (job, job.Base, job.Base.Shape))
|
||||
return job.Base.Shape
|
||||
if job:
|
||||
PathLog.warning(translate("PathAreaOp", "job %s has no Base.") % job.Label)
|
||||
if job.Stock:
|
||||
PathLog.debug("job=%s base=%s shape=%s" % (job, job.Stock, job.Stock.Shape))
|
||||
return job.Stock.Shape
|
||||
else:
|
||||
PathLog.warning(translate("PathAreaOp", "job %s has no Base.") % job.Label)
|
||||
else:
|
||||
PathLog.warning(translate("PathAreaOp", "no job for op %s found.") % obj.Label)
|
||||
return None
|
||||
@@ -157,10 +171,13 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
if hasattr(obj, prop):
|
||||
obj.setEditorMode(prop, 2)
|
||||
|
||||
self.setupAdditionalProperties(obj)
|
||||
|
||||
self.initOpFinalDepth = obj.OpFinalDepth.Value
|
||||
self.initOpStartDepth = obj.OpStartDepth.Value
|
||||
self.docRestored = True
|
||||
# PathLog.debug("Imported existing OpFinalDepth of " + str(self.initOpFinalDepth) + " for recompute() purposes.")
|
||||
# PathLog.debug("Imported existing StartDepth of " + str(self.initOpStartDepth) + " for recompute() purposes.")
|
||||
|
||||
self.setupAdditionalProperties(obj)
|
||||
self.areaOpOnDocumentRestored(obj)
|
||||
|
||||
def areaOpOnDocumentRestored(self, obj):
|
||||
@@ -173,45 +190,73 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
areaOpShapeForDepths() return value.
|
||||
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)
|
||||
except:
|
||||
shape = self.areaOpShapeForDepths(obj, job)
|
||||
except Exception as ee:
|
||||
PathLog.error(ee)
|
||||
shape = None
|
||||
|
||||
maxDep = 1.0
|
||||
minDep = 0.0
|
||||
|
||||
if obj.UseRotation == 'Off':
|
||||
bb = job.Stock.Shape.BoundBox
|
||||
maxDep = bb.ZMax
|
||||
minDep = bb.ZMin
|
||||
# Set initial start and final depths
|
||||
if shape is None:
|
||||
PathLog.debug("shape is None")
|
||||
startDepth = 1.0
|
||||
finalDepth = 0.0
|
||||
else:
|
||||
opHeights = self.opDetermineRotationRadii(obj) # return is list with tuples [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfst)]
|
||||
(xRotRad, yRotRad, zRotRad) = opHeights[0]
|
||||
# (clrOfst, safOfset) = opHeights[1]
|
||||
bb = job.Stock.Shape.BoundBox
|
||||
startDepth = bb.ZMax
|
||||
finalDepth = bb.ZMin
|
||||
|
||||
maxDep = xRotRad
|
||||
if yRotRad > xRotRad:
|
||||
maxDep = yRotRad
|
||||
minDep = -1 * maxDep
|
||||
# Adjust start and final depths if rotation is enabled
|
||||
if obj.EnableRotation != 'Off':
|
||||
self.initWithRotation = True
|
||||
self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox
|
||||
# Calculate rotational distances/radii
|
||||
opHeights = self.opDetermineRotationRadii(obj) # return is list with tuples [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfset)]
|
||||
(xRotRad, yRotRad, zRotRad) = opHeights[0]
|
||||
# (self.safOfset, self.safOfst) = opHeights[1]
|
||||
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
|
||||
|
||||
# Manage operation start and final depths
|
||||
if self.docRestored is True:
|
||||
PathLog.debug("doc restored")
|
||||
if self.docRestored is True: # This op is NOT the first in the Operations list
|
||||
PathLog.debug("Doc restored")
|
||||
obj.FinalDepth.Value = obj.OpFinalDepth.Value
|
||||
obj.StartDepth.Value = obj.OpStartDepth.Value
|
||||
else:
|
||||
PathLog.debug("new operation")
|
||||
obj.OpFinalDepth.Value = minDep
|
||||
obj.OpStartDepth.Value = maxDep
|
||||
if self.initOpFinalDepth is None and self.initFinalDepth is None:
|
||||
self.initFinalDepth = minDep
|
||||
self.initOpFinalDepth = minDep
|
||||
else:
|
||||
PathLog.debug("-initFinalDepth" + str(self.initFinalDepth))
|
||||
PathLog.debug("-initOpFinalDepth" + str(self.initOpFinalDepth))
|
||||
PathLog.debug("New operation")
|
||||
obj.StartDepth.Value = startDepth
|
||||
obj.FinalDepth.Value = finalDepth
|
||||
obj.OpStartDepth.Value = startDepth
|
||||
obj.OpFinalDepth.Value = finalDepth
|
||||
|
||||
obj.UseRotation = 'Off'
|
||||
if obj.EnableRotation != 'Off':
|
||||
if self.initOpFinalDepth is None:
|
||||
self.initOpFinalDepth = finalDepth
|
||||
PathLog.debug("Saved self.initOpFinalDepth")
|
||||
if self.initOpStartDepth is None:
|
||||
self.initOpStartDepth = startDepth
|
||||
PathLog.debug("Saved self.initOpStartDepth")
|
||||
self.defValsSet = True
|
||||
PathLog.debug("Default OpDepths are Start: {}, and Final: {}".format(obj.OpStartDepth.Value, obj.OpFinalDepth.Value))
|
||||
PathLog.debug("Default Depths are Start: {}, and Final: {}".format(startDepth, finalDepth))
|
||||
|
||||
self.areaOpSetDefaultValues(obj, job)
|
||||
|
||||
@@ -287,15 +332,71 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
areaOpUseProjection(obj) ... return true if operation can use projection
|
||||
instead.'''
|
||||
PathLog.track()
|
||||
self.endVector = None
|
||||
PathLog.debug("opExecute() in PathAreaOp.py")
|
||||
PathLog.info("\n----- opExecute() in PathAreaOp.py")
|
||||
# PathLog.debug("OpDepths are Start: {}, and Final: {}".format(obj.OpStartDepth.Value, obj.OpFinalDepth.Value))
|
||||
# PathLog.debug("Depths are Start: {}, and Final: {}".format(obj.StartDepth.Value, obj.FinalDepth.Value))
|
||||
# PathLog.debug("initOpDepths are Start: {}, and Final: {}".format(self.initOpStartDepth, self.initOpFinalDepth))
|
||||
|
||||
# Instantiate class variables for operation reference
|
||||
self.endVector = None
|
||||
self.rotateFlag = False
|
||||
self.modelName = None
|
||||
self.leadIn = 2.0 # safOfset / 2.0
|
||||
self.leadIn = 2.0 # self.safOfst / 2.0
|
||||
self.cloneNames = []
|
||||
self.guiMsgs = [] # list of message tuples (title, msg) to be displayed in GUI
|
||||
self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox
|
||||
self.useTempJobClones('Delete') # Clear temporary group and recreate for temp job clones
|
||||
|
||||
# Initialize depthparams
|
||||
# Import OpFinalDepth from pre-existing operation for recompute() scenarios
|
||||
if self.defValsSet is True:
|
||||
PathLog.debug("self.defValsSet is True.")
|
||||
if self.initOpStartDepth is not None:
|
||||
if self.initOpStartDepth != obj.OpStartDepth.Value:
|
||||
obj.OpStartDepth.Value = self.initOpStartDepth
|
||||
obj.StartDepth.Value = self.initOpStartDepth
|
||||
|
||||
if self.initOpFinalDepth is not None:
|
||||
if self.initOpFinalDepth != obj.OpFinalDepth.Value:
|
||||
obj.OpFinalDepth.Value = self.initOpFinalDepth
|
||||
obj.FinalDepth.Value = self.initOpFinalDepth
|
||||
self.defValsSet = False
|
||||
|
||||
if obj.EnableRotation != 'Off':
|
||||
# Calculate operation heights based upon rotation radii
|
||||
opHeights = self.opDetermineRotationRadii(obj)
|
||||
(self.xRotRad, self.yRotRad, self.zRotRad) = opHeights[0]
|
||||
(self.safOfset, self.safOfst) = opHeights[1]
|
||||
|
||||
# Set clearnance and safe heights based upon rotation radii
|
||||
if obj.EnableRotation == 'A(x)':
|
||||
self.strDep = self.xRotRad
|
||||
elif obj.EnableRotation == 'B(y)':
|
||||
self.strDep = self.yRotRad
|
||||
else:
|
||||
self.strDep = max(self.xRotRad, self.yRotRad)
|
||||
self.finDep = -1 * self.strDep
|
||||
|
||||
obj.ClearanceHeight.Value = self.strDep + self.safOfset
|
||||
obj.SafeHeight.Value = self.strDep + self.safOfst
|
||||
|
||||
if self.initWithRotation is False:
|
||||
if obj.FinalDepth.Value == obj.OpFinalDepth.Value:
|
||||
obj.FinalDepth.Value = self.finDep
|
||||
if obj.StartDepth.Value == obj.OpStartDepth.Value:
|
||||
obj.StartDepth.Value = self.strDep
|
||||
|
||||
# Create visual axises when debugging.
|
||||
if PathLog.getLevel(PathLog.thisModule()) == 4:
|
||||
self.visualAxis()
|
||||
else:
|
||||
self.strDep = obj.StartDepth.Value
|
||||
self.finDep = obj.FinalDepth.Value
|
||||
|
||||
# Set axial feed rates based upon horizontal feed rates
|
||||
safeCircum = 2 * math.pi * obj.SafeHeight.Value
|
||||
self.axialFeed = 360 / safeCircum * self.horizFeed
|
||||
self.axialRapid = 360 / safeCircum * self.horizRapid
|
||||
|
||||
# Initiate depthparams and calculate operation heights for rotational operation
|
||||
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
||||
self.depthparams = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
@@ -306,40 +407,21 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
final_depth=obj.FinalDepth.Value,
|
||||
user_depths=None)
|
||||
|
||||
# Recalculate operation heights for rotational operation
|
||||
if obj.UseRotation != 'Off':
|
||||
# Calculate operation heights based upon rotation radii
|
||||
opHeights = self.opDetermineRotationRadii(obj) # return is [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfst)]
|
||||
(xRotRad, yRotRad, zRotRad) = opHeights[0]
|
||||
(clrOfst, safOfset) = opHeights[1]
|
||||
# self.leadIn = 0.0 #safOfset / 2.0
|
||||
|
||||
# Set clearnance and safe heights based upon rotation radii
|
||||
obj.ClearanceHeight.Value = xRotRad + clrOfst
|
||||
obj.SafeHeight.Value = xRotRad + safOfset
|
||||
if yRotRad > xRotRad:
|
||||
obj.ClearanceHeight.Value = yRotRad + clrOfst
|
||||
obj.SafeHeight.Value = yRotRad + safOfset
|
||||
|
||||
# Set axial feed rates based upon horizontal feed rates
|
||||
safeCircum = 2 * math.pi * obj.SafeHeight.Value
|
||||
self.axialFeed = 360 / safeCircum * self.horizFeed
|
||||
self.axialRapid = 360 / safeCircum * self.horizRapid
|
||||
|
||||
# Set start point
|
||||
if PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint:
|
||||
start = obj.StartPoint
|
||||
else:
|
||||
start = None
|
||||
|
||||
aOS = self.areaOpShapes(obj) # list of tuples (shape, isHole, sub, angle, axis, tag)
|
||||
aOS = self.areaOpShapes(obj) # list of tuples (shape, isHole, sub, angle, axis)
|
||||
|
||||
# Adjust tuples length received from other PathWB tools/operations beside PathPocketShape
|
||||
shapes = []
|
||||
for shp in aOS:
|
||||
if len(shp) == 2:
|
||||
(fc, iH) = shp
|
||||
tup = fc, iH, 'notPocket', 0.0, 'X'
|
||||
# fc, iH, sub, angle, axis
|
||||
tup = fc, iH, 'otherOp', 0.0, 'S', obj.StartDepth.Value, obj.FinalDepth.Value
|
||||
shapes.append(tup)
|
||||
else:
|
||||
shapes.append(shp)
|
||||
@@ -355,45 +437,69 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
|
||||
shapes = [j['shape'] for j in jobs]
|
||||
|
||||
# PathLog.debug("Pre_path depths are Start: {}, and Final: {}".format(obj.StartDepth.Value, obj.FinalDepth.Value))
|
||||
sims = []
|
||||
for (shape, isHole, sub, angle, axis) in shapes:
|
||||
startDep = obj.StartDepth.Value # + safOfset
|
||||
safeDep = obj.SafeHeight.Value
|
||||
clearDep = obj.ClearanceHeight.Value
|
||||
finalDep = obj.FinalDepth.Value # finDep
|
||||
numShapes = len(shapes)
|
||||
|
||||
if numShapes == 1:
|
||||
nextAxis = shapes[0][4]
|
||||
elif numShapes > 1:
|
||||
nextAxis = shapes[1][4]
|
||||
else:
|
||||
nextAxis = 'L'
|
||||
|
||||
for ns in range(0, numShapes):
|
||||
(shape, isHole, sub, angle, axis, strDep, finDep) = shapes[ns]
|
||||
if ns < numShapes - 1:
|
||||
nextAxis = shapes[ns + 1][4]
|
||||
else:
|
||||
nextAxis = 'L'
|
||||
|
||||
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
||||
self.depthparams = PathUtils.depth_params(
|
||||
clearance_height=clearDep, # obj.ClearanceHeight.Value
|
||||
safe_height=safeDep, # obj.SafeHeight.Value
|
||||
start_depth=startDep,
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=strDep, # obj.StartDepth.Value,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step, # obj.FinalDepth.Value
|
||||
final_depth=finalDep,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=finDep, # obj.FinalDepth.Value,
|
||||
user_depths=None)
|
||||
|
||||
try:
|
||||
(pp, sim) = self._buildPathArea(obj, shape, isHole, start, getsim)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintError(e)
|
||||
FreeCAD.Console.PrintError("Something unexpected happened. Check project and tool config.")
|
||||
else:
|
||||
ppCmds = pp.Commands
|
||||
if obj.UseRotation != 'Off' and self.rotateFlag is True:
|
||||
if obj.EnableRotation != 'Off' and self.rotateFlag is True:
|
||||
# Rotate model to index for cut
|
||||
axisOfRot = 'A'
|
||||
if axis == 'Y':
|
||||
if axis == 'X':
|
||||
axisOfRot = 'A'
|
||||
elif axis == 'Y':
|
||||
axisOfRot = 'B'
|
||||
# Reverse angle temporarily to match model. Error in FreeCAD render of B axis rotations
|
||||
if obj.B_AxisErrorOverride is True:
|
||||
angle = -1 * angle
|
||||
elif axis == 'Z':
|
||||
axisOfRot = 'C'
|
||||
else:
|
||||
axisOfRot = 'A'
|
||||
# Rotate Model to correct angle
|
||||
ppCmds.insert(0, Path.Command('G0', {axisOfRot: angle, 'F': self.axialFeed}))
|
||||
ppCmds.insert(0, Path.Command('G1', {axisOfRot: angle, 'F': self.axialFeed}))
|
||||
ppCmds.insert(0, Path.Command('N100', {}))
|
||||
|
||||
# Raise cutter to safe depth and return index to starting position
|
||||
ppCmds.append(Path.Command('G0', {'Z': safeDep, 'F': self.vertRapid}))
|
||||
ppCmds.append(Path.Command('G0', {axisOfRot: 0.0, 'F': self.axialFeed}))
|
||||
ppCmds.append(Path.Command('N200', {}))
|
||||
ppCmds.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
|
||||
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)
|
||||
sims.append(sim)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintError(e)
|
||||
FreeCAD.Console.PrintError("Something unexpected happened. Check project and tool config.")
|
||||
# Eif
|
||||
|
||||
if self.areaOpRetractTool(obj):
|
||||
self.endVector = None
|
||||
@@ -401,10 +507,11 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
# Raise cutter to safe height and rotate back to original orientation
|
||||
if self.rotateFlag is True:
|
||||
self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
|
||||
self.commandlist.append(Path.Command('G0', {'A': 0.0, 'F': self.axialFeed}))
|
||||
self.commandlist.append(Path.Command('G0', {'B': 0.0, 'F': self.axialFeed}))
|
||||
FreeCAD.ActiveDocument.getObject(self.modelName).purgeTouched()
|
||||
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
|
||||
PathLog.debug("obj.Name: " + str(obj.Name))
|
||||
return sims
|
||||
|
||||
@@ -435,34 +542,34 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
return False
|
||||
|
||||
def opDetermineRotationRadii(self, obj):
|
||||
'''opDetermineRotationRadii(self, obj)
|
||||
'''opDetermineRotationRadii(obj)
|
||||
Determine rotational radii for 4th-axis rotations, for clearance/safe heights '''
|
||||
|
||||
parentJob = PathUtils.findParentJob(obj)
|
||||
bb = parentJob.Stock.Shape.BoundBox
|
||||
# bb = parentJob.Stock.Shape.BoundBox
|
||||
xlim = 0.0
|
||||
ylim = 0.0
|
||||
zlim = 0.0
|
||||
|
||||
# Determine boundbox radius based upon xzy limits data
|
||||
if math.fabs(bb.ZMin) > math.fabs(bb.ZMax):
|
||||
zlim = bb.ZMin
|
||||
if math.fabs(self.stockBB.ZMin) > math.fabs(self.stockBB.ZMax):
|
||||
zlim = self.stockBB.ZMin
|
||||
else:
|
||||
zlim = bb.ZMax
|
||||
zlim = self.stockBB.ZMax
|
||||
|
||||
if obj.UseRotation != 'B(y)':
|
||||
if obj.EnableRotation != 'B(y)':
|
||||
# Rotation is around X-axis, cutter moves along same axis
|
||||
if math.fabs(bb.YMin) > math.fabs(bb.YMax):
|
||||
ylim = bb.YMin
|
||||
if math.fabs(self.stockBB.YMin) > math.fabs(self.stockBB.YMax):
|
||||
ylim = self.stockBB.YMin
|
||||
else:
|
||||
ylim = bb.YMax
|
||||
ylim = self.stockBB.YMax
|
||||
|
||||
if obj.UseRotation != 'A(x)':
|
||||
if obj.EnableRotation != 'A(x)':
|
||||
# Rotation is around Y-axis, cutter moves along same axis
|
||||
if math.fabs(bb.XMin) > math.fabs(bb.XMax):
|
||||
xlim = bb.XMin
|
||||
if math.fabs(self.stockBB.XMin) > math.fabs(self.stockBB.XMax):
|
||||
xlim = self.stockBB.XMin
|
||||
else:
|
||||
xlim = bb.XMax
|
||||
xlim = self.stockBB.XMax
|
||||
|
||||
xRotRad = math.sqrt(ylim**2 + zlim**2)
|
||||
yRotRad = math.sqrt(xlim**2 + zlim**2)
|
||||
@@ -473,41 +580,42 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
|
||||
return [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfst)]
|
||||
|
||||
def pocketRotationAnalysis(self, obj, objRef, sub, prnt):
|
||||
'''pocketRotationAnalysis(self, obj, objRef, sub, prnt)
|
||||
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()
|
||||
|
||||
rtn = False
|
||||
praInfo = "faceRotationAnalysis() in PathAreaOp.py"
|
||||
rtn = True
|
||||
axis = 'X'
|
||||
orientation = 'X'
|
||||
angle = 500.0
|
||||
zTol = 1.0E-9
|
||||
rndTol = 1.0 - zTol
|
||||
testId = "pocketRotationAnalysis() in PathAreaOp.py"
|
||||
precision = 6
|
||||
|
||||
def roundRoughValues(val, zTol, rndTol):
|
||||
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 math.fabs(val) <= zTol:
|
||||
if PathGeom.isRoughly(0.0, val) is True:
|
||||
return 0.0
|
||||
# Convert VAL.99999999 to next integer
|
||||
elif math.fabs(val % 1) > rndTol:
|
||||
elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance:
|
||||
return round(val)
|
||||
else:
|
||||
return val
|
||||
return round(val, precision)
|
||||
|
||||
face = objRef.Shape.getElement(sub)
|
||||
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)
|
||||
|
||||
norm = face.normalAt(0, 0)
|
||||
nX = roundRoughValues(norm.x, zTol, rndTol)
|
||||
nY = roundRoughValues(norm.y, zTol, rndTol)
|
||||
nZ = roundRoughValues(norm.z, zTol, rndTol)
|
||||
testId += "\n -normalAt(0,0): " + str(nX) + ", " + str(nY) + ", " + str(nZ)
|
||||
|
||||
surf = face.Surface.Axis
|
||||
saX = roundRoughValues(surf.x, zTol, rndTol)
|
||||
saY = roundRoughValues(surf.y, zTol, rndTol)
|
||||
saZ = roundRoughValues(surf.z, zTol, rndTol)
|
||||
testId += "\n -Surface.Axis: " + str(saX) + ", " + str(saY) + ", " + str(saZ)
|
||||
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:
|
||||
@@ -518,7 +626,7 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
elif saZ == -1.0:
|
||||
angle = -180.0
|
||||
else:
|
||||
testId += "_else_X" + str(saZ)
|
||||
praInfo += "_else_X" + str(saZ)
|
||||
elif saY == 1.0:
|
||||
orientation = "Y"
|
||||
angle = 90.0
|
||||
@@ -537,16 +645,16 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
elif saX == -1.0:
|
||||
angle = 90.0
|
||||
else:
|
||||
testId += "_else_X" + str(saX)
|
||||
praInfo += "_else_X" + str(saX)
|
||||
else:
|
||||
orientation = "X"
|
||||
ratio = saX / saZ
|
||||
angle = math.degrees(math.atan(ratio))
|
||||
if ratio < 0.0:
|
||||
testId += " NEG-ratio"
|
||||
angle -= 90
|
||||
praInfo += " NEG-ratio"
|
||||
# angle -= 90
|
||||
else:
|
||||
testId += " POS-ratio"
|
||||
praInfo += " POS-ratio"
|
||||
angle = -1 * angle
|
||||
if saX < 0.0:
|
||||
angle = angle + 180.0
|
||||
@@ -566,16 +674,23 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
if nX != 0.0:
|
||||
angle = -1 * angle
|
||||
|
||||
# Enforce enabled rotation in settings
|
||||
if orientation == 'Y':
|
||||
axis = 'X'
|
||||
if obj.UseRotation == 'B(y)': # Axis disabled
|
||||
angle = 500.0
|
||||
if obj.EnableRotation == 'B(y)': # Required axis disabled
|
||||
rtn = False
|
||||
else:
|
||||
axis = 'Y'
|
||||
if obj.UseRotation == 'A(x)': # Axis disabled
|
||||
angle = 500.0
|
||||
if obj.EnableRotation == 'A(x)': # Required axis disabled
|
||||
rtn = False
|
||||
|
||||
if angle != 500.0 and angle != 0.0:
|
||||
if angle == 500.0:
|
||||
rtn = False
|
||||
|
||||
if angle == 0.0:
|
||||
rtn = False
|
||||
|
||||
if rtn is True:
|
||||
self.rotateFlag = True
|
||||
rtn = True
|
||||
if obj.ReverseDirection is True:
|
||||
@@ -583,11 +698,267 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
angle = angle + 180.0
|
||||
else:
|
||||
angle = angle - 180.0
|
||||
testId += "\n - ... rotation triggered"
|
||||
else:
|
||||
testId += "\n - ... NO rotation triggered"
|
||||
angle = round(angle, precision)
|
||||
|
||||
testId += "\n -Suggested rotation: angle: " + str(angle) + ", axis: " + str(axis)
|
||||
if prnt is True:
|
||||
PathLog.debug("testId: " + testId)
|
||||
return (rtn, angle, axis)
|
||||
praInfo += "\n -Rotation analysis: angle: " + str(angle) + ", axis: " + str(axis)
|
||||
if rtn is True:
|
||||
praInfo += "\n - ... rotation triggered"
|
||||
else:
|
||||
praInfo += "\n - ... NO rotation triggered"
|
||||
|
||||
PathLog.debug("\n" + str(praInfo))
|
||||
|
||||
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 FreeCAD.GuiUp and len(self.guiMsgs) > 0:
|
||||
# self.guiMsgs.pop(0) # remove formatted place holder.
|
||||
from PySide.QtGui import QMessageBox
|
||||
# from PySide import QtGui
|
||||
for entry in self.guiMsgs:
|
||||
(title, msg) = entry
|
||||
QMessageBox.warning(None, title, msg)
|
||||
# QtGui.QMessageBox.warning(None, title, msg)
|
||||
self.guiMsgs = [] # Reset messages
|
||||
return True
|
||||
|
||||
# Types: information, warning, critical, question
|
||||
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'
|
||||
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)
|
||||
|
||||
if False:
|
||||
FreeCAD.ActiveDocument.addObject("Part::Cylinder", zAx)
|
||||
cyl = FreeCAD.ActiveDocument.getObject(zAx)
|
||||
cyl.Label = zAx
|
||||
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(zAx)
|
||||
cylGui.ShapeColor = (0.000, 0.000, 0.498)
|
||||
cylGui.Transparency = 85
|
||||
cylGui.Visibility = False
|
||||
vaGrp.addObject(cyl)
|
||||
|
||||
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.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.removeObject(clnNm)
|
||||
if FreeCAD.ActiveDocument.getObject(stckClnNm):
|
||||
FreeCAD.ActiveDocument.removeObject(stckClnNm)
|
||||
FreeCAD.ActiveDocument.addObject('Part::Feature', clnNm).Shape = base.Shape
|
||||
FreeCAD.ActiveDocument.addObject('Part::Feature', stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape
|
||||
if FreeCAD.GuiUp:
|
||||
FreeCADGui.ActiveDocument.getObject(stckClnNm).Transparency = 90
|
||||
FreeCADGui.ActiveDocument.getObject(clnNm).ShapeColor = (1.000, 0.667, 0.000)
|
||||
self.useTempJobClones(clnNm)
|
||||
self.useTempJobClones(stckClnNm)
|
||||
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)
|
||||
|
||||
if obj.InverseAngle is True:
|
||||
angle = -1 * angle
|
||||
|
||||
# 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.info(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 # self.strDep
|
||||
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):
|
||||
'''warnDisabledAxis(self, obj, axis)
|
||||
Provide user feedback if required axis is disabled'''
|
||||
if axis == 'X' and obj.EnableRotation == 'B(y)':
|
||||
PathLog.warning(translate('Path', "Part feature is inaccessible. Selected feature(s) require 'A(x)' for access."))
|
||||
return True
|
||||
elif axis == 'Y' and obj.EnableRotation == 'A(x)':
|
||||
PathLog.warning(translate('Path', "Part feature is inaccessible. Selected feature(s) require 'B(y)' for access."))
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,21 +21,36 @@
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Additional modifications and contributions beginning 2019 *
|
||||
# * Focus: 4th-axis integration *
|
||||
# * by Russell Johnson <russ4262@gmail.com> *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import ArchPanel
|
||||
import FreeCAD
|
||||
import Part
|
||||
import Path
|
||||
import PathScripts.PathAreaOp as PathAreaOp
|
||||
# import PathScripts.PathAreaOp as PathAreaOp
|
||||
import PathScripts.PathLog as PathLog
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathScripts.PathProfileBase as PathProfileBase
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
import numpy
|
||||
|
||||
from PathScripts.PathUtils import depth_params
|
||||
# from PathScripts.PathUtils import depth_params
|
||||
from PySide import QtCore
|
||||
|
||||
__title__ = "Path Profile Faces Operation"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Path Profile operation based on faces."
|
||||
__contributors__ = "russ4262 (Russell Johnson)"
|
||||
__created__ = "2014"
|
||||
__scriptVersion__ = "2g testing"
|
||||
__lastModified__ = "2019-06-12 23:29 CST"
|
||||
|
||||
if False:
|
||||
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
|
||||
PathLog.trackModule(PathLog.thisModule())
|
||||
@@ -43,14 +58,11 @@ else:
|
||||
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
|
||||
|
||||
# Qt translation handling
|
||||
|
||||
|
||||
def translate(context, text, disambig=None):
|
||||
return QtCore.QCoreApplication.translate(context, text, disambig)
|
||||
|
||||
__title__ = "Path Profile Faces Operation"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Path Profile operation based on faces."
|
||||
|
||||
|
||||
class ObjectProfile(PathProfileBase.ObjectProfile):
|
||||
'''Proxy object for Profile operations based on faces.'''
|
||||
@@ -63,6 +75,7 @@ class ObjectProfile(PathProfileBase.ObjectProfile):
|
||||
def areaOpFeatures(self, obj):
|
||||
'''baseObject() ... returns super of receiver
|
||||
Used to call base implementation in overwritten functions.'''
|
||||
# return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels | PathOp.FeatureRotation
|
||||
return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels
|
||||
|
||||
def initAreaOp(self, obj):
|
||||
@@ -72,10 +85,22 @@ class ObjectProfile(PathProfileBase.ObjectProfile):
|
||||
obj.addProperty("App::PropertyBool", "processPerimeter", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the outline"))
|
||||
obj.addProperty("App::PropertyBool", "processCircles", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile round holes"))
|
||||
|
||||
if not hasattr(obj, '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, 'B_AxisErrorOverride'):
|
||||
obj.addProperty('App::PropertyBool', 'B_AxisErrorOverride', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Match B rotations to model (error in FreeCAD rendering).'))
|
||||
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.'))
|
||||
|
||||
self.baseObject().initAreaOp(obj)
|
||||
|
||||
def areaOpShapes(self, obj):
|
||||
'''areaOpShapes(obj) ... returns envelope for all base shapes or wires for Arch.Panels.'''
|
||||
PathLog.track()
|
||||
PathLog.debug("----- areaOpShapes() in PathProfileFaces.py")
|
||||
|
||||
if obj.UseComp:
|
||||
self.commandlist.append(Path.Command("(Compensated Tool Path. Diameter: " + str(self.radius * 2) + ")"))
|
||||
else:
|
||||
@@ -83,39 +108,173 @@ class ObjectProfile(PathProfileBase.ObjectProfile):
|
||||
|
||||
shapes = []
|
||||
self.profileshape = []
|
||||
finalDepths = []
|
||||
|
||||
baseSubsTuples = []
|
||||
subCount = 0
|
||||
allTuples = []
|
||||
|
||||
if obj.Base: # The user has selected subobjects from the base. Process each.
|
||||
for base in obj.Base:
|
||||
if obj.EnableRotation != 'Off':
|
||||
for p in range(0, len(obj.Base)):
|
||||
(base, subsList) = obj.Base[p]
|
||||
for sub in subsList:
|
||||
subCount += 1
|
||||
shape = getattr(base.Shape, sub)
|
||||
if isinstance(shape, Part.Face):
|
||||
rtn = False
|
||||
(norm, surf) = self.getFaceNormAndSurf(shape)
|
||||
(rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf)
|
||||
if rtn is True:
|
||||
(clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount)
|
||||
# Verify faces are correctly oriented - InverseAngle might be necessary
|
||||
faceIA = getattr(clnBase.Shape, sub)
|
||||
(norm, surf) = self.getFaceNormAndSurf(faceIA)
|
||||
(rtn, praAngle, praAxis, praInfo) = self.faceRotationAnalysis(obj, norm, surf)
|
||||
if rtn is True:
|
||||
PathLog.error(translate("Path", "Face appears misaligned after initial rotation."))
|
||||
if obj.AttemptInverseAngle is True and obj.InverseAngle is False:
|
||||
(clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle)
|
||||
else:
|
||||
msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.")
|
||||
PathLog.error(msg)
|
||||
# title = translate("Path", 'Rotation Warning')
|
||||
# self.guiMessage(title, msg, False)
|
||||
else:
|
||||
PathLog.debug("Face appears to be oriented correctly.")
|
||||
|
||||
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
|
||||
# Eif
|
||||
allTuples.append(tup)
|
||||
# Eif
|
||||
# Efor
|
||||
# Efor
|
||||
if subCount > 1:
|
||||
msg = translate('Path', "Multiple faces in Base Geometry.") + " "
|
||||
msg += translate('Path', "Depth settings will be applied to all faces.")
|
||||
PathLog.warning(msg)
|
||||
# title = translate("Path", "Depth Warning")
|
||||
# self.guiMessage(title, msg)
|
||||
(Tags, Grps) = self.sortTuplesByIndex(allTuples, 2) # return (TagList, GroupList)
|
||||
subList = []
|
||||
for o in range(0, len(Tags)):
|
||||
subList = []
|
||||
for (base, sub, tag, angle, axis, stock) in Grps[o]:
|
||||
subList.append(sub)
|
||||
pair = base, subList, angle, axis, stock
|
||||
baseSubsTuples.append(pair)
|
||||
# Efor
|
||||
else:
|
||||
PathLog.info(translate("Path", "EnableRotation property is 'Off'."))
|
||||
stock = PathUtils.findParentJob(obj).Stock
|
||||
for (base, subList) in obj.Base:
|
||||
baseSubsTuples.append((base, subList, 0.0, 'X', stock))
|
||||
|
||||
# for base in obj.Base:
|
||||
for (base, subsList, angle, axis, stock) in baseSubsTuples:
|
||||
holes = []
|
||||
faces = []
|
||||
for sub in base[1]:
|
||||
shape = getattr(base[0].Shape, sub)
|
||||
|
||||
for sub in subsList:
|
||||
shape = getattr(base.Shape, sub)
|
||||
if isinstance(shape, Part.Face):
|
||||
faces.append(shape)
|
||||
if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face
|
||||
for wire in shape.Wires[1:]:
|
||||
holes.append((base[0].Shape, wire))
|
||||
holes.append((base.Shape, wire))
|
||||
else:
|
||||
FreeCAD.Console.PrintWarning("found a base object which is not a face. Can't continue.")
|
||||
return
|
||||
ignoreSub = base.Name + '.' + sub
|
||||
msg = translate('Path', "Found a selected object which is not a face. Ignoring: {}".format(ignoreSub))
|
||||
PathLog.error(msg)
|
||||
FreeCAD.Console.PrintWarning(msg)
|
||||
# return
|
||||
|
||||
for shape, wire in holes:
|
||||
f = Part.makeFace(wire, 'Part::FaceMakerSimple')
|
||||
drillable = PathUtils.isDrillable(shape, wire)
|
||||
if (drillable and obj.processCircles) or (not drillable and obj.processHoles):
|
||||
env = PathUtils.getEnvelope(shape, subshape=f, depthparams=self.depthparams)
|
||||
PathLog.track()
|
||||
shapes.append((env, True))
|
||||
# Recalculate depthparams
|
||||
(strDep, finDep) = self.calculateStartFinalDepths(obj, shape, stock)
|
||||
finalDepths.append(finDep)
|
||||
PathLog.debug("Adjusted face depths strDep: {}, and finDep: {}".format(self.strDep, self.finDep))
|
||||
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
||||
self.depthparams = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=strDep, # obj.StartDepth.Value,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=finDep, # obj.FinalDepth.Value,
|
||||
user_depths=None)
|
||||
env = PathUtils.getEnvelope(shape, subshape=f, depthparams=self.depthparams)
|
||||
# shapes.append((env, True))
|
||||
tup = env, True, 'pathProfileFaces', angle, axis, strDep, finDep
|
||||
shapes.append(tup)
|
||||
|
||||
if len(faces) > 0:
|
||||
profileshape = Part.makeCompound(faces)
|
||||
self.profileshape.append(profileshape)
|
||||
|
||||
if obj.processPerimeter:
|
||||
env = PathUtils.getEnvelope(base[0].Shape, subshape=profileshape, depthparams=self.depthparams)
|
||||
PathLog.track()
|
||||
shapes.append((env, False))
|
||||
if profileshape:
|
||||
# Recalculate depthparams
|
||||
(strDep, finDep) = self.calculateStartFinalDepths(obj, profileshape, stock)
|
||||
finalDepths.append(finDep)
|
||||
PathLog.debug("Adjusted face depths strDep: {}, and finDep: {}".format(self.strDep, self.finDep))
|
||||
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
||||
self.depthparams = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=strDep, # obj.StartDepth.Value,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=finDep, # obj.FinalDepth.Value,
|
||||
user_depths=None)
|
||||
else:
|
||||
strDep = obj.StartDepth.Value
|
||||
finDep = obj.FinalDepth.Value
|
||||
try:
|
||||
env = PathUtils.getEnvelope(base.Shape, subshape=profileshape, depthparams=self.depthparams)
|
||||
except Exception:
|
||||
# PathUtils.getEnvelope() failed to return an object.
|
||||
PathLog.error(translate('Path', 'Unable to create path for face(s).'))
|
||||
else:
|
||||
# shapes.append((env, False))
|
||||
tup = env, False, 'pathProfileFaces', angle, axis, strDep, finDep
|
||||
shapes.append(tup)
|
||||
else:
|
||||
for shape in faces:
|
||||
# Recalculate depthparams
|
||||
(strDep, finDep) = self.calculateStartFinalDepths(obj, shape, stock)
|
||||
finalDepths.append(finDep)
|
||||
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
||||
self.depthparams = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=strDep, # obj.StartDepth.Value,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=finDep, # obj.FinalDepth.Value,
|
||||
user_depths=None)
|
||||
env = PathUtils.getEnvelope(base.Shape, subshape=shape, depthparams=self.depthparams)
|
||||
tup = env, False, 'pathProfileFaces', angle, axis, strDep, finDep
|
||||
shapes.append(tup)
|
||||
# Eif
|
||||
|
||||
# adjust FinalDepth as needed
|
||||
finalDepth = min(finalDepths)
|
||||
if obj.FinalDepth.Value < finalDepth:
|
||||
obj.FinalDepth.Value = finalDepth
|
||||
else: # Try to build targets from the job base
|
||||
if 1 == len(self.model) and hasattr(self.model[0], "Proxy"):
|
||||
if isinstance(self.model[0].Proxy, ArchPanel.PanelSheet): # process the sheet
|
||||
@@ -126,17 +285,22 @@ class ObjectProfile(PathProfileBase.ObjectProfile):
|
||||
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)
|
||||
shapes.append((env, True))
|
||||
# shapes.append((env, True))
|
||||
tup = env, True, 'pathProfileFaces', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value
|
||||
shapes.append(tup)
|
||||
|
||||
if obj.processPerimeter:
|
||||
for shape in self.model[0].Proxy.getOutlines(self.model[0], transform=True):
|
||||
for wire in shape.Wires:
|
||||
f = Part.makeFace(wire, 'Part::FaceMakerSimple')
|
||||
env = PathUtils.getEnvelope(self.model[0].Shape, subshape=f, depthparams=self.depthparams)
|
||||
shapes.append((env, False))
|
||||
# shapes.append((env, False))
|
||||
tup = env, False, 'pathProfileFaces', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value
|
||||
shapes.append(tup)
|
||||
|
||||
self.removalshapes = shapes
|
||||
PathLog.debug("%d shapes" % len(shapes))
|
||||
|
||||
return shapes
|
||||
|
||||
def areaOpSetDefaultValues(self, obj, job):
|
||||
@@ -146,17 +310,27 @@ class ObjectProfile(PathProfileBase.ObjectProfile):
|
||||
obj.processHoles = False
|
||||
obj.processCircles = False
|
||||
obj.processPerimeter = True
|
||||
obj.ReverseDirection = False
|
||||
obj.InverseAngle = False
|
||||
obj.AttemptInverseAngle = True
|
||||
obj.B_AxisErrorOverride = False
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
setup = []
|
||||
setup = PathProfileBase.SetupProperties()
|
||||
setup.append("processHoles")
|
||||
setup.append("processPerimeter")
|
||||
setup.append("processCircles")
|
||||
return PathProfileBase.SetupProperties() + setup
|
||||
setup.append("ReverseDirection")
|
||||
setup.append("InverseAngle")
|
||||
setup.append("B_AxisErrorOverride")
|
||||
setup.append("AttemptInverseAngle")
|
||||
return setup
|
||||
|
||||
def Create(name, obj = None):
|
||||
|
||||
def Create(name, obj=None):
|
||||
'''Create(name) ... Creates and returns a Profile based on faces operation.'''
|
||||
if obj is None:
|
||||
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
||||
proxy = ObjectProfile(obj, name)
|
||||
obj.Proxy = ObjectProfile(obj, name)
|
||||
return obj
|
||||
|
||||
Reference in New Issue
Block a user