From cd3c1d574eb9ecb0fc4d18ba93cc64680ce51744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCdepohl?= Date: Wed, 4 Jan 2017 20:08:35 +0100 Subject: [PATCH] Path: Helix taskpanel shows interdependencies of holes Instead of a flat list of holes now a tree is shown, with the various machining operations for the individual holes grouped together. --- src/Mod/Path/PathScripts/PathHelix.py | 260 ++++++++++++++++++-------- 1 file changed, 181 insertions(+), 79 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathHelix.py b/src/Mod/Path/PathScripts/PathHelix.py index bdfcd681c..da2e63a72 100644 --- a/src/Mod/Path/PathScripts/PathHelix.py +++ b/src/Mod/Path/PathScripts/PathHelix.py @@ -45,34 +45,6 @@ else: def translate(context, text, disambig=None): return text -def hollow_cylinder(cyl): - """Test if this is a hollow cylinder""" - from Part import Circle - circle1 = None - line = None - for edge in cyl.Edges: - if isinstance(edge.Curve, Circle): - if circle1 is None: - circle1 = edge - else: - circle2 = edge - else: - line = edge - center = (circle1.Curve.Center + circle2.Curve.Center).scale(0.5, 0.5, 0.5) - p = (circle1.valueAt(circle1.ParameterRange[0]) + circle2.valueAt(circle1.ParameterRange[0])).scale(0.5, 0.5, 0.5) - to_outside = (p - center).normalize() - u, v = cyl.Surface.parameter(p) - normal = cyl.normalAt(u, v).normalize() - - cos_a = to_outside.dot(normal) - - if cos_a > 1.0 - 1e-12: - return False - elif cos_a < -1.0 + 1e-12: - return True - else: - raise Exception("Strange cylinder encountered, cannot determine if it is hollow or not") - def z_cylinder(cyl): """ Test if cylinder is aligned to z-Axis""" if cyl.Surface.Axis.x != 0.0: @@ -81,11 +53,6 @@ def z_cylinder(cyl): return False return True -def full_cylinder(cyl): - p1 = cyl.valueAt(cyl.ParameterRange[0], cyl.ParameterRange[2]) - p2 = cyl.valueAt(cyl.ParameterRange[1], cyl.ParameterRange[2]) - return fmt(p1.x) == fmt(p2.x) and fmt(p1.y) == fmt(p2.y) and p1.z == p2.z - def connected(edge, face): for otheredge in face.Edges: if edge.isSame(otheredge): @@ -230,6 +197,35 @@ def helix_cut(center, r_out, r_in, dr, zmax, zmin, dz, safe_z, tool_diameter, vf return out +def features_by_centers(base, features): + import scipy.spatial + 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 = scipy.spatial.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): @@ -284,6 +280,12 @@ class ObjectPathHelix(object): from Part import Circle, Cylinder, Plane 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)") @@ -300,30 +302,15 @@ class ObjectPathHelix(object): tool = PathUtils.getTool(obj, toolload.ToolNumber) - output = '(helix cut operation' - if obj.Comment: - output += ', '+ str(obj.Comment)+')\n' - else: - output += ')\n' - 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: - centers = {} - - for feature in features: - cylinder = getattr(base.Shape, feature) - xc, yc, _ = cylinder.Surface.Center - if (xc, yc) not in centers: - centers[xc, yc] = {} - centers[xc, yc][cylinder.Surface.Radius] = cylinder - - for center, by_radius in centers.items(): - cylinders = sorted(by_radius.values(), key = lambda cyl : cyl.Surface.Radius, reverse=True) - + 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 = [] @@ -400,9 +387,9 @@ class ObjectPathHelix(object): toolload.VertFeed.Value, toolload.HorizFeed.Value, obj.Direction, obj.StartSide) output += '\n' - obj.Path = Path.Path(output) - if obj.ViewObject: - obj.ViewObject.Visibility = True + obj.Path = Path.Path(output) + if obj.ViewObject: + obj.ViewObject.Visibility = True taskpanels = {} @@ -486,6 +473,28 @@ class CommandPathHelix(object): 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): @@ -588,15 +597,16 @@ class TaskPanel(object): widget.currentIndexChanged.connect(change) addWidgets(label, widget) - self.featureList = QtGui.QListWidget() - self.featureList.setMinimumHeight(200) - self.featureList.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) - self.featureList.setDragDropMode(QtGui.QAbstractItemView.DragDrop) - self.featureList.setDefaultDropAction(QtCore.Qt.MoveAction) - self.fillFeatureList() - sm = self.featureList.selectionModel() + 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.featureList) + addWidget(self.featureTree) + self.featureTree.expandAll() self.addButton = QtGui.QPushButton("Add holes") self.addButton.clicked.connect(self.addCylinders) @@ -659,17 +669,65 @@ class TaskPanel(object): features_per_base[base] = [feature] self.obj.Features = list(features_per_base.items()) - self.featureList.clear() - self.fillFeatureList() + self.featureTree.clear() + self.fillFeatureTree() + self.featureTree.expandAll() self.obj.Proxy.execute(self.obj) FreeCAD.ActiveDocument.recompute() def delCylinders(self): del_features = [] - for item in self.featureList.selectedItems(): - obj, feature = item.data(QtCore.Qt.UserRole) - del_features.append((obj, feature)) - self.featureList.takeItem(self.featureList.row(item)) + + 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: @@ -681,20 +739,64 @@ class TaskPanel(object): self.obj.Proxy.execute(self.obj) FreeCAD.ActiveDocument.recompute() - def fillFeatureList(self): - for obj, features in self.obj.Features: - for feature in features: - radius = getattr(obj.Shape, feature).Surface.Radius - item = QtGui.QListWidgetItem() - item.setText(obj.Name + "." + feature + " ({0:.2f})".format(radius)) - item.setData(QtCore.Qt.UserRole, (obj, feature)) - self.featureList.addItem(item) + 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() - for item in self.featureList.selectedItems(): - obj, feature = item.data(QtCore.Qt.UserRole) - FreeCADGui.Selection.addSelection(obj, feature) + 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