Merge pull request #5405 from sliptonic/feature/helix_refactor
[Path] Feature/helix refactor
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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><html><head/><body><p>The direction for the helix, clockwise or counter clockwise.</p></body></html></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><html><head/><body><p>Specify the percent of the tool diameter each helix will be offset to the previous one.</p><p><br/></p><p>A step over of 100% means no overlap of the individual cuts.</p></body></html></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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user