Files
create/src/Mod/Path/PathScripts/PathHelix.py
sliptonic ddb2001dcb Path: Change Profile 'side' enumeration to inside/outside
left/right was too ambiguous
correct profile direction set
2017-07-10 19:17:41 -03:00

872 lines
33 KiB
Python

# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2016 Lorenz Hüdepohl <dev@stellardeath.org> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from . import PathUtils
from .PathUtils import fmt
import Part
import FreeCAD
import Path
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui
#from DraftTools import translate
"""Helix Drill object and FreeCAD command"""
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
def z_cylinder(cyl):
""" Test if cylinder is aligned to z-Axis"""
axis = cyl.Surface.Axis
if abs(axis.x) > 1e-10 * abs(axis.z):
return False
if abs(axis.y) > 1e-10 * abs(axis.z):
return False
return True
def connected(edge, face):
for otheredge in face.Edges:
if edge.isSame(otheredge):
return True
return False
def cylinders_in_selection():
from Part import Cylinder
selections = FreeCADGui.Selection.getSelectionEx()
cylinders = []
for selection in selections:
base = selection.Object
cylinders.append((base, []))
for feature in selection.SubElementNames:
subobj = getattr(base.Shape, feature)
if subobj.ShapeType == 'Face':
if isinstance(subobj.Surface, Cylinder):
if z_cylinder(subobj):
cylinders[-1][1].append(feature)
return cylinders
def helix_cut(center, r_out, r_in, dr, zmax, zmin, dz, safe_z, tool_diameter, vfeed, hfeed, direction, startside):
"""
center: 2-tuple
(x0, y0) coordinates of center
r_out, r_in: floats
radial range, cut from outer radius r_out in layers of dr to inner radius r_in
zmax, zmin: floats
z-range, cut from zmax in layers of dz down to zmin
safe_z: float
safety layer height
tool_diameter: float
Width of tool
"""
from numpy import ceil, linspace
if (zmax <= zmin):
return
out = "(helix_cut <{0}, {1}>, {2})".format(center[0], center[1],
", ".join(map(str, (r_out, r_in, dr, zmax, zmin, dz, safe_z,
tool_diameter, vfeed, hfeed, direction, startside))))
x0, y0 = center
nz = max(int(ceil((zmax - zmin)/dz)), 2)
zi = linspace(zmax, zmin, 2 * nz + 1)
if dr > tool_diameter:
FreeCAD.Console.PrintWarning("PathHelix: Warning, shortening dr to tool diameter!\n")
dr = tool_diameter
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 direction == "CW":
code = "G2"
elif 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):
out = ""
out += rapid(x=x0+r, y=y0)
out += rapid(z=zmax + tool_diameter)
out += feed(z=zmax, f=vfeed)
# z = zmin
for i in range(1, nz+1):
out += arc(x0-r, y0, i=-r, j=0.0, z=zi[2*i-1], f=hfeed)
out += arc(x0+r, y0, i= r, j=0.0, z=zi[2*i], f=hfeed)
out += arc(x0-r, y0, i=-r, j=0.0, z=zmin, f=hfeed)
out += arc(x0+r, y0, i=r, j=0.0, z=zmin, f=hfeed)
out += feed(z=zmax + tool_diameter, f=vfeed)
out += rapid(z=safe_z)
return out
assert(r_out > 0.0)
assert(r_in >= 0.0)
msg = None
if r_out < 0.0:
msg = "r_out < 0"
elif r_in > 0 and r_out - r_in < tool_diameter:
msg = "r_out - r_in = {0} is < tool diameter of {1}".format(r_out - r_in, tool_diameter)
elif r_in == 0.0 and not r_out > tool_diameter/2.:
msg = "Cannot drill a hole of diameter {0} with a tool of diameter {1}".format(2 * r_out, tool_diameter)
elif startside not in ["inside", "outside"]:
msg = "Invalid value for parameter 'startside'"
if msg:
out += "(ERROR: Hole at {0}:".format((x0, y0, zmax)) + msg + ")\n"
FreeCAD.Console.PrintError("PathHelix: Hole at {0}:".format((x0, y0, zmax)) + msg + "\n")
return out
if r_in > 0:
out += "(annulus mode)\n"
r_out = r_out - tool_diameter/2
r_in = r_in + tool_diameter/2
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 - tool_diameter/2]
assert(radii[0] > 0)
else:
out += "(full hole mode)\n"
r_out = r_out - tool_diameter/2
r_in = dr/2
nr = max(1 + int(ceil((r_out - r_in)/dr)), 2)
radii = linspace(r_out, r_in, nr)
assert(all(radii > 0))
if startside == "inside":
radii = radii[::-1]
for r in radii:
out += "(radius {0})\n".format(r)
out += helix_cut_r(r)
return out
def features_by_centers(base, features):
try:
from scipy.spatial import KDTree
except ImportError:
from PathScripts.kdtree import KDTree
features = sorted(features,
key=lambda feature: getattr(base.Shape, feature).Surface.Radius,
reverse=True)
coordinates = [(cylinder.Surface.Center.x, cylinder.Surface.Center.y) for cylinder in
[getattr(base.Shape, feature) for feature in features]]
tree = KDTree(coordinates)
seen = {}
by_centers = {}
for n, feature in enumerate(features):
if n in seen:
continue
seen[n] = True
cylinder = getattr(base.Shape, feature)
xc, yc, _ = cylinder.Surface.Center
by_centers[xc, yc] = {cylinder.Surface.Radius: feature}
for coord in tree.query_ball_point((xc, yc), cylinder.Surface.Radius):
seen[coord] = True
cylinder = getattr(base.Shape, features[coord])
by_centers[xc, yc][cylinder.Surface.Radius] = features[coord]
return by_centers
class ObjectPathHelix(object):
def __init__(self, obj):
# Basic
obj.addProperty("App::PropertyLink", "ToolController", "Path",
translate("App::Property", "The tool controller that will be used to calculate the path"))
obj.addProperty("App::PropertyLinkSubList", "Features", "Path",
translate("Features", "Selected features for the drill operation"))
obj.addProperty("App::PropertyBool", "Active", "Path",
translate("Active", "Set to False to disable code generation"))
obj.addProperty("App::PropertyString", "Comment", "Path",
translate("Comment", "An optional comment for this profile, will appear in G-Code"))
# Helix specific
obj.addProperty("App::PropertyEnumeration", "Direction", "Helix Drill",
translate("Direction", "The direction of the circular cuts, clockwise (CW), or counter clockwise (CCW)"))
obj.Direction = ['CW', 'CCW']
obj.addProperty("App::PropertyEnumeration", "StartSide", "Helix Drill",
translate("Direction", "Start cutting from the inside or outside"))
obj.StartSide = ['inside', 'outside']
obj.addProperty("App::PropertyLength", "DeltaR", "Helix Drill",
translate("DeltaR", "Radius increment (must be smaller than tool diameter)"))
# Depth Properties
obj.addProperty("App::PropertyDistance", "Clearance", "Depths",
translate("Clearance", "Safe distance above the top of the hole to which to retract the tool"))
obj.addProperty("App::PropertyLength", "StepDown", "Depths",
translate("StepDown", "Incremental Step Down of Tool"))
obj.addProperty("App::PropertyBool", "UseStartDepth", "Depths",
translate("Use Start Depth", "Set to True to manually specify a start depth"))
obj.addProperty("App::PropertyDistance", "StartDepth", "Depths",
translate("Start Depth", "Starting Depth of Tool - first cut depth in Z"))
obj.addProperty("App::PropertyBool", "UseFinalDepth", "Depths",
translate("Use Final Depth", "Set to True to manually specify a final depth"))
obj.addProperty("App::PropertyDistance", "FinalDepth", "Depths",
translate("Final Depth", "Final Depth of Tool - lowest value in Z"))
obj.addProperty("App::PropertyDistance", "ThroughDepth", "Depths",
translate("Through Depth", "Add this amount of additional cutting depth "
"to open-ended holes. Only used if UseFinalDepth is False"))
# The current tool number, read-only
# this is apparently used internally, to keep track of tool chagnes
obj.addProperty("App::PropertyIntegerConstraint", "ToolNumber", "Tool",
translate("PathProfile", "The current tool in use"))
obj.ToolNumber = (0, 0, 1000, 1)
obj.setEditorMode('ToolNumber', 1) # make this read only
obj.Proxy = self
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def execute(self, obj):
# from Part import Circle, Cylinder, Plane
# from PathScripts import PathUtils
# from math import sqrt
output = '(helix cut operation'
if obj.Comment:
output += ', ' + str(obj.Comment) + ')\n'
else:
output += ')\n'
if obj.Features:
if not obj.Active:
obj.Path = Path.Path("(helix cut operation inactive)")
if obj.ViewObject:
obj.ViewObject.Visibility = False
return
if not obj.ToolController:
obj.ToolController = PathUtils.findToolController(obj)
toolLoad = obj.ToolController
if toolLoad is None or toolLoad.ToolNumber == 0:
FreeCAD.Console.PrintError("PathHelix: No tool selected for helix cut operation, insert a tool change operation first\n")
obj.Path = Path.Path("(ERROR: no tool selected for helix cut operation)")
return
tool = toolLoad.Proxy.getTool(toolLoad)
zsafe = max(baseobj.Shape.BoundBox.ZMax for baseobj, features in obj.Features) + obj.Clearance.Value
output += "G0 Z" + fmt(zsafe)
drill_jobs = []
for base, features in obj.Features:
for center, by_radius in features_by_centers(base, features).items():
radii = sorted(by_radius.keys(), reverse=True)
cylinders = map(lambda radius: getattr(base.Shape, by_radius[radius]), radii)
zsafe = max(cyl.BoundBox.ZMax for cyl in cylinders) + obj.Clearance.Value
cur_z = cylinders[0].BoundBox.ZMax
jobs = []
for cylinder in cylinders:
# Find other edge of current cylinder
other_edge = None
for edge in cylinder.Edges:
if isinstance(edge.Curve, Part.Circle) and edge.Curve.Center.z != cur_z:
other_edge = edge
break
next_z = other_edge.Curve.Center.z
dz = next_z - cur_z
r = cylinder.Surface.Radius
if dz < 0:
# This is a closed hole if the face connected to
# the current cylinder at next_z has the cylinder's
# edge as its OuterWire
closed = None
for face in base.Shape.Faces:
if connected(other_edge, face) and not face.isSame(cylinder.Faces[0]):
wire = face.OuterWire
if len(wire.Edges) == 1 and wire.Edges[0].isSame(other_edge):
closed = True
else:
closed = False
if closed is None:
raise Exception("Cannot determine if this cylinder is closed on the z = {0} side".format(next_z))
xc, yc, _ = cylinder.Surface.Center
jobs.append(dict(xc=xc, yc=yc,
zmin=next_z, zmax=cur_z, zsafe=zsafe,
r_out=r, r_in=0.0, closed=closed))
elif dz > 0:
new_jobs = []
for job in jobs:
if job["zmin"] < next_z < job["zmax"]:
# split this job
job1 = dict(job)
job2 = dict(job)
job1["zmin"] = next_z
job2["zmax"] = next_z
job2["r_in"] = r
new_jobs.append(job1)
new_jobs.append(job2)
else:
new_jobs.append(job)
jobs = new_jobs
else:
FreeCAD.Console.PrintError("PathHelix: Encountered cylinder with zero height\n")
break
cur_z = next_z
if obj.UseStartDepth:
jobs = [job for job in jobs if job["zmin"] < obj.StartDepth.Value]
if jobs:
jobs[0]["zmax"] = obj.StartDepth.Value
if obj.UseFinalDepth:
jobs = [job for job in jobs if job["zmax"] > obj.FinalDepth.Value]
if jobs:
jobs[-1]["zmin"] = obj.FinalDepth.Value
else:
if not jobs[-1]["closed"]:
jobs[-1]["zmin"] -= obj.ThroughDepth.Value
drill_jobs.extend(jobs)
if len(drill_jobs) > 0:
drill_jobs = PathUtils.sort_jobs(drill_jobs, ['xc', 'yc'], ['xc', 'zmax'])
for job in drill_jobs:
output += helix_cut((job["xc"], job["yc"]), job["r_out"], job["r_in"], obj.DeltaR.Value,
job["zmax"], job["zmin"], obj.StepDown.Value,
job["zsafe"], tool.Diameter,
toolLoad.VertFeed.Value, toolLoad.HorizFeed.Value,
obj.Direction, obj.StartSide)
output += '\n'
obj.Path = Path.Path(output)
# if obj.ViewObject:
# obj.ViewObject.Visibility = True
class ViewProviderPathHelix(object):
def __init__(self, vobj):
vobj.Proxy = self
def attach(self, vobj):
self.Object = vobj.Object
return
def getIcon(self):
return ":/icons/Path-Helix.svg"
def setEdit(self, vobj, mode=0):
FreeCADGui.Control.closeDialog()
taskpanel = TaskPanel(vobj.Object)
FreeCADGui.Control.showDialog(taskpanel)
return True
def __getstate__(self):
return None
def __setstate__(self, state):
return None
class CommandPathHelix(object):
def GetResources(self):
return {'Pixmap': 'Path-Helix',
'MenuText': QtCore.QT_TRANSLATE_NOOP("PathHelix", "PathHelix"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathHelix", "Creates a helix cut from selected circles")}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
for o in FreeCAD.ActiveDocument.Objects:
if o.Name[:3] == "Job":
return True
return False
def Activated(self):
import FreeCADGui
# import Path
from PathScripts import PathUtils
FreeCAD.ActiveDocument.openTransaction(translate("PathHelix", "Create a helix cut"))
FreeCADGui.addModule("PathScripts.PathHelix")
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "PathHelix")
ObjectPathHelix(obj)
ViewProviderPathHelix(obj.ViewObject)
obj.Features = cylinders_in_selection()
obj.DeltaR = 1.0
if not obj.ToolController:
obj.ToolController = PathUtils.findToolController(obj)
toolLoad = obj.ToolController
if toolLoad is not None:
obj.ToolNumber = toolLoad.ToolNumber
tool = toolLoad.Proxy.getTool(toolLoad)
if tool:
# start with 25% overlap
obj.DeltaR = tool.Diameter * 0.75
obj.Active = True
obj.Comment = ""
obj.Direction = "CW"
obj.StartSide = "inside"
obj.Clearance = 10.0
obj.StepDown = 1.0
obj.UseStartDepth = False
obj.StartDepth = 1.0
obj.UseFinalDepth = False
obj.FinalDepth = 0.0
obj.ThroughDepth = 0.0
PathUtils.addToJob(obj)
obj.ViewObject.startEditing()
FreeCAD.ActiveDocument.recompute()
def print_exceptions(func):
from functools import wraps
import traceback
import sys
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except:
ex_type, ex, tb = sys.exc_info()
FreeCAD.Console.PrintError("".join(traceback.format_exception(ex_type, ex, tb)) + "\n")
raise
return wrapper
def print_all_exceptions(cls):
for entry in dir(cls):
obj = getattr(cls, entry)
if not entry.startswith("__") and hasattr(obj, "__call__"):
setattr(cls, entry, print_exceptions(obj))
return cls
@print_all_exceptions
class TaskPanel(object):
def __init__(self, obj):
#from Units import Quantity
from PathScripts import PathUtils
self.obj = obj
self.previous_value = {}
self.form = QtGui.QToolBox()
ui = FreeCADGui.UiLoader()
grayed_out = "background-color: #d0d0d0;"
def nextToolBoxItem(label, iconFile):
widget = QtGui.QWidget()
layout = QtGui.QGridLayout()
widget.setLayout(layout)
icon = QtGui.QIcon(iconFile)
self.form.addItem(widget, icon, label)
return layout
def addFiller():
row = layout.rowCount()
widget = QtGui.QWidget()
layout.addWidget(widget, row, 0, 1, 2)
layout.setRowStretch(row, 1)
layout = nextToolBoxItem("Geometry", ":/icons/PartDesign_InternalExternalGear.svg")
def addWidget(widget):
row = layout.rowCount()
layout.addWidget(widget, row, 0, 1, 2)
def addWidgets(widget1, widget2):
row = layout.rowCount()
layout.addWidget(widget1, row, 0)
layout.addWidget(widget2, row, 1)
def addQuantity(property, labelstring, activator=None, max=None):
self.previous_value[property] = getattr(self.obj, property)
widget = ui.createWidget("Gui::InputField")
if activator:
self.previous_value[activator] = getattr(self.obj, activator)
currently_active = getattr(self.obj, activator)
label = QtGui.QCheckBox(labelstring)
def change(state):
setattr(self.obj, activator, label.isChecked())
if label.isChecked():
widget.setStyleSheet("")
else:
widget.setStyleSheet(grayed_out)
self.obj.Proxy.execute(self.obj)
FreeCAD.ActiveDocument.recompute()
label.stateChanged.connect(change)
label.setChecked(currently_active)
if not currently_active:
widget.setStyleSheet(grayed_out)
label.setToolTip(self.obj.getDocumentationOfProperty(activator))
else:
label = QtGui.QLabel(labelstring)
label.setToolTip(self.obj.getDocumentationOfProperty(property))
quantity = getattr(self.obj, property)
widget.setText(quantity.UserString)
widget.setToolTip(self.obj.getDocumentationOfProperty(property))
if max:
# cannot use widget.setMaximum() as apparently ui.createWidget()
# returns the object up-casted to QWidget.
widget.setProperty("maximum", max)
def change(quantity):
setattr(self.obj, property, quantity)
self.obj.Proxy.execute(self.obj)
FreeCAD.ActiveDocument.recompute()
QtCore.QObject.connect(widget, QtCore.SIGNAL("valueChanged(const Base::Quantity &)"), change)
addWidgets(label, widget)
return label, widget
def addCheckBox(property, label):
self.previous_value[property] = getattr(self.obj, property)
widget = QtGui.QCheckBox(label)
widget.setToolTip(self.obj.getDocumentationOfProperty(property))
def change(state):
setattr(self.obj, property, widget.isChecked())
self.obj.Proxy.execute(self.obj)
FreeCAD.ActiveDocument.recompute()
widget.stateChanged.connect(change)
widget.setChecked(getattr(self.obj, property))
addWidget(widget)
def addEnumeration(property, label, options):
self.previous_value[property] = getattr(self.obj, property)
label = QtGui.QLabel(label)
label.setToolTip(self.obj.getDocumentationOfProperty(property))
widget = QtGui.QComboBox()
widget.setToolTip(self.obj.getDocumentationOfProperty(property))
for option_label, option_value in options:
widget.addItem(option_label)
def change(index):
setattr(self.obj, property, options[index][1])
self.obj.Proxy.execute(self.obj)
FreeCAD.ActiveDocument.recompute()
widget.currentIndexChanged.connect(change)
addWidgets(label, widget)
self.featureTree = QtGui.QTreeWidget()
self.featureTree.setMinimumHeight(200)
self.featureTree.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
# self.featureTree.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
# self.featureTree.setDefaultDropAction(QtCore.Qt.MoveAction)
self.fillFeatureTree()
sm = self.featureTree.selectionModel()
sm.selectionChanged.connect(self.selectFeatures)
addWidget(self.featureTree)
self.featureTree.expandAll()
self.addButton = QtGui.QPushButton("Add holes")
self.addButton.clicked.connect(self.addCylinders)
self.delButton = QtGui.QPushButton("Delete")
self.delButton.clicked.connect(self.delCylinders)
addWidgets(self.addButton, self.delButton)
# End of "Features" section
layout = nextToolBoxItem("Drill parameters", ":/icons/Path-OperationB.svg")
addCheckBox("Active", "Operation is active")
toolLoad = PathUtils.findToolController(obj)
tool = toolLoad and toolLoad.Proxy.getTool(toolLoad)
if not tool:
drmax = None
else:
drmax = tool.Diameter
addQuantity("DeltaR", "Step in Radius", max=drmax)
addQuantity("StepDown", "Step in Z")
addEnumeration("Direction", "Cut direction",
[("Clockwise", "CW"), ("Counter-Clockwise", "CCW")])
addEnumeration("StartSide", "Start Side",
[("Start from inside", "inside"), ("Start from outside", "outside")])
# End of "Drill parameters" section
addFiller()
layout = nextToolBoxItem("Cutting Depths", ":/icons/Path-Depths.svg")
addQuantity("Clearance", "Clearance Distance")
addQuantity("StartDepth", "Absolute start height", "UseStartDepth")
fdcheckbox, fdinput = addQuantity("FinalDepth", "Absolute final height", "UseFinalDepth")
tdlabel, tdinput = addQuantity("ThroughDepth", "Extra drill depth\nfor open holes")
# End of "Cutting Depths" section
addFiller()
# make ThroughDepth and FinalDepth mutually exclusive
def fd_change(state):
if fdcheckbox.isChecked():
tdinput.setStyleSheet(grayed_out)
else:
tdinput.setStyleSheet("")
fdcheckbox.stateChanged.connect(fd_change)
def td_change(quantity):
fdcheckbox.setChecked(False)
QtCore.QObject.connect(tdinput, QtCore.SIGNAL("valueChanged(const Base::Quantity &)"), td_change)
if obj.UseFinalDepth:
tdinput.setStyleSheet(grayed_out)
def addCylinders(self):
features_per_base = {}
for base, features in self.obj.Features:
features_per_base[base] = list(set(features))
for base, features in cylinders_in_selection():
for feature in features:
if base in features_per_base:
if feature not in features_per_base[base]:
features_per_base[base].append(feature)
else:
features_per_base[base] = [feature]
self.obj.Features = list(features_per_base.items())
self.featureTree.clear()
self.fillFeatureTree()
self.featureTree.expandAll()
self.obj.Proxy.execute(self.obj)
FreeCAD.ActiveDocument.recompute()
def delCylinders(self):
del_features = []
def delete_feature(item, base=None):
kind, feature = item.data(0, QtCore.Qt.UserRole)
assert(kind == "feature")
if base is None:
base_item = item.parent().parent()
_, base = base_item.data(0, QtCore.Qt.UserRole)
del_features.append((base, feature))
item.parent().takeChild(item.parent().indexOfChild(item))
def delete_hole(item, base=None):
kind, center = item.data(0, QtCore.Qt.UserRole)
assert(kind == "hole")
if base is None:
base_item = item.parent()
_, base = base_item.data(0, QtCore.Qt.UserRole)
for i in reversed(range(item.childCount())):
delete_feature(item.child(i), base=base)
item.parent().takeChild(item.parent().indexOfChild(item))
def delete_base(item):
kind, base = item.data(0, QtCore.Qt.UserRole)
assert(kind == "base")
for i in reversed(range(item.childCount())):
delete_hole(item.child(i), base=base)
self.featureTree.takeTopLevelItem(self.featureTree.indexOfTopLevelItem(item))
for item in self.featureTree.selectedItems():
kind, info = item.data(0, QtCore.Qt.UserRole)
if kind == "base":
delete_base(item)
elif kind == "hole":
parent = item.parent()
delete_hole(item)
if parent.childCount() == 0:
self.featureTree.takeTopLevelItem(self.featureTree.indexOfTopLevelItem(parent))
elif kind == "feature":
parent = item.parent()
delete_feature(item)
if parent.childCount() == 0:
parent.parent().takeChild(parent.parent().indexOfChild(parent))
else:
raise Exception("No such item kind: {0}".format(kind))
for base, features in cylinders_in_selection():
for feature in features:
del_features.append((base, feature))
new_features = []
for obj, features in self.obj.Features:
for feature in features:
if (obj, feature) not in del_features:
new_features.append((obj, feature))
self.obj.Features = new_features
self.obj.Proxy.execute(self.obj)
FreeCAD.ActiveDocument.recompute()
def fillFeatureTree(self):
for base, features in self.obj.Features:
base_item = QtGui.QTreeWidgetItem()
base_item.setText(0, base.Name)
base_item.setData(0, QtCore.Qt.UserRole, ("base", base))
self.featureTree.addTopLevelItem(base_item)
for center, by_radius in features_by_centers(base, features).items():
hole_item = QtGui.QTreeWidgetItem()
hole_item.setText(0, "Hole at ({0[0]:.2f}, {0[1]:.2f})".format(center))
hole_item.setData(0, QtCore.Qt.UserRole, ("hole", center))
base_item.addChild(hole_item)
for radius in sorted(by_radius.keys(), reverse=True):
feature = by_radius[radius]
cylinder = getattr(base.Shape, feature)
cyl_item = QtGui.QTreeWidgetItem()
cyl_item.setText(0, "Diameter {0:.2f}, {1}".format(
2 * cylinder.Surface.Radius, feature))
cyl_item.setData(0, QtCore.Qt.UserRole, ("feature", feature))
hole_item.addChild(cyl_item)
def selectFeatures(self, selected, deselected):
FreeCADGui.Selection.clearSelection()
def select_feature(item, base=None):
kind, feature = item.data(0, QtCore.Qt.UserRole)
assert(kind == "feature")
if base is None:
base_item = item.parent().parent()
_, base = base_item.data(0, QtCore.Qt.UserRole)
FreeCADGui.Selection.addSelection(base, feature)
def select_hole(item, base=None):
kind, center = item.data(0, QtCore.Qt.UserRole)
assert(kind == "hole")
if base is None:
base_item = item.parent()
_, base = base_item.data(0, QtCore.Qt.UserRole)
for i in range(item.childCount()):
select_feature(item.child(i), base=base)
def select_base(item):
kind, base = item.data(0, QtCore.Qt.UserRole)
assert(kind == "base")
for i in range(item.childCount()):
select_hole(item.child(i), base=base)
for item in self.featureTree.selectedItems():
kind, info = item.data(0, QtCore.Qt.UserRole)
if kind == "base":
select_base(item)
elif kind == "hole":
select_hole(item)
elif kind == "feature":
select_feature(item)
def needsFullSpace(self):
return True
def accept(self):
FreeCADGui.ActiveDocument.resetEdit()
FreeCADGui.Control.closeDialog()
def reject(self):
for property in self.previous_value:
setattr(self.obj, property, self.previous_value[property])
self.obj.Proxy.execute(self.obj)
FreeCAD.ActiveDocument.recompute()
FreeCADGui.ActiveDocument.resetEdit()
FreeCADGui.Control.closeDialog()
if FreeCAD.GuiUp:
# import FreeCADGui
FreeCADGui.addCommand('Path_Helix', CommandPathHelix())