diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 4bde8cb986..7b895527d9 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -29,6 +29,8 @@ SET(PathScripts_SRCS PathScripts/PathDressupDragknife.py PathScripts/PathDressupHoldingTags.py PathScripts/PathDressupRampEntry.py + PathScripts/PathDressupTag.py + PathScripts/PathDressupTagGui.py PathScripts/PathDressupTagPreferences.py PathScripts/PathDrilling.py PathScripts/PathEngrave.py diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index b028ce7c90..7ae7566b18 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -53,6 +53,7 @@ class PathWorkbench (Workbench): from PathScripts import PathDressupDragknife from PathScripts import PathDressupHoldingTags from PathScripts import PathDressupRampEntry + from PathScripts import PathDressupTagGui from PathScripts import PathDrilling from PathScripts import PathEngrave from PathScripts import PathFacePocket @@ -86,7 +87,7 @@ class PathWorkbench (Workbench): twodopcmdlist = ["Path_Contour", "Path_Profile", "Path_Profile_Edges", "Path_Pocket", "Path_Drilling", "Path_Engrave", "Path_MillFace", "Path_Helix"] threedopcmdlist = ["Path_Surfacing"] modcmdlist = ["Path_Copy", "Path_CompoundExtended", "Path_Array", "Path_SimpleCopy" ] - dressupcmdlist = ["PathDressup_Dogbone", "PathDressup_DragKnife", "PathDressup_HoldingTags", "PathDressup_RampEntry"] + dressupcmdlist = ["PathDressup_Dogbone", "PathDressup_DragKnife", "PathDressup_HoldingTags", "PathDressup_Tag", "PathDressup_RampEntry"] extracmdlist = ["Path_SelectLoop", "Path_Shape", "Path_Area", "Path_Area_Workplane", "Path_Stock"] #modcmdmore = ["Path_Hop",] #remotecmdlist = ["Path_Remote"] diff --git a/src/Mod/Path/PathScripts/PathDressupTag.py b/src/Mod/Path/PathScripts/PathDressupTag.py new file mode 100644 index 0000000000..f646b48952 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathDressupTag.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2017 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 * +# * * +# *************************************************************************** +import FreeCAD +import Part +import Path +import PathScripts.PathLog as PathLog +import PathScripts.PathPreferencesPathDressup as PathPreferencesPathDressup +import math +import sys + +from PathScripts.PathDressupTagPreferences import HoldingTagPreferences +from PathScripts.PathGeom import PathGeom +from PySide import QtCore + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule() + +# Qt tanslation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + +class TagSolid: + def __init__(self, proxy, z, R): + self.proxy = proxy + self.z = z + self.toolRadius = R + self.angle = math.fabs(proxy.obj.Angle) + self.width = math.fabs(proxy.obj.Width) + self.height = math.fabs(proxy.obj.Height) + self.radius = math.fabs(proxy.obj.Radius) + self.actualHeight = self.height + self.fullWidth = 2 * self.toolRadius + self.width + + r1 = self.fullWidth / 2 + self.r1 = r1 + self.r2 = r1 + height = self.actualHeight * 1.01 + radius = 0 + if self.angle == 90 and height > 0: + # cylinder + self.solid = Part.makeCylinder(r1, height) + radius = min(min(self.radius, r1), self.height) + PathLog.debug("Part.makeCylinder(%f, %f)" % (r1, height)) + elif self.angle > 0.0 and height > 0.0: + # cone + rad = math.radians(self.angle) + tangens = math.tan(rad) + dr = height / tangens + if dr < r1: + # with top + r2 = r1 - dr + s = height / math.sin(rad) + radius = min(r2, s) * math.tan((math.pi - rad)/2) * 0.95 + else: + # triangular + r2 = 0 + height = r1 * tangens * 1.01 + self.actualHeight = height + self.r2 = r2 + PathLog.debug("Part.makeCone(r1=%.2f, r2=%.2f, h=%.2f)" % (r1, r2, height)) + self.solid = Part.makeCone(r1, r2, height) + else: + # degenerated case - no tag + PathLog.debug("Part.makeSphere(%.2f)" % (r1 / 10000)) + self.solid = Part.makeSphere(r1 / 10000) + + radius = min(self.radius, radius) + self.realRadius = radius + if radius != 0: + PathLog.debug("makeFillet(%.4f)" % radius) + self.solid = self.solid.makeFillet(radius, [self.solid.Edges[0]]) + + #lastly determine the center of the model, we want to make sure the seam of + # the tag solid points away (in the hopes it doesn't coincide with a path) + self.baseCenter = FreeCAD.Vector((proxy.ptMin.x+proxy.ptMax.x)/2, (proxy.ptMin.y+proxy.ptMax.y)/2, 0) + + def cloneAt(self, pos): + clone = self.solid.copy() + pos.z = 0 + angle = -PathGeom.getAngle(pos - self.baseCenter) * 180 / math.pi + clone.rotate(FreeCAD.Vector(0,0,0), FreeCAD.Vector(0,0,1), angle) + pos.z = self.z - self.actualHeight * 0.01 + clone.translate(pos) + return clone + + +class ObjectDressup(QtCore.QObject): + changed = QtCore.Signal() + def __init__(self, obj): + obj.Proxy = self + self.obj = obj + obj.addProperty('App::PropertyLink', 'Base','Base', QtCore.QT_TRANSLATE_NOOP('PathDressup_Tag', 'The base path to modify')) + obj.addProperty('App::PropertyLength', 'Width', 'Tag', QtCore.QT_TRANSLATE_NOOP('PathDressup_Tag', 'Width of tags.')) + obj.addProperty('App::PropertyLength', 'Height', 'Tag', QtCore.QT_TRANSLATE_NOOP('PathDressup_Tag', 'Height of tags.')) + obj.addProperty('App::PropertyAngle', 'Angle', 'Tag', QtCore.QT_TRANSLATE_NOOP('PathDressup_Tag', 'Angle of tag plunge and ascent.')) + obj.addProperty('App::PropertyLength', 'Radius', 'Tag', QtCore.QT_TRANSLATE_NOOP('PathDressup_Tag', 'Radius of the fillet for the tag.')) + obj.addProperty('App::PropertyVectorList', 'Positions', 'Tag', QtCore.QT_TRANSLATE_NOOP('PathDressup_Tag', 'Locations of insterted holding tags')) + obj.addProperty('App::PropertyIntegerList', 'Disabled', 'Tag', QtCore.QT_TRANSLATE_NOOP('PathDressup_Tag', 'Ids of disabled holding tags')) + obj.addProperty('App::PropertyInteger', 'SegmentationFactor', 'Tag', QtCore.QT_TRANSLATE_NOOP('PathDressup_Tag', 'Factor determining the # segments used to approximate rounded tags.')) + obj.addProperty('App::PropertyLink', 'Debug', 'Debug', QtCore.QT_TRANSLATE_NOOP('PathDressup_Tag', 'Some elements for debugging')) + if PathLog.getLevel(PathLog.thisModule()) != PathLog.Level.DEBUG: + obj.setEditorMode('Debug', 2) # hide + dbg = obj.Document.addObject('App::DocumentObjectGroup', 'TagDebug') + obj.Debug = dbg + self.solids = [] + super(ObjectDressup, self).__init__() + + def __getstate__(self): + return None + + def __setstate__(self, state): + return None + + def assignDefaultValues(self): + self.obj.Width = HoldingTagPreferences.defaultWidth(self.toolRadius() * 2) + self.obj.Height = HoldingTagPreferences.defaultHeight(self.toolRadius()) + self.obj.Angle = HoldingTagPreferences.defaultAngle() + self.obj.Radius = HoldingTagPreferences.defaultRadius() + + def execute(self, obj): + PathLog.track() + if not obj.Base: + PathLog.error(translate('PathDressup_Tag', 'No Base object found.')) + return + if not obj.Base.isDerivedFrom('Path::Feature'): + PathLog.error(translate('PathDressup_Tag', 'Base is not a Path::Feature object.')) + return + if not obj.Base.Path: + PathLog.error(translate('PathDressup_Tag', 'Base doesn\'t have a Path to dress-up.')) + return + if not obj.Base.Path.Commands: + PathLog.error(translate('PathDressup_Tag', 'Base Path is empty.')) + return + + self.obj = obj; + + minZ = sys.maxint + minX = minZ + minY = minZ + + maxZ = -sys.maxint + maxX = maxZ + maxY = maxZ + + # the assumption is that all helixes are in the xy-plane - in other words there is no + # intermittent point of a command that has a lower/higer Z-position than the start and + # and end positions of a command. + lastPt = FreeCAD.Vector(0, 0, 0) + for cmd in obj.Base.Path.Commands: + pt = PathGeom.commandEndPoint(cmd, lastPt) + if lastPt.x != pt.x: + maxX = max(pt.x, maxX) + minX = min(pt.x, minX) + if lastPt.y != pt.y: + maxY = max(pt.y, maxY) + minY = min(pt.y, minY) + if lastPt.z != pt.z: + maxZ = max(pt.z, maxZ) + minZ = min(pt.z, minZ) + lastPt = pt + PathLog.debug("bb = (%.2f, %.2f, %.2f) ... (%.2f, %.2f, %.2f)" % (minX, minY, minZ, maxX, maxY, maxZ)) + self.ptMin = FreeCAD.Vector(minX, minY, minZ) + self.ptMax = FreeCAD.Vector(maxX, maxY, maxZ) + self.masterSolid = TagSolid(self, minZ, self.toolRadius()) + self.solids = [self.masterSolid.cloneAt(pos) for pos in self.obj.Positions] + self.changed.emit() + PathLog.track() + + def toolRadius(self): + return self.obj.Base.ToolController.Tool.Diameter / 2.0 + + def addTagsToDocuemnt(self): + for i, solid in enumerate(self.solids): + obj = FreeCAD.ActiveDocument.addObject('Part::Compound', "tag_%02d" % i) + obj.Shape = solid + + +PathLog.notice('Loading PathDressupTag... done\n') diff --git a/src/Mod/Path/PathScripts/PathDressupTagGui.py b/src/Mod/Path/PathScripts/PathDressupTagGui.py new file mode 100644 index 0000000000..50b9364024 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathDressupTagGui.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2017 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 * +# * * +# *************************************************************************** +import FreeCAD +import FreeCADGui +import PathScripts.PathLog as PathLog +import PathScripts.PathUtils as PathUtils + +from PathScripts.PathPreferences import PathPreferences +from PySide import QtCore +from pivy import coin + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule() + +# Qt tanslation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + +class HoldingTagMarker: + def __init__(self, point, colors): + self.point = point + self.color = colors + self.sep = coin.SoSeparator() + self.pos = coin.SoTranslation() + self.pos.translation = (point.x, point.y, point.z) + self.sphere = coin.SoSphere() + self.scale = coin.SoType.fromName('SoShapeScale').createInstance() + self.scale.setPart('shape', self.sphere) + self.scale.scaleFactor.setValue(7) + self.material = coin.SoMaterial() + self.sep.addChild(self.pos) + self.sep.addChild(self.material) + self.sep.addChild(self.scale) + self.enabled = True + self.selected = False + + def setSelected(self, select): + self.selected = select + self.sphere.radius = 1.5 if select else 1.0 + self.setEnabled(self.enabled) + + def setEnabled(self, enabled): + self.enabled = enabled + if enabled: + self.material.diffuseColor = self.color[0] if not self.selected else self.color[2] + self.material.transparency = 0.0 + else: + self.material.diffuseColor = self.color[1] if not self.selected else self.color[2] + self.material.transparency = 0.6 + +class PathDressupTagViewProvider: + + def __init__(self, vobj): + PathLog.track() + vobj.Proxy = self + self.vobj = vobj + self.panel = None + + def setupColors(self): + def colorForColorValue(val): + v = [((val >> n) & 0xff) / 255. for n in [24, 16, 8, 0]] + return coin.SbColor(v[0], v[1], v[2]) + + pref = PathPreferences.preferences() + # R G B A + npc = pref.GetUnsigned('DefaultPathMarkerColor', (( 85*256 + 255)*256 + 0)*256 + 255) + hpc = pref.GetUnsigned('DefaultHighlightPathColor', ((255*256 + 125)*256 + 0)*256 + 255) + dpc = pref.GetUnsigned('DefaultDisabledPathColor', ((205*256 + 205)*256 + 205)*256 + 154) + self.colors = [colorForColorValue(npc), colorForColorValue(dpc), colorForColorValue(hpc)] + + def attach(self, vobj): + PathLog.track() + self.setupColors() + self.obj = vobj.Object + self.tags = [] + self.switch = coin.SoSwitch() + vobj.RootNode.addChild(self.switch) + self.turnMarkerDisplayOn(False) + + if self.obj and self.obj.Base: + for i in self.obj.Base.InList: + if hasattr(i, 'Group') and self.obj.Base.Name in [o.Name for o in i.Group]: + i.Group = [o for o in i.Group if o.Name != self.obj.Base.Name] + if self.obj.Base.ViewObject: + PathLog.info("Setting visibility for %s" % (self.obj.Base.Name)) + self.obj.Base.ViewObject.Visibility = False + else: + PathLog.info("Ignoring visibility") + if PathLog.getLevel(PathLog.thisModule()) != PathLog.Level.DEBUG and self.obj.Debug.ViewObject: + self.obj.Debug.ViewObject.Visibility = False + + self.obj.Proxy.changed.connect(self.onModelChanged) + + def turnMarkerDisplayOn(self, display): + sw = coin.SO_SWITCH_ALL if display else coin.SO_SWITCH_NONE + self.switch.whichChild = sw + + def claimChildren(self): + PathLog.track() + return [self.obj.Base, self.obj.Debug] + + def onDelete(self, arg1=None, arg2=None): + PathLog.track() + '''this makes sure that the base operation is added back to the project and visible''' + if self.obj.Base.ViewObject: + self.obj.Base.ViewObject.Visibility = True + PathUtils.addToJob(arg1.Object.Base) + self.obj.Debug.removeObjectsFromDocument() + self.obj.Debug.Document.removeObject(self.obj.Debug.Name) + self.obj.Debug = None + return True + + def updateData(self, obj, propName): + PathLog.track(propName) + if 'Disabled' == propName: + for tag in self.tags: + self.switch.removeChild(tag.sep) + tags = [] + for i, p in enumerate(obj.Positions): + tag = HoldingTagMarker(p, self.colors) + tag.setEnabled(not i in obj.Disabled) + tags.append(tag) + self.switch.addChild(tag.sep) + self.tags = tags + + def onModelChanged(self): + PathLog.track() + self.obj.Debug.removeObjectsFromDocument() + for solid in self.obj.Proxy.solids: + tag = self.obj.Document.addObject('Part::Feature', 'tag') + tag.Shape = solid + if tag.ViewObject and self.obj.Debug.ViewObject: + tag.ViewObject.Visibility = self.obj.Debug.ViewObject.Visibility + tag.ViewObject.Transparency = 80 + self.obj.Debug.addObject(tag) + tag.purgeTouched() + + def selectTag(self, index): + PathLog.track(index) + for i, tag in enumerate(self.tags): + tag.setSelected(i == index) + + def tagAtPoint(self, point): + p = FreeCAD.Vector(point[0], point[1], point[2]) + for i, tag in enumerate(self.tags): + if PathGeom.pointsCoincide(p, tag.point, tag.sphere.radius.getValue() * 1.1): + return i + return -1 + + # SelectionObserver interface + def allow(self, doc, obj, sub): + if obj == self.obj: + return True + return False + + def addSelection(self, doc, obj, sub, point): + i = self.tagAtPoint(point) + if self.panel: + self.panel.selectTagWithId(i) + FreeCADGui.updateGui() + +class CommandPathDressupTag: + + def GetResources(self): + return {'Pixmap': 'Path-Dressup', + 'MenuText': QtCore.QT_TRANSLATE_NOOP('PathDressup_Tag', 'Tag Dress-up'), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP('PathDressup_Tag', 'Creates a Tag Dress-up object from a selected path')} + + 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: + PathLog.error(translate('PathDressup_Tag', 'Please select one path object\n')) + return + baseObject = selection[0] + if not baseObject.isDerivedFrom('Path::Feature'): + PathLog.error(translate('PathDressup_Tag', 'The selected object is not a path\n')) + return + if baseObject.isDerivedFrom('Path::FeatureCompoundPython'): + PathLog.error(translate('PathDressup_Tag', 'Please select a Profile object')) + return + + # everything ok! + FreeCAD.ActiveDocument.openTransaction(translate('PathDressup_Tag', 'Create Tag Dress-up')) + FreeCADGui.addModule('PathScripts.PathDressupTag') + FreeCADGui.addModule('PathScripts.PathDressupTagGui') + FreeCADGui.addModule('PathScripts.PathUtils') + FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "TagDressup")') + FreeCADGui.doCommand('dbo = PathScripts.PathDressupTag.ObjectDressup(obj)') + FreeCADGui.doCommand('obj.Base = FreeCAD.ActiveDocument.' + selection[0].Name) + FreeCADGui.doCommand('PathScripts.PathDressupTagGui.PathDressupTagViewProvider(obj.ViewObject)') + FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)') + FreeCADGui.doCommand('dbo.assignDefaultValues()') + FreeCADGui.doCommand('obj.Positions = [App.Vector(-10, -10, 0), App.Vector(10, 10, 0)]') + FreeCADGui.doCommand('dbo.execute(obj)') + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + +if FreeCAD.GuiUp: + # register the FreeCAD command + FreeCADGui.addCommand('PathDressup_Tag', CommandPathDressupTag()) + +PathLog.notice('Loading PathDressupTagGui... done\n') diff --git a/src/Mod/Path/PathScripts/PathUtils.py b/src/Mod/Path/PathScripts/PathUtils.py index 1c813427bd..431fdb8850 100644 --- a/src/Mod/Path/PathScripts/PathUtils.py +++ b/src/Mod/Path/PathScripts/PathUtils.py @@ -24,15 +24,16 @@ '''PathUtils -common functions used in PathScripts for filterig, sorting, and generating gcode toolpath data ''' import FreeCAD import FreeCADGui -import Part import math -from DraftGeomUtils import geomType -import PathScripts -from PathScripts import PathJob import numpy -from PathScripts import PathLog -from FreeCAD import Vector +import Part import Path +import PathScripts + +from DraftGeomUtils import geomType +from FreeCAD import Vector +from PathScripts import PathJob +from PathScripts import PathLog from PySide import QtCore from PySide import QtGui @@ -455,13 +456,12 @@ def addToJob(obj, jobname=None): if len(jobs) == 1: job = jobs[0] else: - FreeCAD.Console.PrintError("Didn't find the job") + PathLog.error("Job %s does not exist" % jobname) return None else: jobs = GetJobs() if len(jobs) == 0: job = PathJob.CommandJob.Create() - elif len(jobs) == 1: job = jobs[0] else: @@ -736,15 +736,6 @@ def guessDepths(objshape, subs=None): return depth_params(clearance, safe, start, 1.0, 0.0, final, user_depths=None, equalstep=False) -def drillTipLength(tool): - """returns the length of the drillbit tip. -""" - if tool.CuttingEdgeAngle == 0.0 or tool.Diameter == 0.0: - return 0.0 - else: - theta = math.radians(tool.CuttingEdgeAngle) - return (tool.Diameter/2) / math.tan(theta) - class depth_params: '''calculates the intermediate depth values for various operations given the starting, ending, and stepdown parameters