# -*- coding: utf-8 -*- # *************************************************************************** # * * # * Copyright (c) 2018 sliptonic * # * * # * 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 * # * * # * Bilinear interpolation code modified heavily from the interpolation * # * library https://github.com/pmav99/interpolation * # * Copyright (c) 2013 by Panagiotis Mavrogiorgos * # * * # *************************************************************************** import FreeCAD import FreeCADGui import Part import Path import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathUtils as PathUtils from PySide import QtCore, QtGui """Z Depth Correction Dressup. This dressup takes a probe file as input and does bilinear interpolation of the Zdepths to correct for a surface which is not parallel to the milling table/bed. The probe file should conform to the format specified by the linuxcnc G38 probe logging: 9-number coordinate consisting of XYZABCUVW http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g38 """ LOG_MODULE = PathLog.thisModule() if False: PathLog.setLevel(PathLog.Level.DEBUG, LOG_MODULE) PathLog.setLevel(PathLog.Level.DEBUG, LOG_MODULE) else: PathLog.setLevel(PathLog.Level.NOTICE, LOG_MODULE) # Qt translation handling def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) movecommands = ['G1', 'G01', 'G2', 'G02', 'G3', 'G03'] rapidcommands = ['G0', 'G00'] arccommands = ['G2', 'G3', 'G02', 'G03'] class ObjectDressup: def __init__(self, obj): obj.addProperty("App::PropertyLink", "Base", "Path", QtCore.QT_TRANSLATE_NOOP("Path_DressupAxisMap", "The base path to modify")) obj.addProperty("App::PropertyFile", "probefile", "ProbeData", QtCore.QT_TRANSLATE_NOOP("Path_DressupZCorrect", "The point file from the surface probing.")) obj.Proxy = self obj.addProperty("Part::PropertyPartShape", "interpSurface", "Path") obj.setEditorMode('interpSurface', 2) # hide obj.addProperty("App::PropertyDistance", "ArcInterpolate", "Interpolate", QtCore.QT_TRANSLATE_NOOP("Path_DressupZCorrect", "Deflection distance for arc interpolation")) obj.addProperty("App::PropertyDistance", "SegInterpolate", "Interpolate", QtCore.QT_TRANSLATE_NOOP("Path_DressupZCorrectp", "break segments into smaller segments of this length.")) obj.ArcInterpolate = 0.1 obj.SegInterpolate = 1.0 def __getstate__(self): return None def __setstate__(self, state): return None def onChanged(self, fp, prop): if str(prop) == "probefile": self._loadFile(fp, fp.probefile) def _bilinearInterpolate(self, surface, x, y): p1 = FreeCAD.Vector(x, y, 100.0) p2 = FreeCAD.Vector(x, y, -100.0) vertical_line = Part.Line(p1, p2) points, curves = vertical_line.intersectCS(surface) return points[0].Z def _loadFile(self, obj, filename): if filename == "": return f1 = open(filename, 'r') try: pointlist = [] for line in f1.readlines(): w = line.split() xval = round(float(w[0]), 2) yval = round(float(w[1]), 2) zval = round(float(w[2]), 2) pointlist.append([xval, yval, zval]) PathLog.debug(pointlist) cols = list(zip(*pointlist)) PathLog.debug("cols: {}".format(cols)) yindex = list(sorted(set(cols[1]))) PathLog.debug("yindex: {}".format(yindex)) array = [] for y in yindex: points = sorted([p for p in pointlist if p[1] == y]) inner = [] for p in points: inner.append(FreeCAD.Vector(p[0], p[1], p[2])) array.append(inner) intSurf = Part.BSplineSurface() intSurf.interpolate(array) obj.interpSurface = intSurf.toShape() except Exception: raise ValueError("File does not contain appropriate point data") def execute(self, obj): sampleD = obj.SegInterpolate.Value curveD = obj.ArcInterpolate.Value if obj.interpSurface.isNull(): # No valid probe data. return unchanged path obj.Path = obj.Base.Path return surface = obj.interpSurface.toNurbs().Faces[0].Surface if obj.Base: if obj.Base.isDerivedFrom("Path::Feature"): if obj.Base.Path: if obj.Base.Path.Commands: pathlist = obj.Base.Path.Commands newcommandlist = [] currLocation = {'X': 0, 'Y': 0, 'Z': 0, 'F': 0} for c in pathlist: PathLog.debug(c) PathLog.debug(" curLoc:{}".format(currLocation)) newparams = dict(c.Parameters) zval = newparams.get("Z", currLocation['Z']) if c.Name in movecommands: curVec = FreeCAD.Vector(currLocation['X'], currLocation['Y'], currLocation['Z']) arcwire = PathGeom.edgeForCmd(c, curVec) if arcwire is None: continue if c.Name in arccommands: pointlist = arcwire.discretize(Deflection=curveD) else: disc_number = int(arcwire.Length / sampleD) if disc_number > 1: pointlist = arcwire.discretize(Number=int(arcwire.Length / sampleD)) else: pointlist = [v.Point for v in arcwire.Vertexes] for point in pointlist: offset = self._bilinearInterpolate(surface, point.x, point.y) newcommand = Path.Command("G1", {'X': point.x, 'Y': point.y, 'Z': point.z + offset}) newcommandlist.append(newcommand) currLocation.update(newcommand.Parameters) currLocation['Z'] = zval else: # Non Feed Command newcommandlist.append(c) currLocation.update(c.Parameters) path = Path.Path(newcommandlist) obj.Path = path class TaskPanel: def __init__(self, obj): self.obj = obj self.form = FreeCADGui.PySideUic.loadUi(":/panels/ZCorrectEdit.ui") FreeCAD.ActiveDocument.openTransaction(translate("Path_DressupZCorrect", "Edit Z Correction Dress-up")) self.interpshape = FreeCAD.ActiveDocument.addObject("Part::Feature", "InterpolationSurface") self.interpshape.Shape = obj.interpSurface self.interpshape.ViewObject.Transparency = 60 self.interpshape.ViewObject.ShapeColor = (1.00000, 1.00000, 0.01961) self.interpshape.ViewObject.Selectable = False stock = PathUtils.findParentJob(obj).Stock self.interpshape.Placement.Base.z = stock.Shape.BoundBox.ZMax def reject(self): FreeCAD.ActiveDocument.abortTransaction() FreeCADGui.Control.closeDialog() FreeCAD.ActiveDocument.recompute() def accept(self): self.getFields() FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.removeObject(self.interpshape.Name) FreeCADGui.ActiveDocument.resetEdit() FreeCADGui.Control.closeDialog() FreeCAD.ActiveDocument.recompute() FreeCAD.ActiveDocument.recompute() def getFields(self): self.obj.Proxy.execute(self.obj) def updateUI(self): if PathLog.getLevel(LOG_MODULE) == PathLog.Level.DEBUG: for obj in FreeCAD.ActiveDocument.Objects: if obj.Name.startswith('Shape'): FreeCAD.ActiveDocument.removeObject(obj.Name) print('object name %s' % self.obj.Name) if hasattr(self.obj.Proxy, "shapes"): PathLog.info("showing shapes attribute") for shapes in self.obj.Proxy.shapes.itervalues(): for shape in shapes: Part.show(shape) else: PathLog.info("no shapes attribute found") def updateModel(self): self.getFields() self.updateUI() FreeCAD.ActiveDocument.recompute() def setFields(self): self.form.ProbePointFileName.setText(self.obj.probefile) self.updateUI() def open(self): pass def setupUi(self): self.setFields() # now that the form is filled, setup the signal handlers self.form.ProbePointFileName.editingFinished.connect(self.updateModel) self.form.SetProbePointFileName.clicked.connect(self.SetProbePointFileName) def SetProbePointFileName(self): filename = QtGui.QFileDialog.getSaveFileName(self.form, translate("Path_Probe", "Select Probe Point File"), None, translate("Path_Probe", "All Files (*.*)")) if filename and filename[0]: self.obj.probefile = str(filename[0]) self.setFields() class ViewProviderDressup: def __init__(self, vobj): vobj.Proxy = self def attach(self, vobj): self.obj = vobj.Object if self.obj and self.obj.Base: for i in self.obj.Base.InList: if hasattr(i, "Group"): group = i.Group for g in group: if g.Name == self.obj.Base.Name: group.remove(g) i.Group = group return def claimChildren(self): return [self.obj.Base] def setEdit(self, vobj, mode=0): FreeCADGui.Control.closeDialog() panel = TaskPanel(vobj.Object) FreeCADGui.Control.showDialog(panel) panel.setupUi() return True def __getstate__(self): return None def __setstate__(self, state): return None def onDelete(self, arg1=None, arg2=None): '''this makes sure that the base operation is added back to the project and visible''' FreeCADGui.ActiveDocument.getObject(arg1.Object.Base.Name).Visibility = True job = PathUtils.findParentJob(arg1.Object) job.Proxy.addOperation(arg1.Object.Base) arg1.Object.Base = None return True class CommandPathDressup: def GetResources(self): return {'Pixmap': 'Path-Dressup', 'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_DressupZCorrect", "Z Depth Correction Dress-up"), 'Accel': "", 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_DressupZCorrect", "Use Probe Map to correct Z depth")} 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): # check that the selection contains exactly what we want selection = FreeCADGui.Selection.getSelection() if len(selection) != 1: FreeCAD.Console.PrintError(translate("Path_Dressup", "Please select one path object\n")) return if not selection[0].isDerivedFrom("Path::Feature"): FreeCAD.Console.PrintError(translate("Path_Dressup", "The selected object is not a path\n")) return if selection[0].isDerivedFrom("Path::FeatureCompoundPython"): FreeCAD.Console.PrintError(translate("Path_Dressup", "Please select a Path object")) return # everything ok! FreeCAD.ActiveDocument.openTransaction(translate("Path_DressupZCorrect", "Create Dress-up")) FreeCADGui.addModule("PathScripts.PathDressupZCorrect") FreeCADGui.addModule("PathScripts.PathUtils") FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "ZCorrectDressup")') FreeCADGui.doCommand('PathScripts.PathDressupZCorrect.ObjectDressup(obj)') FreeCADGui.doCommand('obj.Base = FreeCAD.ActiveDocument.' + selection[0].Name) FreeCADGui.doCommand('PathScripts.PathDressupZCorrect.ViewProviderDressup(obj.ViewObject)') FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)') FreeCADGui.doCommand('Gui.ActiveDocument.getObject(obj.Base.Name).Visibility = False') FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() if FreeCAD.GuiUp: # register the FreeCAD command FreeCADGui.addCommand('Path_DressupZCorrect', CommandPathDressup()) FreeCAD.Console.PrintLog("Loading PathDressup... done\n")