Merge pull request #5405 from sliptonic/feature/helix_refactor

[Path] Feature/helix refactor
This commit is contained in:
sliptonic
2022-01-22 13:09:36 -06:00
committed by GitHub
7 changed files with 329 additions and 244 deletions

View File

@@ -51,7 +51,7 @@ def generate(
):
"""generate(edge, hole_radius, inner_radius, step_over) ... generate helix commands.
hole_radius, inner_radius: outer and inner radius of the hole
step_over: step over radius value"""
step_over: step over % of tool diameter"""
startPoint = edge.Vertexes[0].Point
endPoint = edge.Vertexes[1].Point
@@ -73,16 +73,16 @@ def generate(
)
if type(hole_radius) not in [float, int]:
raise ValueError("hole_radius must be a float")
raise TypeError("Invalid type for hole radius")
if hole_radius < 0.0:
raise ValueError("hole_radius < 0")
if type(inner_radius) not in [float, int]:
raise ValueError("inner_radius must be a float")
raise TypeError("inner_radius must be a float")
if type(tool_diameter) not in [float, int]:
raise ValueError("tool_diameter must be a float")
raise TypeError("tool_diameter must be a float")
if inner_radius > 0 and hole_radius - inner_radius < tool_diameter:
raise ValueError(
@@ -104,6 +104,13 @@ def generate(
elif direction not in ["CW", "CCW"]:
raise ValueError("Invalid value for parameter 'direction'")
if type(step_over) not in [float, int]:
raise TypeError("Invalid value for parameter 'step_over'")
if step_over <= 0 or step_over > 1:
raise ValueError("Invalid value for parameter 'step_over'")
step_over_distance = step_over * tool_diameter
if not (
isclose(startPoint.sub(endPoint).x, 0, rtol=1e-05, atol=1e-06)
and (isclose(startPoint.sub(endPoint).y, 0, rtol=1e-05, atol=1e-06))
@@ -117,13 +124,13 @@ def generate(
PathLog.debug("(annulus mode)\n")
outer_radius = hole_radius - tool_diameter / 2
step_radius = inner_radius + tool_diameter / 2
if abs((outer_radius - step_radius) / step_over) < 1e-5:
radii = [(outer_radius + step_radius) / 2]
if abs((outer_radius - step_radius) / step_over_distance) < 1e-5:
radii = [(outer_radius + inner_radius) / 2]
else:
nr = max(int(ceil((outer_radius - step_radius) / step_over)), 2)
nr = max(int(ceil((outer_radius - inner_radius) / step_over_distance)), 2)
radii = linspace(outer_radius, step_radius, nr)
elif hole_radius <= 2 * step_over:
elif hole_radius <= 2 * tool_diameter:
PathLog.debug("(single helix mode)\n")
radii = [hole_radius - tool_diameter / 2]
if radii[0] <= 0:
@@ -136,16 +143,17 @@ def generate(
else:
PathLog.debug("(full hole mode)\n")
outer_radius = hole_radius - tool_diameter / 2
step_radius = step_over / 2
nr = max(1 + int(ceil((outer_radius - step_radius) / step_over)), 2)
radii = [r for r in linspace(outer_radius, step_radius, nr) if r > 0]
nr = max(1 + int(ceil((outer_radius - inner_radius) / step_over_distance)), 2)
PathLog.debug("nr: {}".format(nr))
radii = [r for r in linspace(outer_radius, inner_radius, nr) if r > 0]
if not radii:
raise ValueError(
"Cannot helix a hole of diameter {0} with a tool of diameter {1}".format(
2 * hole_radius, tool_diameter
)
)
PathLog.debug("Radii: {}".format(radii))
# calculate the number of full and partial turns required
# Each full turn is two 180 degree arcs. Zsteps is equally spaced step
# down values

View File

@@ -14,6 +14,19 @@
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="3" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QFrame" name="frame">
<property name="frameShape">
@@ -81,14 +94,14 @@
</item>
</widget>
</item>
<item row="1" column="0">
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Direction</string>
</property>
</widget>
</item>
<item row="1" column="1">
<item row="2" column="1">
<widget class="QComboBox" name="direction">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The direction for the helix, clockwise or counter clockwise.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@@ -105,14 +118,14 @@
</item>
</widget>
</item>
<item row="2" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Step over percent</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="4" column="1">
<widget class="QSpinBox" name="stepOverPercent">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Specify the percent of the tool diameter each helix will be offset to the previous one.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;A step over of 100% means no overlap of the individual cuts.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@@ -131,24 +144,32 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Extra Offset</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="Gui::InputField" name="extraOffset">
<property name="unit" stdset="0">
<string notr="true"/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Gui::InputField</class>
<extends>QLineEdit</extends>
<header>Gui/InputField.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -36,7 +36,7 @@ __doc__ = "Helper for adding Feed Rate to Path Commands"
TODO: This needs to be able to handle feedrates for axes other than X,Y,Z
"""
if True:
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:

View File

@@ -24,7 +24,6 @@ import FreeCAD
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathUtil as PathUtil
import PySide
__title__ = "Path UI helper and utility functions"
@@ -32,32 +31,33 @@ __author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "A collection of helper and utility functions for the Path GUI."
def translate(context, text, disambig=None):
return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# PathLog.trackModule(PathLog.thisModule())
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
def updateInputField(obj, prop, widget, onBeforeChange=None):
'''updateInputField(obj, prop, widget) ... update obj's property prop with the value of widget.
"""updateInputField(obj, prop, widget) ... update obj's property prop with the value of widget.
The property's value is only assigned if the new value differs from the current value.
This prevents onChanged notifications where the value didn't actually change.
Gui::InputField and Gui::QuantitySpinBox widgets are supported - and the property can
be of type Quantity or Float.
If onBeforeChange is specified it is called before a new value is assigned to the property.
Returns True if a new value was assigned, False otherwise (new value is the same as the current).
'''
"""
PathLog.track()
value = widget.property('rawValue')
value = widget.property("rawValue")
attr = PathUtil.getProperty(obj, prop)
attrValue = attr.Value if hasattr(attr, 'Value') else attr
attrValue = attr.Value if hasattr(attr, "Value") else attr
isDiff = False
if not PathGeom.isRoughly(attrValue, value):
isDiff = True
else:
if hasattr(obj, 'ExpressionEngine'):
if hasattr(obj, "ExpressionEngine"):
noExpr = True
for (prp, expr) in obj.ExpressionEngine:
if prp == prop:
@@ -76,7 +76,9 @@ def updateInputField(obj, prop, widget, onBeforeChange=None):
widget.update()
if isDiff:
PathLog.debug("updateInputField(%s, %s): %.2f -> %.2f" % (obj.Label, prop, attr, value))
PathLog.debug(
"updateInputField(%s, %s): %.2f -> %.2f" % (obj.Label, prop, attr, value)
)
if onBeforeChange:
onBeforeChange(obj)
PathUtil.setProperty(obj, prop, value)
@@ -86,14 +88,14 @@ def updateInputField(obj, prop, widget, onBeforeChange=None):
class QuantitySpinBox:
'''Controller class to interface a Gui::QuantitySpinBox.
"""Controller class to interface a Gui::QuantitySpinBox.
The spin box gets bound to a given property and supports update in both directions.
QuatitySpinBox(widget, obj, prop, onBeforeChange=None)
widget ... expected to be reference to a Gui::QuantitySpinBox
obj ... document object
prop ... canonical name of the (sub-) property
onBeforeChange ... an optional callback being executed before the value of the property is changed
'''
"""
def __init__(self, widget, obj, prop, onBeforeChange=None):
PathLog.track(widget)
@@ -103,59 +105,61 @@ class QuantitySpinBox:
self.obj = obj
self.attachTo(obj, prop)
def attachTo(self, obj, prop = None):
'''attachTo(obj, prop=None) ... use an existing editor for the given object and property'''
def attachTo(self, obj, prop=None):
"""attachTo(obj, prop=None) ... use an existing editor for the given object and property"""
PathLog.track(self.prop, prop)
self.obj = obj
self.prop = prop
if obj and prop:
attr = PathUtil.getProperty(obj, prop)
if attr is not None:
if hasattr(attr, 'Value'):
self.widget.setProperty('unit', attr.getUserPreferred()[2])
self.widget.setProperty('binding', "%s.%s" % (obj.Name, prop))
if hasattr(attr, "Value"):
self.widget.setProperty("unit", attr.getUserPreferred()[2])
self.widget.setProperty("binding", "%s.%s" % (obj.Name, prop))
self.valid = True
else:
PathLog.warning(translate('PathGui', "Cannot find property %s of %s") % (prop, obj.Label))
PathLog.warning("Cannot find property {} of {}".format(prop, obj.Label))
self.valid = False
else:
self.valid = False
def expression(self):
'''expression() ... returns the expression if one is bound to the property'''
"""expression() ... returns the expression if one is bound to the property"""
PathLog.track(self.prop, self.valid)
if self.valid:
return self.widget.property('expression')
return ''
return self.widget.property("expression")
return ""
def setMinimum(self, quantity):
'''setMinimum(quantity) ... set the minimum'''
"""setMinimum(quantity) ... set the minimum"""
PathLog.track(self.prop, self.valid)
if self.valid:
value = quantity.Value if hasattr(quantity, 'Value') else quantity
self.widget.setProperty('setMinimum', value)
value = quantity.Value if hasattr(quantity, "Value") else quantity
self.widget.setProperty("setMinimum", value)
def updateSpinBox(self, quantity=None):
'''updateSpinBox(quantity=None) ... update the display value of the spin box.
"""updateSpinBox(quantity=None) ... update the display value of the spin box.
If no value is provided the value of the bound property is used.
quantity can be of type Quantity or Float.'''
quantity can be of type Quantity or Float."""
PathLog.track(self.prop, self.valid)
if self.valid:
expr = self._hasExpression()
expr = self._hasExpression()
if quantity is None:
if expr:
quantity = FreeCAD.Units.Quantity(self.obj.evalExpression(expr))
else:
quantity = PathUtil.getProperty(self.obj, self.prop)
value = quantity.Value if hasattr(quantity, 'Value') else quantity
self.widget.setProperty('rawValue', value)
value = quantity.Value if hasattr(quantity, "Value") else quantity
self.widget.setProperty("rawValue", value)
def updateProperty(self):
'''updateProperty() ... update the bound property with the value from the spin box'''
"""updateProperty() ... update the bound property with the value from the spin box"""
PathLog.track(self.prop, self.valid)
if self.valid:
return updateInputField(self.obj, self.prop, self.widget, self.onBeforeChange)
return updateInputField(
self.obj, self.prop, self.widget, self.onBeforeChange
)
return None
def _hasExpression(self):

View File

@@ -20,17 +20,18 @@
# * *
# ***************************************************************************
from Generators import helix_generator
from PathScripts.PathUtils import fmt
from PathScripts.PathUtils import sort_locations
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import Part
import Path
import PathScripts.PathCircularHoleBase as PathCircularHoleBase
import PathScripts.PathLog as PathLog
import PathScripts.PathOp as PathOp
import PathFeedRate
from PathScripts.PathUtils import fmt
from PathScripts.PathUtils import findParentJob
from PathScripts.PathUtils import sort_locations
from PySide import QtCore
__title__ = "Path Helix Drill Operation"
__author__ = "Lorenz Hüdepohl"
@@ -42,167 +43,187 @@ __scriptVersion__ = "1b testing"
__lastModified__ = "2019-07-12 09:50 CST"
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
translate = FreeCAD.Qt.translate
class ObjectHelix(PathCircularHoleBase.ObjectOp):
'''Proxy class for Helix operations.'''
"""Proxy class for Helix operations."""
@classmethod
def helixOpPropertyEnumerations(self, dataType="data"):
"""helixOpPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
Args:
dataType = 'data', 'raw', 'translated'
Notes:
'data' is list of internal string literals used in code
'raw' is list of (translated_text, data_string) tuples
'translated' is list of translated string literals
"""
# Enumeration lists for App::PropertyEnumeration properties
enums = {
"Direction": [
(translate("Path_Helix", "CW"), "CW"),
(translate("Path_Helix", "CCW"), "CCW"),
], # this is the direction that the profile runs
"StartSide": [
(translate("PathProfile", "Outside"), "Outside"),
(translate("PathProfile", "Inside"), "Inside"),
], # side of profile that cutter is on in relation to direction of profile
}
if dataType == "raw":
return enums
data = list()
idx = 0 if dataType == "translated" else 1
PathLog.debug(enums)
for k, v in enumerate(enums):
data.append((v, [tup[idx] for tup in enums[v]]))
PathLog.debug(data)
return data
def circularHoleFeatures(self, obj):
'''circularHoleFeatures(obj) ... enable features supported by Helix.'''
return PathOp.FeatureStepDown | PathOp.FeatureBaseEdges | PathOp.FeatureBaseFaces
"""circularHoleFeatures(obj) ... enable features supported by Helix."""
return (
PathOp.FeatureStepDown | PathOp.FeatureBaseEdges | PathOp.FeatureBaseFaces
)
def initCircularHoleOperation(self, obj):
'''initCircularHoleOperation(obj) ... create helix specific properties.'''
obj.addProperty("App::PropertyEnumeration", "Direction", "Helix Drill", translate("PathHelix", "The direction of the circular cuts, ClockWise (CW), or CounterClockWise (CCW)"))
obj.Direction = ['CW', 'CCW']
"""initCircularHoleOperation(obj) ... create helix specific properties."""
obj.addProperty(
"App::PropertyEnumeration",
"Direction",
"Helix Drill",
QT_TRANSLATE_NOOP(
"App::Property",
"The direction of the circular cuts, ClockWise (CW), or CounterClockWise (CCW)",
),
)
obj.addProperty("App::PropertyEnumeration", "StartSide", "Helix Drill", translate("PathHelix", "Start cutting from the inside or outside"))
obj.StartSide = ['Inside', 'Outside']
obj.addProperty(
"App::PropertyEnumeration",
"StartSide",
"Helix Drill",
QT_TRANSLATE_NOOP(
"App::Property", "Start cutting from the inside or outside"
),
)
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"))
obj.addProperty(
"App::PropertyPercent",
"StepOver",
"Helix Drill",
QT_TRANSLATE_NOOP(
"App::Property", "Percent of cutter diameter to step over on each pass"
),
)
obj.addProperty(
"App::PropertyLength",
"StartRadius",
"Helix Drill",
QT_TRANSLATE_NOOP("App::Property", "Starting Radius"),
)
obj.addProperty(
"App::PropertyDistance",
"OffsetExtra",
"Helix Drill",
QT_TRANSLATE_NOOP(
"App::Property",
"Extra value to stay away from final profile- good for roughing toolpath",
),
)
ENUMS = self.helixOpPropertyEnumerations()
for n in ENUMS:
setattr(obj, n[0], n[1])
def opOnDocumentRestored(self, obj):
if not hasattr(obj, 'StartRadius'):
obj.addProperty("App::PropertyLength", "StartRadius", "Helix Drill", translate("PathHelix", "Starting Radius"))
if not hasattr(obj, "StartRadius"):
obj.addProperty(
"App::PropertyLength",
"StartRadius",
"Helix Drill",
QT_TRANSLATE_NOOP("App::Property", "Starting Radius"),
)
if not hasattr(obj, "OffsetExtra"):
obj.addProperty(
"App::PropertyDistance",
"OffsetExtra",
"Helix Drill",
QT_TRANSLATE_NOOP(
"App::Property",
"Extra value to stay away from final profile- good for roughing toolpath",
),
)
def circularHoleExecute(self, obj, holes):
'''circularHoleExecute(obj, holes) ... generate helix commands for each hole in holes'''
"""circularHoleExecute(obj, holes) ... generate helix commands for each hole in holes"""
PathLog.track()
self.commandlist.append(Path.Command('(helix cut operation)'))
self.commandlist.append(Path.Command("(helix cut operation)"))
self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
self.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value}))
zsafe = max(baseobj.Shape.BoundBox.ZMax for baseobj, features in obj.Base) + obj.ClearanceHeight.Value
output = ''
output += "G0 Z" + fmt(zsafe)
holes = sort_locations(holes, ["x", "y"])
tool = obj.ToolController.Tool
tooldiamter = (
tool.Diameter.Value if hasattr(tool.Diameter, "Value") else tool.Diameter
)
args = {
"edge": None,
"hole_radius": None,
"step_down": obj.StepDown.Value,
"step_over": obj.StepOver / 100,
"tool_diameter": tooldiamter,
"inner_radius": obj.StartRadius.Value + obj.OffsetExtra.Value,
"direction": obj.Direction,
"startAt": obj.StartSide,
}
holes = sort_locations(holes, ['x', 'y'])
for hole in holes:
output += self.helix_cut(obj, hole['x'], hole['y'], hole['r'] / 2, float(obj.StartRadius.Value), (float(obj.StepOver.Value) / 50.0) * self.radius)
PathLog.debug(output)
args["hole_radius"] = (hole["r"] / 2) - (obj.OffsetExtra.Value)
startPoint = FreeCAD.Vector(hole["x"], hole["y"], obj.StartDepth.Value)
endPoint = FreeCAD.Vector(hole["x"], hole["y"], obj.FinalDepth.Value)
args["edge"] = Part.makeLine(startPoint, endPoint)
def helix_cut(self, obj, x0, y0, r_out, r_in, dr):
'''helix_cut(obj, x0, y0, r_out, r_in, dr) ... generate helix commands for specified hole.
x0, y0: coordinates of center
r_out, r_in: outer and inner radius of the hole
dr: step over radius value'''
from numpy import ceil, linspace
# move to starting postion
self.commandlist.append(
Path.Command("G0", {"Z": obj.ClearanceHeight.Value})
)
self.commandlist.append(
Path.Command(
"G0",
{
"X": startPoint.x,
"Y": startPoint.y,
"Z": obj.ClearanceHeight.Value,
},
)
)
self.commandlist.append(
Path.Command(
"G0", {"X": startPoint.x, "Y": startPoint.y, "Z": startPoint.z}
)
)
if (obj.StartDepth.Value <= obj.FinalDepth.Value):
return ""
results = helix_generator.generate(**args)
out = "(helix_cut <{0}, {1}>, {2})".format(
x0, y0, ", ".join(map(str, (r_out, r_in, dr, obj.StartDepth.Value,
obj.FinalDepth.Value, obj.StepDown.Value, obj.SafeHeight.Value,
self.radius, self.vertFeed, self.horizFeed, obj.Direction, obj.StartSide))))
for command in results:
self.commandlist.append(command)
nz = max(int(ceil((obj.StartDepth.Value - obj.FinalDepth.Value) / obj.StepDown.Value)), 2)
zi = linspace(obj.StartDepth.Value, obj.FinalDepth.Value, 2 * nz + 1)
def xyz(x=None, y=None, z=None):
out = ""
if x is not None:
out += " X" + fmt(x)
if y is not None:
out += " Y" + fmt(y)
if z is not None:
out += " Z" + fmt(z)
return out
def rapid(x=None, y=None, z=None):
return "G0" + xyz(x, y, z) + "\n"
def F(f=None):
return (" F" + fmt(f) if f else "")
def feed(x=None, y=None, z=None, f=None):
return "G1" + xyz(x, y, z) + F(f) + "\n"
def arc(x, y, i, j, z, f):
if obj.Direction == "CW":
code = "G2"
elif obj.Direction == "CCW":
code = "G3"
return code + " I" + fmt(i) + " J" + fmt(j) + " X" + fmt(x) + " Y" + fmt(y) + " Z" + fmt(z) + F(f) + "\n"
def helix_cut_r(r):
arc_cmd = 'G2' if obj.Direction == 'CW' else 'G3'
out = ""
out += rapid(x=x0 + r, y=y0)
self.commandlist.append(Path.Command('G0', {'X': x0 + r, 'Y': y0, 'F': self.horizRapid}))
out += rapid(z=obj.StartDepth.Value + 2 * self.radius)
self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
out += feed(z=obj.StartDepth.Value, f=self.vertFeed)
self.commandlist.append(Path.Command('G1', {'Z': obj.StartDepth.Value, 'F': self.vertFeed}))
# z = obj.FinalDepth.Value
for i in range(1, nz + 1):
out += arc(x0 - r, y0, i=-r, j=0.0, z=zi[2 * i - 1], f=self.horizFeed)
self.commandlist.append(Path.Command(arc_cmd, {'X': x0 - r, 'Y': y0, 'Z': zi[2 * i - 1], 'I': -r, 'J': 0.0, 'F': self.horizFeed}))
out += arc(x0 + r, y0, i=r, j=0.0, z=zi[2 * i], f=self.horizFeed)
self.commandlist.append(Path.Command(arc_cmd, {'X': x0 + r, 'Y': y0, 'Z': zi[2 * i], 'I': r, 'J': 0.0, 'F': self.horizFeed}))
out += arc(x0 - r, y0, i=-r, j=0.0, z=obj.FinalDepth.Value, f=self.horizFeed)
self.commandlist.append(Path.Command(arc_cmd, {'X': x0 - r, 'Y': y0, 'Z': obj.FinalDepth.Value, 'I': -r, 'J': 0.0, 'F': self.horizFeed}))
out += arc(x0 + r, y0, i=r, j=0.0, z=obj.FinalDepth.Value, f=self.horizFeed)
self.commandlist.append(Path.Command(arc_cmd, {'X': x0 + r, 'Y': y0, 'Z': obj.FinalDepth.Value, 'I': r, 'J': 0.0, 'F': self.horizFeed}))
out += feed(z=obj.StartDepth.Value + 2 * self.radius, f=self.vertFeed)
out += rapid(z=obj.SafeHeight.Value)
self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
return out
msg = None
if r_out < 0.0:
msg = "r_out < 0"
elif r_in > 0 and r_out - r_in < 2 * self.radius:
msg = "r_out - r_in = {0} is < tool diameter of {1}".format(r_out - r_in, 2 * self.radius)
elif r_in == 0.0 and not r_out > self.radius / 2.:
msg = "Cannot helix a hole of diameter {0} with a tool of diameter {1}".format(2 * r_out, 2 * self.radius)
elif obj.StartSide not in ["Inside", "Outside"]:
msg = "Invalid value for parameter 'obj.StartSide'"
elif r_in > 0:
out += "(annulus mode)\n"
r_out = r_out - self.radius
r_in = r_in + self.radius
if abs((r_out - r_in) / dr) < 1e-5:
radii = [(r_out + r_in) / 2]
else:
nr = max(int(ceil((r_out - r_in) / dr)), 2)
radii = linspace(r_out, r_in, nr)
elif r_out <= 2 * dr:
out += "(single helix mode)\n"
radii = [r_out - self.radius]
if radii[0] <= 0:
msg = "Cannot helix a hole of diameter {0} with a tool of diameter {1}".format(2 * r_out, 2 * self.radius)
else:
out += "(full hole mode)\n"
r_out = r_out - self.radius
r_in = dr / 2
nr = max(1 + int(ceil((r_out - r_in) / dr)), 2)
radii = [r for r in linspace(r_out, r_in, nr) if r > 0]
if not radii:
msg = "Cannot helix a hole of diameter {0} with a tool of diameter {1}".format(2 * r_out, 2 * self.radius)
if msg:
out += "(ERROR: Hole at {0}: ".format((x0, y0, obj.StartDepth.Value)) + msg + ")\n"
PathLog.error("{0} - ".format((x0, y0, obj.StartDepth.Value)) + msg)
return out
if obj.StartSide == "Inside":
radii = radii[::-1]
for r in radii:
out += "(radius {0})\n".format(r)
out += helix_cut_r(r)
return out
def opSetDefaultValues(self, obj, job):
obj.Direction = "CW"
obj.StartSide = "Inside"
obj.StepOver = 100
PathFeedRate.setFeedRate(self.commandlist, obj.ToolController)
def SetupProperties():
@@ -215,7 +236,7 @@ def SetupProperties():
def Create(name, obj=None, parentJob=None):
'''Create(name) ... Creates and returns a Helix operation.'''
"""Create(name) ... Creates and returns a Helix operation."""
if obj is None:
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
obj.Proxy = ObjectHelix(obj, name, parentJob)

View File

@@ -22,11 +22,15 @@
import FreeCAD
import FreeCADGui
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui
import PathScripts.PathHelix as PathHelix
import PathScripts.PathLog as PathLog
import PathScripts.PathOpGui as PathOpGui
import PathScripts.PathGui as PathGui
from PySide.QtCore import QT_TRANSLATE_NOOP
translate = FreeCAD.Qt.translate
from PySide import QtCore
@@ -42,27 +46,50 @@ else:
class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
'''Page controller class for Helix operations.'''
"""Page controller class for Helix operations."""
def getForm(self):
'''getForm() ... return UI'''
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpHelixEdit.ui")
"""getForm() ... return UI"""
form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpHelixEdit.ui")
comboToPropertyMap = [("startSide", "StartSide"), ("direction", "Direction")]
enumTups = PathHelix.ObjectHelix.helixOpPropertyEnumerations(dataType="raw")
self.populateCombobox(form, enumTups, comboToPropertyMap)
return form
def populateCombobox(self, form, enumTups, comboBoxesPropertyMap):
"""fillComboboxes(form, comboBoxesPropertyMap) ... populate comboboxes with translated enumerations
** comboBoxesPropertyMap will be unnecessary if UI files use strict combobox naming protocol.
Args:
form = UI form
enumTups = list of (translated_text, data_string) tuples
comboBoxesPropertyMap = list of (translated_text, data_string) tuples
"""
# Load appropriate enumerations in each combobox
for cb, prop in comboBoxesPropertyMap:
box = getattr(form, cb) # Get the combobox
box.clear() # clear the combobox
for text, data in enumTups[prop]: # load enumerations
box.addItem(text, data)
def getFields(self, obj):
'''getFields(obj) ... transfers values from UI to obj's proprties'''
"""getFields(obj) ... transfers values from UI to obj's proprties"""
PathLog.track()
if obj.Direction != str(self.form.direction.currentText()):
obj.Direction = str(self.form.direction.currentText())
if obj.StartSide != str(self.form.startSide.currentText()):
obj.StartSide = str(self.form.startSide.currentText())
if obj.Direction != str(self.form.direction.currentData()):
obj.Direction = str(self.form.direction.currentData())
if obj.StartSide != str(self.form.startSide.currentData()):
obj.StartSide = str(self.form.startSide.currentData())
if obj.StepOver != self.form.stepOverPercent.value():
obj.StepOver = self.form.stepOverPercent.value()
PathGui.updateInputField(obj, "OffsetExtra", self.form.extraOffset)
self.updateToolController(obj, self.form.toolController)
self.updateCoolant(obj, self.form.coolantController)
def setFields(self, obj):
'''setFields(obj) ... transfers obj's property values to UI'''
"""setFields(obj) ... transfers obj's property values to UI"""
PathLog.track()
self.form.stepOverPercent.setValue(obj.StepOver)
@@ -72,11 +99,14 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
self.setupToolController(obj, self.form.toolController)
self.setupCoolant(obj, self.form.coolantController)
self.form.extraOffset.setText(FreeCAD.Units.Quantity(obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString)
def getSignalsForUpdate(self, obj):
'''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
signals = []
signals.append(self.form.stepOverPercent.editingFinished)
signals.append(self.form.extraOffset.editingFinished)
signals.append(self.form.direction.currentIndexChanged)
signals.append(self.form.startSide.currentIndexChanged)
signals.append(self.form.toolController.currentIndexChanged)
@@ -84,12 +114,17 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
return signals
Command = PathOpGui.SetupOperation('Helix',
PathHelix.Create,
TaskPanelOpPage,
'Path_Helix',
QtCore.QT_TRANSLATE_NOOP("Path_Helix", "Helix"),
QtCore.QT_TRANSLATE_NOOP("Path_Helix", "Creates a Path Helix object from a features of a base object"),
PathHelix.SetupProperties)
Command = PathOpGui.SetupOperation(
"Helix",
PathHelix.Create,
TaskPanelOpPage,
"Path_Helix",
QT_TRANSLATE_NOOP("Path_Helix", "Helix"),
QT_TRANSLATE_NOOP(
"Path_Helix", "Creates a Path Helix object from a features of a base object"
),
PathHelix.SetupProperties,
)
FreeCAD.Console.PrintLog("Loading PathHelixGui... done\n")

View File

@@ -42,7 +42,7 @@ def _resetArgs():
"edge": edg,
"hole_radius": 10.0,
"step_down": 1.0,
"step_over": 5.0,
"step_over": 0.5,
"tool_diameter": 5.0,
"inner_radius": 0.0,
"direction": "CW",
@@ -62,7 +62,6 @@ G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z18.000000\
G0 X5.000000 Y5.000000 Z18.000000\
G0 Z20.000000"
def test00(self):
"""Test Basic Helix Generator Return"""
args = _resetArgs()
@@ -71,34 +70,27 @@ G0 Z20.000000"
self.assertTrue(type(result[0]) is Path.Command)
gcode = "".join([r.toGCode() for r in result])
print(gcode)
self.assertTrue(
gcode == self.expectedHelixGCode, "Incorrect helix g-code generated"
)
def test01(self):
"""Test Basic Helix Generator hole_radius is float > 0"""
"""Test Value and Type checking"""
args = _resetArgs()
args["hole_radius"] = '10'
self.assertRaises(ValueError, generator.generate, **args)
args["hole_radius"] = "10"
self.assertRaises(TypeError, generator.generate, **args)
args["hole_radius"] = -10.0
self.assertRaises(ValueError, generator.generate, **args)
def test02(self):
"""Test Basic Helix Generator inner_radius is float"""
args = _resetArgs()
args["inner_radius"] = '2'
self.assertRaises(ValueError, generator.generate, **args)
args["inner_radius"] = "2"
self.assertRaises(TypeError, generator.generate, **args)
def test03(self):
"""Test Basic Helix Generator tool_diameter is float"""
args = _resetArgs()
args["tool_diameter"] = '5'
self.assertRaises(ValueError, generator.generate, **args)
args["tool_diameter"] = "5"
self.assertRaises(TypeError, generator.generate, **args)
def test04(self):
"""Test Basic Helix Generator tool fit with radius difference less than tool diameter"""
args = _resetArgs()
# require tool fit 1: radius diff less than tool diam
args["hole_radius"] = 10.0
@@ -112,14 +104,18 @@ G0 Z20.000000"
args["tool_diameter"] = 5.0
self.assertRaises(ValueError, generator.generate, **args)
def test05(self):
"""Test Basic Helix Generator validate the startAt enumeration value"""
# step_over is a percent value between 0 and 1
args = _resetArgs()
args["step_over"] = 50
self.assertRaises(ValueError, generator.generate, **args)
args["step_over"] = "50"
self.assertRaises(TypeError, generator.generate, **args)
# Other argument testing
args = _resetArgs()
args["startAt"] = "Other"
self.assertRaises(ValueError, generator.generate, **args)
def test06(self):
"""Test Basic Helix Generator validate the direction enumeration value"""
args = _resetArgs()
args["direction"] = "clock"
self.assertRaises(ValueError, generator.generate, **args)