From 730e2c523ee79b97559055a1bdbd56da07f4a36c Mon Sep 17 00:00:00 2001 From: ml Date: Sat, 29 Oct 2016 22:43:32 -0700 Subject: [PATCH 01/17] First shot at base algorithm for inserting holding tabs. --- src/Mod/Path/CMakeLists.txt | 1 + src/Mod/Path/Gui/Resources/Path.qrc | 1 + .../Gui/Resources/panels/HoldingTabsEdit.ui | 121 +++++ src/Mod/Path/InitGui.py | 10 +- .../PathScripts/PathDressupHoldingTabs.py | 514 ++++++++++++++++++ 5 files changed, 645 insertions(+), 2 deletions(-) create mode 100644 src/Mod/Path/Gui/Resources/panels/HoldingTabsEdit.ui create mode 100644 src/Mod/Path/PathScripts/PathDressupHoldingTabs.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index c4cf6bd04..225732aa1 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -38,6 +38,7 @@ SET(PathScripts_SRCS PathScripts/PathDrilling.py PathScripts/PathDressup.py PathScripts/DragknifeDressup.py + PathScripts/PathDressupHoldingTabs.py PathScripts/PathHop.py PathScripts/PathUtils.py PathScripts/PathSelection.py diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index 27353e675..7ad296a00 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -90,5 +90,6 @@ panels/DogboneEdit.ui panels/DlgSelectPostProcessor.ui preferences/PathJob.ui + panels/HoldingTabsEdit.ui diff --git a/src/Mod/Path/Gui/Resources/panels/HoldingTabsEdit.ui b/src/Mod/Path/Gui/Resources/panels/HoldingTabsEdit.ui new file mode 100644 index 000000000..35c18c9d4 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/HoldingTabsEdit.ui @@ -0,0 +1,121 @@ + + + TaskPanel + + + + 0 + 0 + 352 + 387 + + + + Holding Tabs + + + + + + QFrame::NoFrame + + + 0 + + + + + 0 + 0 + 334 + 340 + + + + Holding Tabs + + + + + + Count + + + + + + + Width + + + + + + + + 0 + 0 + + + + <html><head/><body><p>List of holding tabs calculated by the paremeters entered above. You can un-check tabs you don't want to be inserted.</p></body></html> + + + + + + + <html><head/><body><p>Enter the number of tabs you wish to have.</p><p><br/></p><p>Note that sometimes it's necessary to enter a larger than desired count number and disable the ones tabs you don't want in order to get the holding tab layout you want.</p></body></html> + + + + + + + <html><head/><body><p>Width of each tab.</p></body></html> + + + + + + + Height + + + + + + + <html><head/><body><p>The height of the holding tab measured from the bottom of the path. By default this is set to the (estimated) height of the path.</p></body></html> + + + + + + + true + + + Angle + + + + + + + true + + + <html><head/><body><p>Angle of tab walls.</p></body></html> + + + + + + + + + + + + diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 468f07fa9..68bc0bdc7 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -75,6 +75,7 @@ class PathWorkbench (Workbench): from PathScripts import PathProfileEdges from PathScripts import DogboneDressup from PathScripts import PathMillFace + from PathScripts import PathDressupHoldingTabs import PathCommands # build commands list @@ -84,7 +85,7 @@ class PathWorkbench (Workbench): twodopcmdlist = ["Path_Contour", "Path_Profile", "Path_Profile_Edges", "Path_Pocket", "Path_Drilling", "Path_Engrave", "Path_MillFace"] threedopcmdlist = ["Path_Surfacing"] modcmdlist = ["Path_Copy", "Path_CompoundExtended", "Path_Array", "Path_SimpleCopy" ] - dressupcmdlist = ["Dogbone_Dressup", "DragKnife_Dressup"] + dressupcmdlist = ["Dogbone_Dressup", "DragKnife_Dressup", "PathDressup_HoldingTabs"] extracmdlist = ["Path_SelectLoop"] #modcmdmore = ["Path_Hop",] #remotecmdlist = ["Path_Remote"] @@ -119,6 +120,11 @@ class PathWorkbench (Workbench): # "Path", "Remote Operations")], remotecmdlist) self.appendMenu([translate("Path", "&Path")], extracmdlist) + # Add preferences pages + import os + FreeCADGui.addPreferencePage(FreeCAD.getHomePath( + ) + os.sep + "Mod" + os.sep + "Path" + os.sep + "PathScripts" + os.sep + "DlgSettingsPath.ui", "Path") + Log('Loading Path workbench... done\n') def GetClassName(self): @@ -136,7 +142,7 @@ class PathWorkbench (Workbench): if len(FreeCADGui.Selection.getSelection()) == 1: if FreeCADGui.Selection.getSelection()[0].isDerivedFrom("Path::Feature"): self.appendContextMenu("", ["Path_Inspect"]) - if "Profile" or "Contour" in FreeCADGui.Selection.getSelection()[0].Name: + if "Profile" in FreeCADGui.Selection.getSelection()[0].Name: self.appendContextMenu("", ["Add_Tag"]) self.appendContextMenu("", ["Set_StartPoint"]) self.appendContextMenu("", ["Set_EndPoint"]) diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTabs.py b/src/Mod/Path/PathScripts/PathDressupHoldingTabs.py new file mode 100644 index 000000000..2f12b0956 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTabs.py @@ -0,0 +1,514 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2014 Yorik van Havre * +# * * +# * 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 Path +from PathScripts import PathUtils +from PySide import QtCore, QtGui +import math +import Part +import DraftGeomUtils + +"""Holding Tabs Dressup object and FreeCAD command""" + +# Qt tanslation handling +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + + def translate(context, text, disambig=None): + return QtGui.QApplication.translate(context, text, disambig, _encoding) + +except AttributeError: + + def translate(context, text, disambig=None): + return QtGui.QApplication.translate(context, text, disambig) + +debugDressup = True + +def debugMarker(vector, label, color = None, radius = 0.5): + if debugDressup: + obj = FreeCAD.ActiveDocument.addObject("Part::Sphere", label) + obj.Label = label + obj.Radius = radius + obj.Placement = FreeCAD.Placement(vector, FreeCAD.Rotation(FreeCAD.Vector(0,0,1), 0)) + if color: + obj.ViewObject.ShapeColor = color + +movecommands = ['G0', 'G00', 'G1', 'G01', 'G2', 'G02', 'G3', 'G03'] +movestraight = ['G1', 'G01'] +movecw = ['G2', 'G02'] +moveccw = ['G3', 'G03'] +movearc = movecw + moveccw + +def getAngle(v): + a = v.getAngle(FreeCAD.Vector(1,0,0)) + if v.x < 0 and v.y < 0: + return a - math.pi/2 + if v.y < 0: + return -a + return a + +class PathData: + def __init__(self, obj): + self.obj = obj + self.edges = [] + lastPt = FreeCAD.Vector(0, 0, 0) + for cmd in obj.Base.Path.Commands: + if cmd.Name in movecommands: + pt = self.point(cmd, lastPt) + if cmd.Name in movestraight: + self.edges.append(Part.Edge(Part.Line(lastPt, pt))) + elif cmd.Name in movearc: + center = lastPt + self.point(cmd, FreeCAD.Vector(0,0,0), 'I', 'J', 'K') + A = lastPt - center + B = pt - center + a = getAngle(A) + b = getAngle(B) + if cmd.Name in movecw and a < 0 and math.fabs(math.pi - b) < 0.0000001: + angle = (a - b) / 2 + elif cmd.Name in moveccw and math.fabs(math.pi - a) < 0.0000001 and b < 0: + angle = (b - a) / 2 + else: + angle = (a + b) / 2. + d = -B.x * A.y + B.y * A.x + + R = (lastPt - center).Length + ptm = center + FreeCAD.Vector(math.cos(angle), math.sin(angle), 0) * R + #if pt.z == 2.: # and pt.x == 10. and math.fabs(pt.y) == 12.5: + # #print("%s (%.2f %.2f) -> (%.2f, %.2f): center=(%.2f, %.2f)" % (cmd, lastPt.x, lastPt.y, pt.x, pt.y, center.x, center.y)) + # print(" a = %+.2f b = %+.2f) %+.2f -> angle = %+.2f r = %.2f" % (a/math.pi, b/math.pi, d, angle/math.pi, R)) + # debugMarker(lastPt, 'arc', (1.0, 1.0, 0.0), 0.3) + # debugMarker(ptm, 'arc', (1.0, 1.0, 0.0), 0.3) + # debugMarker(pt, 'arc', (1.0, 1.0, 0.0), 0.3) + self.edges.append(Part.Edge(Part.Arc(lastPt, ptm, pt))) + lastPt = pt + self.base = self.findBottomWire(self.edges) + # determine overall length + self.length = self.base.Length + + def point(self, cmd, pt, X='X', Y='Y', Z='Z'): + x = cmd.Parameters.get(X, pt.x) + y = cmd.Parameters.get(Y, pt.y) + z = cmd.Parameters.get(Z, pt.z) + return FreeCAD.Vector(x, y, z) + + def findBottomWire(self, edges): + (minZ, maxZ) = self.findZLimits(edges) + self.minZ = minZ + self.maxZ = maxZ + bottom = [e for e in edges if e.Vertexes[0].Point.z == minZ and e.Vertexes[1].Point.z == minZ] + wire = Part.Wire(bottom) + if wire.isClosed(): + return wire + # if we get here there are already holding tabs, or we're not looking at a profile + # let's try and insert the missing pieces - another day + raise ValueError("Selected path doesn't seem to be a Profile operation.") + + + def findZLimits(self, edges): + # not considering arcs and spheres in Z direction, find the highes and lowest Z values + minZ = edges[0].Vertexes[0].Point.z + maxZ = minZ + for e in edges: + for v in e.Vertexes: + if v.Point.z < minZ: + minZ = v.Point.z + if v.Point.z > maxZ: + maxZ = v.Point.z + return (minZ, maxZ) + +class ObjectDressup: + + def __init__(self, obj): + self.obj = obj + obj.addProperty("App::PropertyLink", "Base","Base", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "The base path to modify")) + obj.addProperty("App::PropertyInteger", "Count", "Tab", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "The number of holding tabs to be generated")) + obj.addProperty("App::PropertyFloat", "Height", "Tab", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "Height of holding tabs measured from bottom of path")) + obj.addProperty("App::PropertyFloat", "Width", "Tab", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "Width of each tab at its base")) + obj.addProperty("App::PropertyFloat", "Angle", "Tab", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "Angle of plunge used for tabs")) + obj.addProperty("App::PropertyIntegerList", "Blacklist", "Tab", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "IDs of disabled paths")) + obj.setEditorMode("Blacklist", 2) + obj.Proxy = self + + def __getstate__(self): + return None + + def __setstate__(self, state): + return None + + def getFingerprint(self, obj): + if hasattr(self, 'pathData'): + return "%d-%.2f-%.2f-%.2f-%s" % (obj.Count, obj.Height, obj.Width, obj.Angle, str(obj.Blacklist)) + return '' + + def execute(self, obj): + if not obj.Base: + return + if not obj.Base.isDerivedFrom("Path::Feature"): + return + if not obj.Base.Path: + return + if not obj.Base.Path.Commands: + return + + pathData = self.setup(obj) + if not pathData: + return + + if hasattr(self, 'fingerprint') and self.fingerprint and self.fingerprint == self.getFingerprint(obj): + return + + self.fingerprint = self.getFingerprint(obj) + + #for e in pathData.base.Edges: + # debugMarker(e.Vertexes[0].Point, 'base', (0.0, 1.0, 1.0), 0.2) + + if obj.Count == 0: + obj.Path = obj.Base.Path + return + + tabDistance = pathData.base.Length / obj.Count + + # start assigning tabs on the longest segment + maxLen = sorted(pathData.base.Edges, key=lambda e: -e.Length)[0].Length + startIndex = 0 + for i in range(0, len(pathData.base.Edges)): + edge = pathData.base.Edges[i] + if edge.Length == maxLen: + startIndex = i + break + + startEdge = pathData.base.Edges[startIndex] + startCount = int(startEdge.Length / tabDistance) + 1 + + lastTabLength = (startEdge.Length + (startCount - 1) * tabDistance) / 2 + if lastTabLength < 0 or lastTabLength > startEdge.Length: + lastTabLength = startEdge.Length / 2 + currentLength = startEdge.Length + minLength = 2 * obj.Width + + print("start index=%-2d -> count=%d (length=%.2f, distance=%.2f)" % (startIndex, startCount, startEdge.Length, tabDistance)) + print(" -> lastTabLength=%.2f)" % lastTabLength) + print(" -> currentLength=%.2f)" % currentLength) + + tabs = { startIndex: startCount } + + for i in range(startIndex + 1, len(pathData.base.Edges)): + edge = pathData.base.Edges[i] + (currentLength, lastTabLength) = self.executeTabLength(i, edge, currentLength, lastTabLength, tabDistance, minLength, tabs) + for i in range(0, startIndex): + edge = pathData.base.Edges[i] + (currentLength, lastTabLength) = self.executeTabLength(i, edge, currentLength, lastTabLength, tabDistance, minLength, tabs) + + self.tabs = tabs + locs = {} + + tabID = 0 + for (i, count) in tabs.iteritems(): + edge = pathData.base.Edges[i] + #debugMarker(edge.Vertexes[0].Point, 'base', (1.0, 0.0, 0.0), 0.2) + #debugMarker(edge.Vertexes[1].Point, 'base', (0.0, 1.0, 0.0), 0.2) + distance = (edge.LastParameter - edge.FirstParameter) / count + for j in range(0, count): + tab = edge.Curve.value((j+0.5) * distance) + tabID += 1 + locs[(tab.x, tab.y)] = tabID + if not tabID in obj.Blacklist: + debugMarker(tab, "tab-%02d" % tabID , (1.0, 0.0, 1.0), 0.5) + + self.tabLocations = locs + #debugMarker(pathData.base.CenterOfMass, 'cog', (0., 0., 0.), 0.5) + + obj.Path = obj.Base.Path + + def executeTabLength(self, index, edge, currentLength, lastTabLength, tabDistance, minLength, tabs): + tabCount = 0 + currentLength += edge.Length + if edge.Length > minLength: + while lastTabLength + tabDistance < currentLength: + tabCount += 1 + lastTabLength += tabDistance + if tabCount > 0: + #print(" index=%d -> count=%d" % (index, tabCount)) + tabs[index] = tabCount + else: + print(" skipping=%-2d (%.2f)" % (index, edge.Length)) + return (currentLength, lastTabLength) + + def holdingTabsLocations(self, obj): + if hasattr(self, "tabLocations") and self.tabLocations: + return self.tabLocations + return {} + + def setup(self, obj): + if not hasattr(self, "pathData") or not self.pathData: + try: + pathData = PathData(obj) + except ValueError: + FreeCAD.Console.PrintError(translate("PathDressup_HoldingTabs", "Cannot insert holding tabs for this path - please select a Profile path\n")) + return None + + # setup the object's properties, in case they're not set yet + obj.Count = self.tabCount(obj) + obj.Angle = self.tabAngle(obj) + obj.Blacklist = self.tabBlacklist(obj) + + # if the heigt isn't set, use the height of the path + if not hasattr(obj, "Height") or not obj.Height: + obj.Height = pathData.maxZ - pathData.minZ + # try and take an educated guess at the width + if not hasattr(obj, "Width") or not obj.Width: + width = sorted(pathData.base.Edges, key=lambda e: -e.Length)[0].Length / 10 + while obj.Count > len([e for e in pathData.base.Edges if e.Length > 3*width]): + width = widht / 2 + obj.Width = width + + # and the tool radius, not sure yet if it's needed + self.toolRadius = 5 + toolLoad = PathUtils.getLastToolLoad(obj) + if toolLoad is None or toolLoad.ToolNumber == 0: + self.toolRadius = 5 + else: + tool = PathUtils.getTool(obj, toolLoad.ToolNumber) + if not tool or tool.Diameter == 0: + self.toolRadius = 5 + else: + self.toolRadius = tool.Diameter / 2 + self.pathData = pathData + return self.pathData + + def tabCount(self, obj): + if hasattr(obj, "Count") and obj.Count: + return obj.Count + return 4 + + def tabHeight(self, obj): + if hasattr(obj, "Height") and obj.Height: + return obj.Height + return 1 + + def tabWidth(self, obj): + if hasattr(obj, "Width") and obj.Width: + return obj.Width + return 3 + + def tabAngle(self, obj): + if hasattr(obj, "Angle") and obj.Angle: + return obj.Angle + return 90 + + def tabBlacklist(self, obj): + if hasattr(obj, "Blacklist") and obj.Blacklist: + return obj.Blacklist + return [] + +class TaskPanel: + DataId = QtCore.Qt.ItemDataRole.UserRole + + def __init__(self, obj): + self.obj = obj + self.form = FreeCADGui.PySideUic.loadUi(":/panels/HoldingTabsEdit.ui") + FreeCAD.ActiveDocument.openTransaction(translate("PathDressup_HoldingTabs", "Edit HoldingTabs Dress-up")) + + def reject(self): + FreeCAD.ActiveDocument.abortTransaction() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + FreeCADGui.Selection.removeObserver(self.s) + + def accept(self): + FreeCAD.ActiveDocument.commitTransaction() + FreeCADGui.ActiveDocument.resetEdit() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + FreeCADGui.Selection.removeObserver(self.s) + FreeCAD.ActiveDocument.recompute() + + def open(self): + self.s = SelObserver() + # install the function mode resident + FreeCADGui.Selection.addObserver(self.s) + + def updateTabList(self): + blacklist = self.obj.Proxy.tabBlacklist(self.obj) + itemList = [] + for (x,y), id in self.obj.Proxy.holdingTabsLocations(self.obj).iteritems(): + label = "%d: (x=%.2f, y=%.2f)" % (id, x, y) + item = QtGui.QListWidgetItem(label) + if id in blacklist: + item.setCheckState(QtCore.Qt.CheckState.Unchecked) + else: + item.setCheckState(QtCore.Qt.CheckState.Checked) + item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsUserCheckable) + item.setData(self.DataId, id) + itemList.append(item) + self.form.lwHoldingTabs.clear() + for item in sorted(itemList, key=lambda item: item.data(self.DataId)): + self.form.lwHoldingTabs.addItem(item) + + def getFields(self): + self.obj.Count = self.form.sbCount.value() + self.obj.Height = self.form.dsbHeight.value() + self.obj.Width = self.form.dsbWidth.value() + self.obj.Angle = self.form.dsbAngle.value() + blacklist = [] + for i in range(0, self.form.lwHoldingTabs.count()): + item = self.form.lwHoldingTabs.item(i) + if item.checkState() == QtCore.Qt.CheckState.Unchecked: + blacklist.append(item.data(self.DataId)) + self.obj.Blacklist = sorted(blacklist) + self.obj.Proxy.execute(self.obj) + + def update(self): + if True or debugDressup: + for obj in FreeCAD.ActiveDocument.Objects: + if obj.Name.startswith('tab'): + FreeCAD.ActiveDocument.removeObject(obj.Name) + self.getFields() + self.updateTabList() + FreeCAD.ActiveDocument.recompute() + + def setupSpinBox(self, widget, val, decimals = 2): + widget.setMinimum(0) + if decimals: + widget.setDecimals(decimals) + widget.setValue(val) + + + def setFields(self): + self.setupSpinBox(self.form.sbCount, self.obj.Proxy.tabCount(self.obj), None) + self.setupSpinBox(self.form.dsbHeight, self.obj.Proxy.tabHeight(self.obj)) + self.setupSpinBox(self.form.dsbWidth, self.obj.Proxy.tabWidth(self.obj)) + self.setupSpinBox(self.form.dsbAngle, self.obj.Proxy.tabAngle(self.obj)) + self.updateTabList() + + def setupUi(self): + self.setFields() + self.form.sbCount.valueChanged.connect(self.update) + self.form.dsbHeight.valueChanged.connect(self.update) + self.form.dsbWidth.valueChanged.connect(self.update) + self.form.dsbAngle.valueChanged.connect(self.update) + self.form.lwHoldingTabs.itemChanged.connect(self.update) + +class SelObserver: + def __init__(self): + import PathScripts.PathSelection as PST + PST.eselect() + + def __del__(self): + import PathScripts.PathSelection as PST + PST.clear() + + def addSelection(self, doc, obj, sub, pnt): + FreeCADGui.doCommand('Gui.Selection.addSelection(FreeCAD.ActiveDocument.' + obj + ')') + FreeCADGui.updateGui() + +class ViewProviderDressup: + + def __init__(self, vobj): + vobj.Proxy = self + + def attach(self, vobj): + self.Object = vobj.Object + return + + def claimChildren(self): + for i in self.Object.Base.InList: + if hasattr(i, "Group"): + group = i.Group + for g in group: + if g.Name == self.Object.Base.Name: + group.remove(g) + i.Group = group + print i.Group + #FreeCADGui.ActiveDocument.getObject(obj.Base.Name).Visibility = False + return [self.Object.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 + PathUtils.addToJob(arg1.Object.Base) + return True + +class CommandPathDressupHoldingTabs: + + def GetResources(self): + return {'Pixmap': 'Path-Dressup', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "HoldingTabs Dress-up"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "Creates a HoldingTabs 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: + FreeCAD.Console.PrintError(translate("PathDressup_HoldingTabs", "Please select one path object\n")) + return + baseObject = selection[0] + if not baseObject.isDerivedFrom("Path::Feature"): + FreeCAD.Console.PrintError(translate("PathDressup_HoldingTabs", "The selected object is not a path\n")) + return + if baseObject.isDerivedFrom("Path::FeatureCompoundPython"): + FreeCAD.Console.PrintError(translate("PathDressup_HoldingTabs", "Please select a Profile object")) + return + + # everything ok! + FreeCAD.ActiveDocument.openTransaction(translate("PathDressup_HoldingTabs", "Create HoldingTabs Dress-up")) + FreeCADGui.addModule("PathScripts.PathDressupHoldingTabs") + FreeCADGui.addModule("PathScripts.PathUtils") + FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "HoldingTabsDressup")') + FreeCADGui.doCommand('dbo = PathScripts.PathDressupHoldingTabs.ObjectDressup(obj)') + FreeCADGui.doCommand('obj.Base = FreeCAD.ActiveDocument.' + selection[0].Name) + FreeCADGui.doCommand('PathScripts.PathDressupHoldingTabs.ViewProviderDressup(obj.ViewObject)') + FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)') + FreeCADGui.doCommand('Gui.ActiveDocument.getObject(obj.Base.Name).Visibility = False') + FreeCADGui.doCommand('dbo.setup(obj)') + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + +if FreeCAD.GuiUp: + # register the FreeCAD command + FreeCADGui.addCommand('PathDressup_HoldingTabs', CommandPathDressupHoldingTabs()) + +FreeCAD.Console.PrintLog("Loading PathDressupHoldingTabs... done\n") From 79935ee025b1204d1ef69049961f05c983c43519 Mon Sep 17 00:00:00 2001 From: ml Date: Sun, 30 Oct 2016 23:03:30 -0700 Subject: [PATCH 02/17] Changed tabs to be editable. --- .../Gui/Resources/panels/HoldingTabsEdit.ui | 193 +++++-- .../PathScripts/PathDressupHoldingTabs.py | 483 +++++++++++------- 2 files changed, 455 insertions(+), 221 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/panels/HoldingTabsEdit.ui b/src/Mod/Path/Gui/Resources/panels/HoldingTabsEdit.ui index 35c18c9d4..fa2ae49bc 100644 --- a/src/Mod/Path/Gui/Resources/panels/HoldingTabsEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/HoldingTabsEdit.ui @@ -11,10 +11,10 @@ - Holding Tabs + Holding Tags - + QFrame::NoFrame @@ -22,85 +22,144 @@ 0 - + 0 0 334 - 340 + 311 - Holding Tabs + Tags - - - - - Count - - - - - - - Width - - - - - + + + 0 0 - - <html><head/><body><p>List of holding tabs calculated by the paremeters entered above. You can un-check tabs you don't want to be inserted.</p></body></html> + + true + + + 80 + + + false + + + + X + + + + + Y + + + + + Width + + + + + Height + + + + + Angle + + + + + + + + + + + Delete + + + + + + + Disable + + + + + + + Add + + + + + + + + + + + + 0 + 0 + 334 + 311 + + + + Generate + + + + + + Width - - - - <html><head/><body><p>Enter the number of tabs you wish to have.</p><p><br/></p><p>Note that sometimes it's necessary to enter a larger than desired count number and disable the ones tabs you don't want in order to get the holding tab layout you want.</p></body></html> - - - - + <html><head/><body><p>Width of each tab.</p></body></html> - + Height - + <html><head/><body><p>The height of the holding tab measured from the bottom of the path. By default this is set to the (estimated) height of the path.</p></body></html> - + true - Angle + Angle - + true @@ -110,6 +169,66 @@ + + + + QDialogButtonBox::Apply|QDialogButtonBox::Ok + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Layout + + + + + + Count + + + + + + + <html><head/><body><p>Enter the number of tabs you wish to have.</p><p><br/></p><p>Note that sometimes it's necessary to enter a larger than desired count number and disable the ones tabs you don't want in order to get the holding tab layout you want.</p></body></html> + + + + + + + Spacing + + + + + + + + + + + + + Auto Apply + + + diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTabs.py b/src/Mod/Path/PathScripts/PathDressupHoldingTabs.py index 2f12b0956..4da2cd2bf 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTabs.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTabs.py @@ -30,7 +30,7 @@ import math import Part import DraftGeomUtils -"""Holding Tabs Dressup object and FreeCAD command""" +"""Holding Tags Dressup object and FreeCAD command""" # Qt tanslation handling try: @@ -69,6 +69,27 @@ def getAngle(v): return -a return a +class Tag: + def __init__(self, x, y, width, height, angle, enabled): + self.x = x + self.y = y + self.width = width + self.height = height + self.angle = angle + self.enabled = enabled + + def toString(self): + return str((self.x, self.y, self.width, self.height, self.angle, self.enabled)) + + @classmethod + def FromString(cls, string): + try: + t = eval(string) + return Tag(t[0], t[1], t[2], t[3], t[4], t[5]) + except: + return None + + class PathData: def __init__(self, obj): self.obj = obj @@ -138,17 +159,110 @@ class PathData: maxZ = v.Point.z return (minZ, maxZ) + def longestPathEdge(self): + return sorted(self.base.Edges, key=lambda e: -e.Length)[0] + + def generateTags(self, obj, count=None, width=None, height=None, angle=90, spacing=None): + print("generateTags(%s, %s, %s, %s, %s)" % (count, width, height, angle, spacing)) + #for e in self.base.Edges: + # debugMarker(e.Vertexes[0].Point, 'base', (0.0, 1.0, 1.0), 0.2) + + if spacing: + tabDistance = spacing + else: + if count: + tabDistance = self.base.Length / count + else: + tabDistance = self.base.Length / 4 + if width: + W = width + else: + W = self.tagWidth() + if height: + H = height + else: + H = self.tagHeight() + + + # start assigning tabs on the longest segment + maxLen = self.longestPathEdge().Length + startIndex = 0 + for i in range(0, len(self.base.Edges)): + edge = self.base.Edges[i] + if edge.Length == maxLen: + startIndex = i + break + + startEdge = self.base.Edges[startIndex] + startCount = int(startEdge.Length / tabDistance) + 1 + + lastTabLength = (startEdge.Length + (startCount - 1) * tabDistance) / 2 + if lastTabLength < 0 or lastTabLength > startEdge.Length: + lastTabLength = startEdge.Length / 2 + currentLength = startEdge.Length + + minLength = 2. * W + + #print("start index=%-2d -> count=%d (length=%.2f, distance=%.2f)" % (startIndex, startCount, startEdge.Length, tabDistance)) + #print(" -> lastTabLength=%.2f)" % lastTabLength) + #print(" -> currentLength=%.2f)" % currentLength) + + tabs = { startIndex: startCount } + + for i in range(startIndex + 1, len(self.base.Edges)): + edge = self.base.Edges[i] + (currentLength, lastTabLength) = self.processEdge(i, edge, currentLength, lastTabLength, tabDistance, minLength, tabs) + for i in range(0, startIndex): + edge = self.base.Edges[i] + (currentLength, lastTabLength) = self.processEdge(i, edge, currentLength, lastTabLength, tabDistance, minLength, tabs) + + tags = [] + + for (i, count) in tabs.iteritems(): + edge = self.base.Edges[i] + #debugMarker(edge.Vertexes[0].Point, 'base', (1.0, 0.0, 0.0), 0.2) + #debugMarker(edge.Vertexes[1].Point, 'base', (0.0, 1.0, 0.0), 0.2) + distance = (edge.LastParameter - edge.FirstParameter) / count + for j in range(0, count): + tab = edge.Curve.value((j+0.5) * distance) + tags.append(Tag(tab.x, tab.y, W, H, angle, True)) + + return tags + + def processEdge(self, index, edge, currentLength, lastTabLength, tabDistance, minLength, tabs): + tabCount = 0 + currentLength += edge.Length + if edge.Length > minLength: + while lastTabLength + tabDistance < currentLength: + tabCount += 1 + lastTabLength += tabDistance + if tabCount > 0: + #print(" index=%d -> count=%d" % (index, tabCount)) + tabs[index] = tabCount + #else: + #print(" skipping=%-2d (%.2f)" % (index, edge.Length)) + + return (currentLength, lastTabLength) + + def tagHeight(self): + return self.maxZ - self.minZ + + def tagWidth(self): + return self.longestPathEdge().Length / 10 + + def tagAngle(self): + return 90 + + def pathLength(self): + return self.base.Length + class ObjectDressup: def __init__(self, obj): self.obj = obj - obj.addProperty("App::PropertyLink", "Base","Base", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "The base path to modify")) - obj.addProperty("App::PropertyInteger", "Count", "Tab", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "The number of holding tabs to be generated")) - obj.addProperty("App::PropertyFloat", "Height", "Tab", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "Height of holding tabs measured from bottom of path")) - obj.addProperty("App::PropertyFloat", "Width", "Tab", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "Width of each tab at its base")) - obj.addProperty("App::PropertyFloat", "Angle", "Tab", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "Angle of plunge used for tabs")) - obj.addProperty("App::PropertyIntegerList", "Blacklist", "Tab", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "IDs of disabled paths")) - obj.setEditorMode("Blacklist", 2) + obj.addProperty("App::PropertyLink", "Base","Base", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTags", "The base path to modify")) + obj.addProperty("App::PropertyStringList", "Tags", "Tag", QtCore.QT_TRANSLATE_NOOP("PathDressup_holdingTags", "Inserted tags")) + obj.setEditorMode("Tags", 2) obj.Proxy = self def __getstate__(self): @@ -157,10 +271,8 @@ class ObjectDressup: def __setstate__(self, state): return None - def getFingerprint(self, obj): - if hasattr(self, 'pathData'): - return "%d-%.2f-%.2f-%.2f-%s" % (obj.Count, obj.Height, obj.Width, obj.Angle, str(obj.Blacklist)) - return '' + def generateTags(self, obj, count=None, width=None, height=None, angle=90, spacing=None): + return self.pathData.generateTags(obj, count, width, height, angle, spacing) def execute(self, obj): if not obj.Base: @@ -174,162 +286,101 @@ class ObjectDressup: pathData = self.setup(obj) if not pathData: + print("execute - no pathData") return - if hasattr(self, 'fingerprint') and self.fingerprint and self.fingerprint == self.getFingerprint(obj): - return + if hasattr(obj, 'Tags') and obj.Tags: + if self.fingerprint == obj.Tags: + print("execute - cache valid") + return + print("execute - tags from property") + tags = [Tag.FromString(tag) for tag in obj.Tags] + else: + print("execute - default tags") + tags = self.generateTags(obj, 4) - self.fingerprint = self.getFingerprint(obj) - - #for e in pathData.base.Edges: - # debugMarker(e.Vertexes[0].Point, 'base', (0.0, 1.0, 1.0), 0.2) - - if obj.Count == 0: + if not tags: + print("execute - no tags") + self.tags = [] obj.Path = obj.Base.Path return - tabDistance = pathData.base.Length / obj.Count - - # start assigning tabs on the longest segment - maxLen = sorted(pathData.base.Edges, key=lambda e: -e.Length)[0].Length - startIndex = 0 - for i in range(0, len(pathData.base.Edges)): - edge = pathData.base.Edges[i] - if edge.Length == maxLen: - startIndex = i - break - - startEdge = pathData.base.Edges[startIndex] - startCount = int(startEdge.Length / tabDistance) + 1 - - lastTabLength = (startEdge.Length + (startCount - 1) * tabDistance) / 2 - if lastTabLength < 0 or lastTabLength > startEdge.Length: - lastTabLength = startEdge.Length / 2 - currentLength = startEdge.Length - minLength = 2 * obj.Width - - print("start index=%-2d -> count=%d (length=%.2f, distance=%.2f)" % (startIndex, startCount, startEdge.Length, tabDistance)) - print(" -> lastTabLength=%.2f)" % lastTabLength) - print(" -> currentLength=%.2f)" % currentLength) - - tabs = { startIndex: startCount } - - for i in range(startIndex + 1, len(pathData.base.Edges)): - edge = pathData.base.Edges[i] - (currentLength, lastTabLength) = self.executeTabLength(i, edge, currentLength, lastTabLength, tabDistance, minLength, tabs) - for i in range(0, startIndex): - edge = pathData.base.Edges[i] - (currentLength, lastTabLength) = self.executeTabLength(i, edge, currentLength, lastTabLength, tabDistance, minLength, tabs) - - self.tabs = tabs - locs = {} - - tabID = 0 - for (i, count) in tabs.iteritems(): - edge = pathData.base.Edges[i] - #debugMarker(edge.Vertexes[0].Point, 'base', (1.0, 0.0, 0.0), 0.2) - #debugMarker(edge.Vertexes[1].Point, 'base', (0.0, 1.0, 0.0), 0.2) - distance = (edge.LastParameter - edge.FirstParameter) / count - for j in range(0, count): - tab = edge.Curve.value((j+0.5) * distance) - tabID += 1 - locs[(tab.x, tab.y)] = tabID - if not tabID in obj.Blacklist: - debugMarker(tab, "tab-%02d" % tabID , (1.0, 0.0, 1.0), 0.5) - - self.tabLocations = locs - #debugMarker(pathData.base.CenterOfMass, 'cog', (0., 0., 0.), 0.5) - + tagID = 0 + for tag in tags: + tagID += 1 + if tag.enabled: + print("x=%s, y=%s, z=%s" % (tag.x, tag.y, pathData.minZ)) + debugMarker(FreeCAD.Vector(tag.x, tag.y, pathData.minZ), "tag-%02d" % tagID , (1.0, 0.0, 1.0), 0.5) + self.fingerprint = [tag.toString() for tag in tags] + self.tags = tags obj.Path = obj.Base.Path - def executeTabLength(self, index, edge, currentLength, lastTabLength, tabDistance, minLength, tabs): - tabCount = 0 - currentLength += edge.Length - if edge.Length > minLength: - while lastTabLength + tabDistance < currentLength: - tabCount += 1 - lastTabLength += tabDistance - if tabCount > 0: - #print(" index=%d -> count=%d" % (index, tabCount)) - tabs[index] = tabCount - else: - print(" skipping=%-2d (%.2f)" % (index, edge.Length)) - return (currentLength, lastTabLength) + def setTags(self, obj, tags): + obj.Tags = [tag.toString() for tag in tags] + self.execute(obj) - def holdingTabsLocations(self, obj): - if hasattr(self, "tabLocations") and self.tabLocations: - return self.tabLocations - return {} + def getTags(self, obj): + if hasattr(self, 'tags'): + return self.tags + return self.setup(obj).generateTags(obj, 4) def setup(self, obj): if not hasattr(self, "pathData") or not self.pathData: try: pathData = PathData(obj) except ValueError: - FreeCAD.Console.PrintError(translate("PathDressup_HoldingTabs", "Cannot insert holding tabs for this path - please select a Profile path\n")) + FreeCAD.Console.PrintError(translate("PathDressup_HoldingTags", "Cannot insert holding tabs for this path - please select a Profile path\n")) return None - # setup the object's properties, in case they're not set yet - obj.Count = self.tabCount(obj) - obj.Angle = self.tabAngle(obj) - obj.Blacklist = self.tabBlacklist(obj) + ## setup the object's properties, in case they're not set yet + #obj.Count = self.tabCount(obj) + #obj.Angle = self.tabAngle(obj) + #obj.Blacklist = self.tabBlacklist(obj) # if the heigt isn't set, use the height of the path - if not hasattr(obj, "Height") or not obj.Height: - obj.Height = pathData.maxZ - pathData.minZ + #if not hasattr(obj, "Height") or not obj.Height: + # obj.Height = pathData.maxZ - pathData.minZ # try and take an educated guess at the width - if not hasattr(obj, "Width") or not obj.Width: - width = sorted(pathData.base.Edges, key=lambda e: -e.Length)[0].Length / 10 - while obj.Count > len([e for e in pathData.base.Edges if e.Length > 3*width]): - width = widht / 2 - obj.Width = width + #if not hasattr(obj, "Width") or not obj.Width: + # width = sorted(pathData.base.Edges, key=lambda e: -e.Length)[0].Length / 10 + # while obj.Count > len([e for e in pathData.base.Edges if e.Length > 3*width]): + # width = widht / 2 + # obj.Width = width # and the tool radius, not sure yet if it's needed - self.toolRadius = 5 - toolLoad = PathUtils.getLastToolLoad(obj) - if toolLoad is None or toolLoad.ToolNumber == 0: - self.toolRadius = 5 - else: - tool = PathUtils.getTool(obj, toolLoad.ToolNumber) - if not tool or tool.Diameter == 0: - self.toolRadius = 5 - else: - self.toolRadius = tool.Diameter / 2 + #self.toolRadius = 5 + #toolLoad = PathUtils.getLastToolLoad(obj) + #if toolLoad is None or toolLoad.ToolNumber == 0: + # self.toolRadius = 5 + #else: + # tool = PathUtils.getTool(obj, toolLoad.ToolNumber) + # if not tool or tool.Diameter == 0: + # self.toolRadius = 5 + # else: + # self.toolRadius = tool.Diameter / 2 self.pathData = pathData return self.pathData - def tabCount(self, obj): - if hasattr(obj, "Count") and obj.Count: - return obj.Count - return 4 + def getHeight(self, obj): + return self.pathData.tagHeight() - def tabHeight(self, obj): - if hasattr(obj, "Height") and obj.Height: - return obj.Height - return 1 + def getWidth(self, obj): + return self.pathData.tagWidth() - def tabWidth(self, obj): - if hasattr(obj, "Width") and obj.Width: - return obj.Width - return 3 + def getAngle(self, obj): + return self.pathData.tagAngle() - def tabAngle(self, obj): - if hasattr(obj, "Angle") and obj.Angle: - return obj.Angle - return 90 - - def tabBlacklist(self, obj): - if hasattr(obj, "Blacklist") and obj.Blacklist: - return obj.Blacklist - return [] + def getPathLength(self, obj): + return self.pathData.pathLength() class TaskPanel: - DataId = QtCore.Qt.ItemDataRole.UserRole + DataTag = QtCore.Qt.ItemDataRole.UserRole + DataValue = QtCore.Qt.ItemDataRole.DisplayRole def __init__(self, obj): self.obj = obj self.form = FreeCADGui.PySideUic.loadUi(":/panels/HoldingTabsEdit.ui") - FreeCAD.ActiveDocument.openTransaction(translate("PathDressup_HoldingTabs", "Edit HoldingTabs Dress-up")) + FreeCAD.ActiveDocument.openTransaction(translate("PathDressup_HoldingTags", "Edit HoldingTags Dress-up")) def reject(self): FreeCAD.ActiveDocument.abortTransaction() @@ -350,44 +401,99 @@ class TaskPanel: # install the function mode resident FreeCADGui.Selection.addObserver(self.s) - def updateTabList(self): - blacklist = self.obj.Proxy.tabBlacklist(self.obj) - itemList = [] - for (x,y), id in self.obj.Proxy.holdingTabsLocations(self.obj).iteritems(): - label = "%d: (x=%.2f, y=%.2f)" % (id, x, y) - item = QtGui.QListWidgetItem(label) - if id in blacklist: - item.setCheckState(QtCore.Qt.CheckState.Unchecked) - else: - item.setCheckState(QtCore.Qt.CheckState.Checked) - item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsUserCheckable) - item.setData(self.DataId, id) - itemList.append(item) - self.form.lwHoldingTabs.clear() - for item in sorted(itemList, key=lambda item: item.data(self.DataId)): - self.form.lwHoldingTabs.addItem(item) + def tableWidgetItem(self, tag, val): + item = QtGui.QTableWidgetItem() + item.setTextAlignment(QtCore.Qt.AlignRight) + item.setData(self.DataTag, tag) + item.setData(self.DataValue, val) + return item def getFields(self): - self.obj.Count = self.form.sbCount.value() - self.obj.Height = self.form.dsbHeight.value() - self.obj.Width = self.form.dsbWidth.value() - self.obj.Angle = self.form.dsbAngle.value() - blacklist = [] - for i in range(0, self.form.lwHoldingTabs.count()): - item = self.form.lwHoldingTabs.item(i) - if item.checkState() == QtCore.Qt.CheckState.Unchecked: - blacklist.append(item.data(self.DataId)) - self.obj.Blacklist = sorted(blacklist) - self.obj.Proxy.execute(self.obj) + tags = [] + for row in range(0, self.form.twTags.rowCount()): + x = self.form.twTags.item(row, 0).data(self.DataValue) + y = self.form.twTags.item(row, 1).data(self.DataValue) + w = self.form.twTags.item(row, 2).data(self.DataValue) + h = self.form.twTags.item(row, 3).data(self.DataValue) + a = self.form.twTags.item(row, 4).data(self.DataValue) + tags.append(Tag(x, y, w, h, a, True)) + print("getFields: %d" % (len(tags))) + self.obj.Proxy.setTags(self.obj, tags) - def update(self): - if True or debugDressup: + def updateTags(self): + self.tags = self.obj.Proxy.getTags(self.obj) + self.form.twTags.blockSignals(True) + self.form.twTags.setSortingEnabled(False) + self.form.twTags.clearSpans() + print("updateTags: %d" % (len(self.tags))) + self.form.twTags.setRowCount(len(self.tags)) + for row, tag in enumerate(self.tags): + self.form.twTags.setItem(row, 0, self.tableWidgetItem(tag, tag.x)) + self.form.twTags.setItem(row, 1, self.tableWidgetItem(tag, tag.y)) + self.form.twTags.setItem(row, 2, self.tableWidgetItem(tag, tag.width)) + self.form.twTags.setItem(row, 3, self.tableWidgetItem(tag, tag.height)) + self.form.twTags.setItem(row, 4, self.tableWidgetItem(tag, tag.angle)) + self.form.twTags.setSortingEnabled(True) + self.form.twTags.blockSignals(False) + + def cleanupUI(self): + if debugDressup: for obj in FreeCAD.ActiveDocument.Objects: - if obj.Name.startswith('tab'): + if obj.Name.startswith('tag'): FreeCAD.ActiveDocument.removeObject(obj.Name) + + def updateUI(self): + self.cleanupUI() self.getFields() - self.updateTabList() - FreeCAD.ActiveDocument.recompute() + if debugDressup: + FreeCAD.ActiveDocument.recompute() + + + def whenApplyClicked(self): + self.cleanupUI() + + count = self.form.sbCount.value() + spacing = self.form.dsbSpacing.value() + width = self.form.dsbWidth.value() + height = self.form.dsbHeight.value() + angle = self.form.dsbAngle.value() + + tags = self.obj.Proxy.generateTags(self.obj, count, width, height, angle, spacing) + self.obj.Proxy.setTags(self.obj, tags) + self.updateTags() + if debugDressup: + FreeCAD.ActiveDocument.recompute() + + def autoApply(self): + if self.form.cbAutoApply.checkState() == QtCore.Qt.CheckState.Checked: + self.whenApplyClicked() + + def updateTagSpacing(self, count): + if count == 0: + spacing = 0 + else: + spacing = self.pathLength / count + self.form.dsbSpacing.blockSignals(True) + self.form.dsbSpacing.setValue(spacing) + self.form.dsbSpacing.blockSignals(False) + + def whenCountChanged(self): + self.updateTagSpacing(self.form.sbCount.value()) + self.autoApply() + + def whenSpacingChanged(self): + if self.form.dsbSpacing.value() == 0: + count = 0 + else: + count = int(self.pathLength / self.form.dsbSpacing.value()) + self.form.sbCount.blockSignals(True) + self.form.sbCount.setValue(count) + self.form.sbCount.blockSignals(False) + self.autoApply() + + def whenOkClicked(self): + self.whenApplyClicked() + self.form.toolBox.setCurrentWidget(self.form.tbpTags) def setupSpinBox(self, widget, val, decimals = 2): widget.setMinimum(0) @@ -395,21 +501,30 @@ class TaskPanel: widget.setDecimals(decimals) widget.setValue(val) - def setFields(self): - self.setupSpinBox(self.form.sbCount, self.obj.Proxy.tabCount(self.obj), None) - self.setupSpinBox(self.form.dsbHeight, self.obj.Proxy.tabHeight(self.obj)) - self.setupSpinBox(self.form.dsbWidth, self.obj.Proxy.tabWidth(self.obj)) - self.setupSpinBox(self.form.dsbAngle, self.obj.Proxy.tabAngle(self.obj)) - self.updateTabList() + self.pathLength = self.obj.Proxy.getPathLength(self.obj) + vHeader = self.form.twTags.verticalHeader() + vHeader.setResizeMode(QtGui.QHeaderView.Fixed) + vHeader.setDefaultSectionSize(20) + self.updateTags() + self.setupSpinBox(self.form.sbCount, self.form.twTags.rowCount(), None) + self.setupSpinBox(self.form.dsbSpacing, 0) + self.setupSpinBox(self.form.dsbHeight, self.obj.Proxy.getHeight(self.obj)) + self.setupSpinBox(self.form.dsbWidth, self.obj.Proxy.getWidth(self.obj)) + self.setupSpinBox(self.form.dsbAngle, self.obj.Proxy.getAngle(self.obj)) + self.updateTagSpacing(self.form.twTags.rowCount()) def setupUi(self): self.setFields() - self.form.sbCount.valueChanged.connect(self.update) - self.form.dsbHeight.valueChanged.connect(self.update) - self.form.dsbWidth.valueChanged.connect(self.update) - self.form.dsbAngle.valueChanged.connect(self.update) - self.form.lwHoldingTabs.itemChanged.connect(self.update) + self.form.sbCount.valueChanged.connect(self.whenCountChanged) + self.form.dsbSpacing.valueChanged.connect(self.whenSpacingChanged) + self.form.dsbHeight.valueChanged.connect(self.autoApply) + self.form.dsbWidth.valueChanged.connect(self.autoApply) + self.form.dsbAngle.valueChanged.connect(self.autoApply) + #self.form.pbAdd.clicked.connect(self.) + self.form.buttonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self.whenApplyClicked) + self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).clicked.connect(self.whenOkClicked) + self.form.twTags.itemChanged.connect(self.updateUI) class SelObserver: def __init__(self): @@ -468,8 +583,8 @@ class CommandPathDressupHoldingTabs: def GetResources(self): return {'Pixmap': 'Path-Dressup', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "HoldingTabs Dress-up"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTabs", "Creates a HoldingTabs Dress-up object from a selected path")} + 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTags", "HoldingTags Dress-up"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTags", "Creates a HoldingTags Dress-up object from a selected path")} def IsActive(self): if FreeCAD.ActiveDocument is not None: @@ -483,18 +598,18 @@ class CommandPathDressupHoldingTabs: # check that the selection contains exactly what we want selection = FreeCADGui.Selection.getSelection() if len(selection) != 1: - FreeCAD.Console.PrintError(translate("PathDressup_HoldingTabs", "Please select one path object\n")) + FreeCAD.Console.PrintError(translate("PathDressup_HoldingTags", "Please select one path object\n")) return baseObject = selection[0] if not baseObject.isDerivedFrom("Path::Feature"): - FreeCAD.Console.PrintError(translate("PathDressup_HoldingTabs", "The selected object is not a path\n")) + FreeCAD.Console.PrintError(translate("PathDressup_HoldingTags", "The selected object is not a path\n")) return if baseObject.isDerivedFrom("Path::FeatureCompoundPython"): - FreeCAD.Console.PrintError(translate("PathDressup_HoldingTabs", "Please select a Profile object")) + FreeCAD.Console.PrintError(translate("PathDressup_HoldingTags", "Please select a Profile object")) return # everything ok! - FreeCAD.ActiveDocument.openTransaction(translate("PathDressup_HoldingTabs", "Create HoldingTabs Dress-up")) + FreeCAD.ActiveDocument.openTransaction(translate("PathDressup_HoldingTags", "Create HoldingTags Dress-up")) FreeCADGui.addModule("PathScripts.PathDressupHoldingTabs") FreeCADGui.addModule("PathScripts.PathUtils") FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "HoldingTabsDressup")') @@ -509,6 +624,6 @@ class CommandPathDressupHoldingTabs: if FreeCAD.GuiUp: # register the FreeCAD command - FreeCADGui.addCommand('PathDressup_HoldingTabs', CommandPathDressupHoldingTabs()) + FreeCADGui.addCommand('PathDressup_HoldingTags', CommandPathDressupHoldingTabs()) FreeCAD.Console.PrintLog("Loading PathDressupHoldingTabs... done\n") From 344250b5c21b5e4ad3024a2e84e211e6f979c393 Mon Sep 17 00:00:00 2001 From: ml Date: Sun, 30 Oct 2016 23:34:05 -0700 Subject: [PATCH 03/17] Renamed tabs to tags. --- src/Mod/Path/CMakeLists.txt | 3 +- src/Mod/Path/Gui/Resources/Path.qrc | 2 +- ...{HoldingTabsEdit.ui => HoldingTagsEdit.ui} | 8 +- src/Mod/Path/InitGui.py | 4 +- ...ldingTabs.py => PathDressupHoldingTags.py} | 88 +++++++++++-------- 5 files changed, 58 insertions(+), 47 deletions(-) rename src/Mod/Path/Gui/Resources/panels/{HoldingTabsEdit.ui => HoldingTagsEdit.ui} (95%) rename src/Mod/Path/PathScripts/{PathDressupHoldingTabs.py => PathDressupHoldingTags.py} (90%) diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 225732aa1..729ecf124 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -38,7 +38,7 @@ SET(PathScripts_SRCS PathScripts/PathDrilling.py PathScripts/PathDressup.py PathScripts/DragknifeDressup.py - PathScripts/PathDressupHoldingTabs.py + PathScripts/PathDressupHoldingTags.py PathScripts/PathHop.py PathScripts/PathUtils.py PathScripts/PathSelection.py @@ -48,6 +48,7 @@ SET(PathScripts_SRCS PathScripts/PathJob.py PathScripts/PathStock.py PathScripts/PathPlane.py + PathScripts/PathPocket.py PathScripts/PathPost.py PathScripts/PathPostProcessor.py PathScripts/PathLoadTool.py diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index 7ad296a00..2f635be9e 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -90,6 +90,6 @@ panels/DogboneEdit.ui panels/DlgSelectPostProcessor.ui preferences/PathJob.ui - panels/HoldingTabsEdit.ui + panels/HoldingTagsEdit.ui diff --git a/src/Mod/Path/Gui/Resources/panels/HoldingTabsEdit.ui b/src/Mod/Path/Gui/Resources/panels/HoldingTagsEdit.ui similarity index 95% rename from src/Mod/Path/Gui/Resources/panels/HoldingTabsEdit.ui rename to src/Mod/Path/Gui/Resources/panels/HoldingTagsEdit.ui index fa2ae49bc..465b0ad6e 100644 --- a/src/Mod/Path/Gui/Resources/panels/HoldingTabsEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/HoldingTagsEdit.ui @@ -131,7 +131,7 @@ - <html><head/><body><p>Width of each tab.</p></body></html> + <html><head/><body><p>Width of each tag.</p></body></html> @@ -145,7 +145,7 @@ - <html><head/><body><p>The height of the holding tab measured from the bottom of the path. By default this is set to the (estimated) height of the path.</p></body></html> + <html><head/><body><p>The height of the holding tag measured from the bottom of the path. By default this is set to the (estimated) height of the path.</p></body></html> @@ -165,7 +165,7 @@ true - <html><head/><body><p>Angle of tab walls.</p></body></html> + <html><head/><body><p>Angle of tag walls.</p></body></html> @@ -205,7 +205,7 @@ - <html><head/><body><p>Enter the number of tabs you wish to have.</p><p><br/></p><p>Note that sometimes it's necessary to enter a larger than desired count number and disable the ones tabs you don't want in order to get the holding tab layout you want.</p></body></html> + <html><head/><body><p>Enter the number of tags you wish to have.</p><p><br/></p><p>Note that sometimes it's necessary to enter a larger than desired count number and disable the ones tags you don't want in order to get the holding tag layout you want.</p></body></html> diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 68bc0bdc7..4659f587f 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -75,7 +75,7 @@ class PathWorkbench (Workbench): from PathScripts import PathProfileEdges from PathScripts import DogboneDressup from PathScripts import PathMillFace - from PathScripts import PathDressupHoldingTabs + from PathScripts import PathDressupHoldingTags import PathCommands # build commands list @@ -85,7 +85,7 @@ class PathWorkbench (Workbench): twodopcmdlist = ["Path_Contour", "Path_Profile", "Path_Profile_Edges", "Path_Pocket", "Path_Drilling", "Path_Engrave", "Path_MillFace"] threedopcmdlist = ["Path_Surfacing"] modcmdlist = ["Path_Copy", "Path_CompoundExtended", "Path_Array", "Path_SimpleCopy" ] - dressupcmdlist = ["Dogbone_Dressup", "DragKnife_Dressup", "PathDressup_HoldingTabs"] + dressupcmdlist = ["Dogbone_Dressup", "DragKnife_Dressup", "PathDressup_HoldingTags"] extracmdlist = ["Path_SelectLoop"] #modcmdmore = ["Path_Hop",] #remotecmdlist = ["Path_Remote"] diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTabs.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py similarity index 90% rename from src/Mod/Path/PathScripts/PathDressupHoldingTabs.py rename to src/Mod/Path/PathScripts/PathDressupHoldingTags.py index 4da2cd2bf..d9dc5b9c4 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTabs.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -142,7 +142,7 @@ class PathData: wire = Part.Wire(bottom) if wire.isClosed(): return wire - # if we get here there are already holding tabs, or we're not looking at a profile + # if we get here there are already holding tags, or we're not looking at a profile # let's try and insert the missing pieces - another day raise ValueError("Selected path doesn't seem to be a Profile operation.") @@ -168,12 +168,12 @@ class PathData: # debugMarker(e.Vertexes[0].Point, 'base', (0.0, 1.0, 1.0), 0.2) if spacing: - tabDistance = spacing + tagDistance = spacing else: if count: - tabDistance = self.base.Length / count + tagDistance = self.base.Length / count else: - tabDistance = self.base.Length / 4 + tagDistance = self.base.Length / 4 if width: W = width else: @@ -184,7 +184,7 @@ class PathData: H = self.tagHeight() - # start assigning tabs on the longest segment + # start assigning tags on the longest segment maxLen = self.longestPathEdge().Length startIndex = 0 for i in range(0, len(self.base.Edges)): @@ -194,55 +194,55 @@ class PathData: break startEdge = self.base.Edges[startIndex] - startCount = int(startEdge.Length / tabDistance) + 1 + startCount = int(startEdge.Length / tagDistance) + 1 - lastTabLength = (startEdge.Length + (startCount - 1) * tabDistance) / 2 - if lastTabLength < 0 or lastTabLength > startEdge.Length: - lastTabLength = startEdge.Length / 2 + lastTagLength = (startEdge.Length + (startCount - 1) * tagDistance) / 2 + if lastTagLength < 0 or lastTagLength > startEdge.Length: + lastTagLength = startEdge.Length / 2 currentLength = startEdge.Length minLength = 2. * W - #print("start index=%-2d -> count=%d (length=%.2f, distance=%.2f)" % (startIndex, startCount, startEdge.Length, tabDistance)) - #print(" -> lastTabLength=%.2f)" % lastTabLength) + #print("start index=%-2d -> count=%d (length=%.2f, distance=%.2f)" % (startIndex, startCount, startEdge.Length, tagDistance)) + #print(" -> lastTagLength=%.2f)" % lastTagLength) #print(" -> currentLength=%.2f)" % currentLength) - tabs = { startIndex: startCount } + edgeDict = { startIndex: startCount } for i in range(startIndex + 1, len(self.base.Edges)): edge = self.base.Edges[i] - (currentLength, lastTabLength) = self.processEdge(i, edge, currentLength, lastTabLength, tabDistance, minLength, tabs) + (currentLength, lastTagLength) = self.processEdge(i, edge, currentLength, lastTagLength, tagDistance, minLength, edgeDict) for i in range(0, startIndex): edge = self.base.Edges[i] - (currentLength, lastTabLength) = self.processEdge(i, edge, currentLength, lastTabLength, tabDistance, minLength, tabs) + (currentLength, lastTagLength) = self.processEdge(i, edge, currentLength, lastTagLength, tagDistance, minLength, edgeDict) tags = [] - for (i, count) in tabs.iteritems(): + for (i, count) in edgeDict.iteritems(): edge = self.base.Edges[i] #debugMarker(edge.Vertexes[0].Point, 'base', (1.0, 0.0, 0.0), 0.2) #debugMarker(edge.Vertexes[1].Point, 'base', (0.0, 1.0, 0.0), 0.2) distance = (edge.LastParameter - edge.FirstParameter) / count for j in range(0, count): - tab = edge.Curve.value((j+0.5) * distance) - tags.append(Tag(tab.x, tab.y, W, H, angle, True)) + tag = edge.Curve.value((j+0.5) * distance) + tags.append(Tag(tag.x, tag.y, W, H, angle, True)) return tags - def processEdge(self, index, edge, currentLength, lastTabLength, tabDistance, minLength, tabs): - tabCount = 0 + def processEdge(self, index, edge, currentLength, lastTagLength, tagDistance, minLength, edgeDict): + tagCount = 0 currentLength += edge.Length if edge.Length > minLength: - while lastTabLength + tabDistance < currentLength: - tabCount += 1 - lastTabLength += tabDistance - if tabCount > 0: - #print(" index=%d -> count=%d" % (index, tabCount)) - tabs[index] = tabCount + while lastTagLength + tagDistance < currentLength: + tagCount += 1 + lastTagLength += tagDistance + if tagCount > 0: + #print(" index=%d -> count=%d" % (index, tagCount)) + edgeDict[index] = tagCount #else: #print(" skipping=%-2d (%.2f)" % (index, edge.Length)) - return (currentLength, lastTabLength) + return (currentLength, lastTagLength) def tagHeight(self): return self.maxZ - self.minZ @@ -309,7 +309,7 @@ class ObjectDressup: for tag in tags: tagID += 1 if tag.enabled: - print("x=%s, y=%s, z=%s" % (tag.x, tag.y, pathData.minZ)) + #print("x=%s, y=%s, z=%s" % (tag.x, tag.y, pathData.minZ)) debugMarker(FreeCAD.Vector(tag.x, tag.y, pathData.minZ), "tag-%02d" % tagID , (1.0, 0.0, 1.0), 0.5) self.fingerprint = [tag.toString() for tag in tags] self.tags = tags @@ -329,13 +329,13 @@ class ObjectDressup: try: pathData = PathData(obj) except ValueError: - FreeCAD.Console.PrintError(translate("PathDressup_HoldingTags", "Cannot insert holding tabs for this path - please select a Profile path\n")) + FreeCAD.Console.PrintError(translate("PathDressup_HoldingTags", "Cannot insert holding tags for this path - please select a Profile path\n")) return None ## setup the object's properties, in case they're not set yet - #obj.Count = self.tabCount(obj) - #obj.Angle = self.tabAngle(obj) - #obj.Blacklist = self.tabBlacklist(obj) + #obj.Count = self.tagCount(obj) + #obj.Angle = self.tagAngle(obj) + #obj.Blacklist = self.tagBlacklist(obj) # if the heigt isn't set, use the height of the path #if not hasattr(obj, "Height") or not obj.Height: @@ -379,7 +379,7 @@ class TaskPanel: def __init__(self, obj): self.obj = obj - self.form = FreeCADGui.PySideUic.loadUi(":/panels/HoldingTabsEdit.ui") + self.form = FreeCADGui.PySideUic.loadUi(":/panels/HoldingTagsEdit.ui") FreeCAD.ActiveDocument.openTransaction(translate("PathDressup_HoldingTags", "Edit HoldingTags Dress-up")) def reject(self): @@ -437,12 +437,14 @@ class TaskPanel: self.form.twTags.blockSignals(False) def cleanupUI(self): + print("cleanupUI") if debugDressup: for obj in FreeCAD.ActiveDocument.Objects: if obj.Name.startswith('tag'): FreeCAD.ActiveDocument.removeObject(obj.Name) def updateUI(self): + print("updateUI") self.cleanupUI() self.getFields() if debugDressup: @@ -450,6 +452,7 @@ class TaskPanel: def whenApplyClicked(self): + print("whenApplyClicked") self.cleanupUI() count = self.form.sbCount.value() @@ -459,16 +462,20 @@ class TaskPanel: angle = self.form.dsbAngle.value() tags = self.obj.Proxy.generateTags(self.obj, count, width, height, angle, spacing) + self.obj.Proxy.setTags(self.obj, tags) self.updateTags() if debugDressup: + # this causes a big of an echo and a double click on the spin buttons, don't know why though FreeCAD.ActiveDocument.recompute() def autoApply(self): + print("autoApply") if self.form.cbAutoApply.checkState() == QtCore.Qt.CheckState.Checked: self.whenApplyClicked() def updateTagSpacing(self, count): + print("updateTagSpacing") if count == 0: spacing = 0 else: @@ -478,10 +485,12 @@ class TaskPanel: self.form.dsbSpacing.blockSignals(False) def whenCountChanged(self): + print("whenCountChanged") self.updateTagSpacing(self.form.sbCount.value()) self.autoApply() def whenSpacingChanged(self): + print("whenSpacingChanged") if self.form.dsbSpacing.value() == 0: count = 0 else: @@ -492,6 +501,7 @@ class TaskPanel: self.autoApply() def whenOkClicked(self): + print("whenOkClicked") self.whenApplyClicked() self.form.toolBox.setCurrentWidget(self.form.tbpTags) @@ -579,7 +589,7 @@ class ViewProviderDressup: PathUtils.addToJob(arg1.Object.Base) return True -class CommandPathDressupHoldingTabs: +class CommandPathDressupHoldingTags: def GetResources(self): return {'Pixmap': 'Path-Dressup', @@ -610,12 +620,12 @@ class CommandPathDressupHoldingTabs: # everything ok! FreeCAD.ActiveDocument.openTransaction(translate("PathDressup_HoldingTags", "Create HoldingTags Dress-up")) - FreeCADGui.addModule("PathScripts.PathDressupHoldingTabs") + FreeCADGui.addModule("PathScripts.PathDressupHoldingTags") FreeCADGui.addModule("PathScripts.PathUtils") - FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "HoldingTabsDressup")') - FreeCADGui.doCommand('dbo = PathScripts.PathDressupHoldingTabs.ObjectDressup(obj)') + FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "HoldingTagsDressup")') + FreeCADGui.doCommand('dbo = PathScripts.PathDressupHoldingTags.ObjectDressup(obj)') FreeCADGui.doCommand('obj.Base = FreeCAD.ActiveDocument.' + selection[0].Name) - FreeCADGui.doCommand('PathScripts.PathDressupHoldingTabs.ViewProviderDressup(obj.ViewObject)') + FreeCADGui.doCommand('PathScripts.PathDressupHoldingTags.ViewProviderDressup(obj.ViewObject)') FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)') FreeCADGui.doCommand('Gui.ActiveDocument.getObject(obj.Base.Name).Visibility = False') FreeCADGui.doCommand('dbo.setup(obj)') @@ -624,6 +634,6 @@ class CommandPathDressupHoldingTabs: if FreeCAD.GuiUp: # register the FreeCAD command - FreeCADGui.addCommand('PathDressup_HoldingTags', CommandPathDressupHoldingTabs()) + FreeCADGui.addCommand('PathDressup_HoldingTags', CommandPathDressupHoldingTags()) -FreeCAD.Console.PrintLog("Loading PathDressupHoldingTabs... done\n") +FreeCAD.Console.PrintLog("Loading PathDressupHoldingTags... done\n") From fc55d7ae236b65149f352d635e5e9e4dde09c79c Mon Sep 17 00:00:00 2001 From: ml Date: Tue, 1 Nov 2016 22:06:00 -0700 Subject: [PATCH 04/17] Fixed angle calculation and deal with round contours properly. --- .../PathScripts/PathDressupHoldingTags.py | 70 +++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index d9dc5b9c4..fc87fe2f8 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -63,12 +63,24 @@ movearc = movecw + moveccw def getAngle(v): a = v.getAngle(FreeCAD.Vector(1,0,0)) - if v.x < 0 and v.y < 0: - return a - math.pi/2 if v.y < 0: return -a return a +def testPrintAngle(v): + print("(%+.2f, %+.2f, %+.2f): %+.2f" % (v.x, v.y, v.z, getAngle(v)/math.pi)) + +def testAngle(x=1, y=1): + testPrintAngle(FreeCAD.Vector( 1*x, 0*y, 0)) + testPrintAngle(FreeCAD.Vector( 1*x, 1*y, 0)) + testPrintAngle(FreeCAD.Vector( 0*x, 1*y, 0)) + testPrintAngle(FreeCAD.Vector(-1*x, 1*y, 0)) + testPrintAngle(FreeCAD.Vector(-1*x, 0*y, 0)) + testPrintAngle(FreeCAD.Vector(-1*x,-1*y, 0)) + testPrintAngle(FreeCAD.Vector( 0*x,-1*y, 0)) + testPrintAngle(FreeCAD.Vector( 1*x,-1*y, 0)) + + class Tag: def __init__(self, x, y, width, height, angle, enabled): self.x = x @@ -104,24 +116,21 @@ class PathData: center = lastPt + self.point(cmd, FreeCAD.Vector(0,0,0), 'I', 'J', 'K') A = lastPt - center B = pt - center - a = getAngle(A) - b = getAngle(B) - if cmd.Name in movecw and a < 0 and math.fabs(math.pi - b) < 0.0000001: - angle = (a - b) / 2 - elif cmd.Name in moveccw and math.fabs(math.pi - a) < 0.0000001 and b < 0: - angle = (b - a) / 2 - else: - angle = (a + b) / 2. d = -B.x * A.y + B.y * A.x + if d == 0: + # we're dealing with half an circle here + angle = getAngle(A) + math.pi/2 + if cmd.Name in movecw: + angle -= math.pi + else: + #print("(%.2f, %.2f) (%.2f, %.2f)" % (A.x, A.y, B.x, B.y)) + C = A + B + angle = getAngle(C) + R = (lastPt - center).Length ptm = center + FreeCAD.Vector(math.cos(angle), math.sin(angle), 0) * R - #if pt.z == 2.: # and pt.x == 10. and math.fabs(pt.y) == 12.5: - # #print("%s (%.2f %.2f) -> (%.2f, %.2f): center=(%.2f, %.2f)" % (cmd, lastPt.x, lastPt.y, pt.x, pt.y, center.x, center.y)) - # print(" a = %+.2f b = %+.2f) %+.2f -> angle = %+.2f r = %.2f" % (a/math.pi, b/math.pi, d, angle/math.pi, R)) - # debugMarker(lastPt, 'arc', (1.0, 1.0, 0.0), 0.3) - # debugMarker(ptm, 'arc', (1.0, 1.0, 0.0), 0.3) - # debugMarker(pt, 'arc', (1.0, 1.0, 0.0), 0.3) + self.edges.append(Part.Edge(Part.Arc(lastPt, ptm, pt))) lastPt = pt self.base = self.findBottomWire(self.edges) @@ -159,11 +168,12 @@ class PathData: maxZ = v.Point.z return (minZ, maxZ) - def longestPathEdge(self): - return sorted(self.base.Edges, key=lambda e: -e.Length)[0] + def shortestAndLongestPathEdge(self): + edges = sorted(self.base.Edges, key=lambda e: e.Length) + return (edges[0], edges[-1]) def generateTags(self, obj, count=None, width=None, height=None, angle=90, spacing=None): - print("generateTags(%s, %s, %s, %s, %s)" % (count, width, height, angle, spacing)) + #print("generateTags(%s, %s, %s, %s, %s)" % (count, width, height, angle, spacing)) #for e in self.base.Edges: # debugMarker(e.Vertexes[0].Point, 'base', (0.0, 1.0, 1.0), 0.2) @@ -185,25 +195,26 @@ class PathData: # start assigning tags on the longest segment - maxLen = self.longestPathEdge().Length + (shortestEdge, longestEdge) = self.shortestAndLongestPathEdge() startIndex = 0 for i in range(0, len(self.base.Edges)): edge = self.base.Edges[i] - if edge.Length == maxLen: + if edge.Length == longestEdge.Length: startIndex = i break startEdge = self.base.Edges[startIndex] - startCount = int(startEdge.Length / tagDistance) + 1 + startCount = int(startEdge.Length / tagDistance) + if (longestEdge.Length - shortestEdge.Length) > shortestEdge.Length: + startCount = int(startEdge.Length / tagDistance) + 1 lastTagLength = (startEdge.Length + (startCount - 1) * tagDistance) / 2 - if lastTagLength < 0 or lastTagLength > startEdge.Length: - lastTagLength = startEdge.Length / 2 currentLength = startEdge.Length - minLength = 2. * W + minLength = min(2. * W, longestEdge.Length) - #print("start index=%-2d -> count=%d (length=%.2f, distance=%.2f)" % (startIndex, startCount, startEdge.Length, tagDistance)) + #print("length=%.2f shortestEdge=%.2f(%.2f) longestEdge=%.2f(%.2f)" % (self.base.Length, shortestEdge.Length, shortestEdge.Length/self.base.Length, longestEdge.Length, longestEdge.Length / self.base.Length)) + #print(" start: index=%-2d count=%d (length=%.2f, distance=%.2f)" % (startIndex, startCount, startEdge.Length, tagDistance)) #print(" -> lastTagLength=%.2f)" % lastTagLength) #print(" -> currentLength=%.2f)" % currentLength) @@ -220,6 +231,7 @@ class PathData: for (i, count) in edgeDict.iteritems(): edge = self.base.Edges[i] + #print(" %d: %d" % (i, count)) #debugMarker(edge.Vertexes[0].Point, 'base', (1.0, 0.0, 0.0), 0.2) #debugMarker(edge.Vertexes[1].Point, 'base', (0.0, 1.0, 0.0), 0.2) distance = (edge.LastParameter - edge.FirstParameter) / count @@ -248,7 +260,7 @@ class PathData: return self.maxZ - self.minZ def tagWidth(self): - return self.longestPathEdge().Length / 10 + return self.shortestAndLongestPathEdge()[1].Length / 10 def tagAngle(self): return 90 @@ -297,7 +309,7 @@ class ObjectDressup: tags = [Tag.FromString(tag) for tag in obj.Tags] else: print("execute - default tags") - tags = self.generateTags(obj, 4) + tags = self.generateTags(obj, 4.) if not tags: print("execute - no tags") @@ -461,7 +473,7 @@ class TaskPanel: height = self.form.dsbHeight.value() angle = self.form.dsbAngle.value() - tags = self.obj.Proxy.generateTags(self.obj, count, width, height, angle, spacing) + tags = self.obj.Proxy.generateTags(self.obj, count, width, height, angle, spacing * 0.99) self.obj.Proxy.setTags(self.obj, tags) self.updateTags() From a9f246b4669c313a17e75df4be05b4badfde1a46 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 18 Nov 2016 15:35:30 -0800 Subject: [PATCH 05/17] Moving tag processing into Tag itself. --- src/Mod/Path/Gui/Resources/Path.qrc | 134 +++++++-------- src/Mod/Path/InitGui.py | 10 +- .../PathScripts/PathDressupHoldingTags.py | 161 +++++++++++++++++- 3 files changed, 225 insertions(+), 80 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index 2f635be9e..5be33f8ea 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -1,95 +1,95 @@ - icons/preferences-path.svg - icons/Path-Toolpath.svg - icons/Path-Compound.svg - icons/Path-Shape.svg - icons/Path-Profile.svg - icons/Path-Contour.svg - icons/Path-Pocket.svg - icons/Path-Drilling.svg - icons/Path-Job.svg - icons/Path-Dressup.svg - icons/Path-Hop.svg - icons/Path-Datums.svg - icons/Path-Copy.svg - icons/Path-ToolTable.svg - icons/Path-LengthOffset.svg - icons/Path-Axis.svg - icons/Path-Stock.svg - icons/Path-Plane.svg - icons/Path-Post.svg - icons/Path-LoadTool.svg - icons/Path-Comment.svg - icons/Path-Stop.svg - icons/Path-Machine.svg - icons/Path-Kurve.svg - icons/Path-FaceProfile.svg - icons/Path-FacePocket.svg - icons/Path-Array.svg - icons/Path-Custom.svg - icons/Path-Inspect.svg - icons/Path-ToolChange.svg - icons/Path-SimpleCopy.svg - icons/Path-Engrave.svg - icons/Path-Sanity.svg icons/Path-3DSurface.svg - icons/Path-Speed.svg + icons/Path-Array.svg + icons/Path-Axis.svg icons/Path-BaseGeometry.svg + icons/Path-Comment.svg + icons/Path-Compound.svg + icons/Path-Contour.svg + icons/Path-Copy.svg + icons/Path-Custom.svg + icons/Path-Datums.svg icons/Path-Depths.svg + icons/Path-Dressup.svg + icons/Path-Drilling.svg + icons/Path-Engrave.svg + icons/Path-FacePocket.svg + icons/Path-FaceProfile.svg + icons/Path-Face.svg icons/Path-Heights.svg + icons/Path-Hop.svg + icons/Path-Inspect.svg + icons/Path-Job.svg + icons/Path-Kurve.svg + icons/Path-LengthOffset.svg + icons/Path-LoadTool.svg icons/Path-MachineLathe.svg icons/Path-MachineMill.svg + icons/Path-Machine.svg icons/Path-OperationA.svg icons/Path-OperationB.svg + icons/Path-Plane.svg + icons/Path-Pocket.svg + icons/Path-Post.svg icons/Path-Profile-Edges.svg icons/Path-Profile-Face.svg + icons/Path-Profile.svg + icons/Path-Sanity.svg icons/Path-SelectLoop.svg - icons/Path-Face.svg - translations/Path_de.qm + icons/Path-Shape.svg + icons/Path-SimpleCopy.svg + icons/Path-Speed.svg + icons/Path-Stock.svg + icons/Path-Stop.svg + icons/Path-ToolChange.svg + icons/Path-Toolpath.svg + icons/Path-ToolTable.svg + icons/preferences-path.svg + panels/ContourEdit.ui + panels/DlgJobChooser.ui + panels/DlgSelectPostProcessor.ui + panels/DlgToolCopy.ui + panels/DogboneEdit.ui + panels/DrillingEdit.ui + panels/EngraveEdit.ui + panels/HoldingTagsEdit.ui + panels/JobEdit.ui + panels/MillFaceEdit.ui + panels/PocketEdit.ui + panels/ProfileEdgesEdit.ui + panels/ProfileEdit.ui + panels/RemoteEdit.ui + panels/SurfaceEdit.ui + panels/ToolControl.ui + panels/ToolEdit.ui + panels/ToolLibraryEditor.ui + preferences/PathJob.ui translations/Path_af.qm - translations/Path_zh-CN.qm - translations/Path_zh-TW.qm - translations/Path_hr.qm translations/Path_cs.qm - translations/Path_nl.qm + translations/Path_de.qm + translations/Path_el.qm + translations/Path_es-ES.qm translations/Path_fi.qm translations/Path_fr.qm + translations/Path_hr.qm translations/Path_hu.qm + translations/Path_it.qm translations/Path_ja.qm + translations/Path_nl.qm translations/Path_no.qm translations/Path_pl.qm + translations/Path_pt-BR.qm translations/Path_pt-PT.qm translations/Path_ro.qm translations/Path_ru.qm - translations/Path_sr.qm - translations/Path_es-ES.qm - translations/Path_sv-SE.qm - translations/Path_uk.qm - translations/Path_it.qm - translations/Path_pt-BR.qm - translations/Path_el.qm translations/Path_sk.qm - translations/Path_tr.qm translations/Path_sl.qm - panels/EngraveEdit.ui - panels/DrillingEdit.ui - panels/PocketEdit.ui - panels/ProfileEdit.ui - panels/SurfaceEdit.ui - panels/RemoteEdit.ui - panels/ToolControl.ui - panels/ToolLibraryEditor.ui - panels/JobEdit.ui - panels/DlgToolCopy.ui - panels/ToolEdit.ui - panels/DlgJobChooser.ui - panels/ContourEdit.ui - panels/MillFaceEdit.ui - panels/ProfileEdgesEdit.ui - panels/DogboneEdit.ui - panels/DlgSelectPostProcessor.ui - preferences/PathJob.ui - panels/HoldingTagsEdit.ui + translations/Path_sr.qm + translations/Path_sv-SE.qm + translations/Path_tr.qm + translations/Path_uk.qm + translations/Path_zh-CN.qm + translations/Path_zh-TW.qm diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 4659f587f..e2b69f6b7 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -25,8 +25,7 @@ class PathWorkbench (Workbench): "Path workbench" def __init__(self): - self.__class__.Icon = FreeCAD.getResourceDir( - ) + "Mod/Path/Resources/icons/PathWorkbench.svg" + self.__class__.Icon = FreeCAD.getResourceDir() + "Mod/Path/Resources/icons/PathWorkbench.svg" self.__class__.MenuText = "Path" self.__class__.ToolTip = "Path workbench" @@ -120,11 +119,6 @@ class PathWorkbench (Workbench): # "Path", "Remote Operations")], remotecmdlist) self.appendMenu([translate("Path", "&Path")], extracmdlist) - # Add preferences pages - import os - FreeCADGui.addPreferencePage(FreeCAD.getHomePath( - ) + os.sep + "Mod" + os.sep + "Path" + os.sep + "PathScripts" + os.sep + "DlgSettingsPath.ui", "Path") - Log('Loading Path workbench... done\n') def GetClassName(self): @@ -142,7 +136,7 @@ class PathWorkbench (Workbench): if len(FreeCADGui.Selection.getSelection()) == 1: if FreeCADGui.Selection.getSelection()[0].isDerivedFrom("Path::Feature"): self.appendContextMenu("", ["Path_Inspect"]) - if "Profile" in FreeCADGui.Selection.getSelection()[0].Name: + if "Profile" or "Contour" in FreeCADGui.Selection.getSelection()[0].Name: self.appendContextMenu("", ["Add_Tag"]) self.appendContextMenu("", ["Set_StartPoint"]) self.appendContextMenu("", ["Set_EndPoint"]) diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index fc87fe2f8..199f46cc0 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -67,6 +67,29 @@ def getAngle(v): return -a return a +class Side: + Left = +1 + Right = -1 + Straight = 0 + On = 0 + + @classmethod + def toString(cls, side): + if side == cls.Left: + return 'Left' + if side == cls.Right: + return 'Right' + return 'On' + + @classmethod + def of(cls, ptRef, pt): + d = -ptRef.x*pt.y + ptRef.y*pt.x + if d < 0: + return cls.Left + if d > 0: + return cls.Right + return cls.Straight + def testPrintAngle(v): print("(%+.2f, %+.2f, %+.2f): %+.2f" % (v.x, v.y, v.z, getAngle(v)/math.pi)) @@ -81,18 +104,73 @@ def testAngle(x=1, y=1): testPrintAngle(FreeCAD.Vector( 1*x,-1*y, 0)) +def testPrintSide(pt1, pt2): + print('(%.2f, %.2f) - (%.2f, %.2f) -> %s' % (pt1.x, pt1.y, pt2.x, pt2.y, Side.toString(Side.of(pt1, pt2)))) + +def testSide(): + testPrintSide(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 1, 0, 0)) + testPrintSide(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector(-1, 0, 0)) + testPrintSide(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 0, 1, 0)) + testPrintSide(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 0,-1, 0)) + +def pathCommandForEdge(edge): + pt = edge.valueAt(edge.LastParameter) + params = {'X': pt.x, 'Y': pt.y, 'Z': pt.z} + if type(edge.Curve) == Part.Line: + return Part.Command('G1', params) + + p1 = edge.valueAt(edge.FirstParameter) + p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter)/2) + p3 = pt + if Side.Left == Side.of(p2 - p1, p3 - p2): + cmd = 'G3' + else: + cmd = 'G2' + offset = pt1 - edge.Curve.Center + params.update({'I': offset.x, 'J': offset.y, 'K': offset.z}) + return Part.Command(cmd, params) + + class Tag: def __init__(self, x, y, width, height, angle, enabled): self.x = x self.y = y - self.width = width - self.height = height - self.angle = angle + self.width = math.fabs(width) + self.height = math.fabs(height) + self.angle = math.fabs(angle) self.enabled = enabled def toString(self): return str((self.x, self.y, self.width, self.height, self.angle, self.enabled)) + def originAt(self, z): + return FreeCAD.Vector(self.x, self.y, z) + + def createSolidsAt(self, z): + r1 = self.width / 2 + height = self.height + if self.angle == 90 and height > 0: + self.solid = Part.makeCylinder(r1, height) + self.core = self.solid + elif self.angle > 0.0 and height > 0.0: + tangens = math.tan(math.radians(self.angle)) + dr = height / tangens + if dr < r1: + r2 = r1 - dr + self.core = Part.makeCylinder(r2, height) + else: + r2 = 0 + height = r1 * tangens + self.core = None + self.solid = Part.makeCone(r1, r2, height) + else: + # degenerated case - no tag + self.solid = Part.makeSphere(r1 / 10000) + self.core = None + self.solid.translate(self.originAt(z)) + if self.core: + self.core.translate(self.originAt(z)) + @classmethod def FromString(cls, string): try: @@ -150,11 +228,25 @@ class PathData: bottom = [e for e in edges if e.Vertexes[0].Point.z == minZ and e.Vertexes[1].Point.z == minZ] wire = Part.Wire(bottom) if wire.isClosed(): - return wire + return Part.Wire(self.sortedBase(bottom)) # if we get here there are already holding tags, or we're not looking at a profile # let's try and insert the missing pieces - another day raise ValueError("Selected path doesn't seem to be a Profile operation.") + def sortedBase(self, base): + # first find the exit point, where base wire is closed + edges = [e for e in self.edges if e.valueAt(e.FirstParameter).z == self.minZ and e.valueAt(e.LastParameter).z != self.maxZ] + exit = sorted(edges, key=lambda e: -e.valueAt(e.LastParameter).z)[0] + pt = exit.valueAt(exit.FirstParameter) + # then find the first base edge, and sort them until done + ordered = [] + while base: + edge = [e for e in base if e.valueAt(e.FirstParameter) == pt][0] + ordered.append(edge) + base.remove(edge) + pt = edge.valueAt(edge.LastParameter) + return ordered + def findZLimits(self, edges): # not considering arcs and spheres in Z direction, find the highes and lowest Z values @@ -268,6 +360,18 @@ class PathData: def pathLength(self): return self.base.Length + def sortedTags(self, tags): + ordered = [] + for edge in self.base.Edges: + ts = [t for t in tags if DraftGeomUtils.isPtOnEdge(t.originAt(self.minZ), edge)] + for t in sorted(ts, key=lambda t: (t.originAt(self.minZ) - edge.valueAt(edge.FirstParameter)).Length): + tags.remove(t) + ordered.append(t) + if tags: + raise ValueError("There's something really wrong here") + return ordered + + class ObjectDressup: def __init__(self, obj): @@ -286,6 +390,46 @@ class ObjectDressup: def generateTags(self, obj, count=None, width=None, height=None, angle=90, spacing=None): return self.pathData.generateTags(obj, count, width, height, angle, spacing) + + def tagIntersection(self, face, edge): + p1 = edge.valueAt(edge.FirstParameter) + pts = edge.Curve.intersect(face.Surface) + if pts[0]: + closest = sorted(pts[0], key=lambda pt: (pt - p1).Length)[0] + return closest + return None + + def createPath(self, edges, tagSolids): + commands = [] + i = 0 + while i != len(edges): + edge = edges[i] + while edge: + for solid in tagSolids: + for face in solid.Faces: + pt = self.tagIntersection(face, edge) + if pt: + if pt == edge.valueAt(edge.FirstParameter): + pt + elif pt != edge.valueAt(edge.LastParameter): + parameter = edge.Curve.parameter(pt) + wire = edge.split(parameter) + commands.append(pathCommandForEdge(wire.Edges[0])) + edge = wire.Edges[1] + break; + else: + commands.append(pathCommandForEdge(edge)) + edge = None + i += 1 + break + if not edge: + break + if edge: + commands.append(pathCommandForEdge(edge)) + edge = None + return self.obj.Path + + def execute(self, obj): if not obj.Base: return @@ -323,9 +467,16 @@ class ObjectDressup: if tag.enabled: #print("x=%s, y=%s, z=%s" % (tag.x, tag.y, pathData.minZ)) debugMarker(FreeCAD.Vector(tag.x, tag.y, pathData.minZ), "tag-%02d" % tagID , (1.0, 0.0, 1.0), 0.5) + + tags = pathData.sortedTags(tags) + for tag in tags: + tag.createSolidsAt(pathData.minZ) + self.fingerprint = [tag.toString() for tag in tags] self.tags = tags - obj.Path = obj.Base.Path + + #obj.Path = self.createPath(pathData.edges, tags) + obj.Path = self.Base.Path def setTags(self, obj, tags): obj.Tags = [tag.toString() for tag in tags] From dce16252dcdd0622a2bcc120ba0dc17c0461e167 Mon Sep 17 00:00:00 2001 From: ml Date: Sun, 6 Nov 2016 16:10:04 -0800 Subject: [PATCH 06/17] First pointless path unit test --- src/Mod/Path/CMakeLists.txt | 2 ++ src/Mod/Path/PathTests/TestPathPost.py | 41 ++++++++++++++++++++++++++ src/Mod/Path/PathTests/__init__.py | 0 src/Mod/Path/TestPathApp.py | 27 +++++++++++++++++ src/Mod/Test/TestGui.py | 1 + 5 files changed, 71 insertions(+) create mode 100644 src/Mod/Path/PathTests/TestPathPost.py create mode 100644 src/Mod/Path/PathTests/__init__.py create mode 100644 src/Mod/Path/TestPathApp.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 729ecf124..283f87a05 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -73,6 +73,8 @@ SET(PathScripts_SRCS PathScripts/DogboneDressup.py PathScripts/PathPreferencesPathJob.py PathScripts/PathPreferences.py + PathTests/TestPathPost.py + TestPathApp.py ) SET(PathScripts_NC_SRCS diff --git a/src/Mod/Path/PathTests/TestPathPost.py b/src/Mod/Path/PathTests/TestPathPost.py new file mode 100644 index 000000000..1907fccb1 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathPost.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2016 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 unittest + +from PathScripts import PathPost + +class PathPostTestCases(unittest.TestCase): + def setUp(self): + self.doc = FreeCAD.newDocument("PathPostTest") + + def tearDown(self): + FreeCAD.closeDocument("PathPostTest") + + def testCommand(self): + self.box = self.doc.addObject("Part::Box", "Box") + print("Running command test") + self.failUnless(True) + diff --git a/src/Mod/Path/PathTests/__init__.py b/src/Mod/Path/PathTests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py new file mode 100644 index 000000000..88850ea8d --- /dev/null +++ b/src/Mod/Path/TestPathApp.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2016 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 TestApp + +from PathTests.TestPathPost import PathPostTestCases diff --git a/src/Mod/Test/TestGui.py b/src/Mod/Test/TestGui.py index 8e62066d5..013871fad 100644 --- a/src/Mod/Test/TestGui.py +++ b/src/Mod/Test/TestGui.py @@ -52,6 +52,7 @@ class TestCmd: QtUnitGui.addTest("TestPartApp") QtUnitGui.addTest("TestPartDesignApp") QtUnitGui.addTest("TestPartDesignGui") + QtUnitGui.addTest("TestPathApp") QtUnitGui.addTest("TestSpreadsheet") QtUnitGui.addTest("TestDraft") QtUnitGui.addTest("TestArch") From 2aa25605293e18544f9daeac58bdbadeca7f13a5 Mon Sep 17 00:00:00 2001 From: ml Date: Sun, 6 Nov 2016 18:21:34 -0800 Subject: [PATCH 07/17] Basic unit test for linuxcnc output. --- src/Mod/Path/PathScripts/PathContour.py | 6 +- src/Mod/Path/PathScripts/PathLoadTool.py | 5 +- src/Mod/Path/PathScripts/PathPost.py | 55 +++++++++------- src/Mod/Path/PathScripts/PathPostProcessor.py | 2 +- src/Mod/Path/PathScripts/linuxcnc_post.py | 15 ++++- src/Mod/Path/PathTests/TestPathPost.py | 66 +++++++++++++++++-- src/Mod/Path/PathTests/test_linuxcnc_00.ngc | 62 +++++++++++++++++ 7 files changed, 175 insertions(+), 36 deletions(-) create mode 100644 src/Mod/Path/PathTests/test_linuxcnc_00.ngc diff --git a/src/Mod/Path/PathScripts/PathContour.py b/src/Mod/Path/PathScripts/PathContour.py index a8386299a..7cdc90cf6 100644 --- a/src/Mod/Path/PathScripts/PathContour.py +++ b/src/Mod/Path/PathScripts/PathContour.py @@ -28,10 +28,11 @@ from FreeCAD import Vector import TechDraw from PathScripts import PathUtils from PathScripts.PathUtils import depth_params +from PySide import QtCore if FreeCAD.GuiUp: import FreeCADGui - from PySide import QtCore, QtGui + from PySide import QtGui # Qt tanslation handling try: _encoding = QtGui.QApplication.UnicodeUTF8 @@ -258,7 +259,8 @@ class ObjectContour: if obj.Active: path = Path.Path(output) obj.Path = path - obj.ViewObject.Visibility = True + if obj.ViewObject: + obj.ViewObject.Visibility = True else: path = Path.Path("(inactive operation)") diff --git a/src/Mod/Path/PathScripts/PathLoadTool.py b/src/Mod/Path/PathScripts/PathLoadTool.py index a115a099c..41e47743f 100644 --- a/src/Mod/Path/PathScripts/PathLoadTool.py +++ b/src/Mod/Path/PathScripts/PathLoadTool.py @@ -181,13 +181,14 @@ PathUtils.addToJob(obj) FreeCAD.ActiveDocument.recompute() @staticmethod - def Create(jobname = None): + def Create(jobname = None, assignViewProvider = True): import PathScripts import PathUtils obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "TC") PathScripts.PathLoadTool.LoadTool(obj) - PathScripts.PathLoadTool._ViewProviderLoadTool(obj.ViewObject) + if assignViewProvider: + PathScripts.PathLoadTool._ViewProviderLoadTool(obj.ViewObject) PathUtils.addToJob(obj, jobname) diff --git a/src/Mod/Path/PathScripts/PathPost.py b/src/Mod/Path/PathScripts/PathPost.py index 9b7cd71fd..1827b3d04 100644 --- a/src/Mod/Path/PathScripts/PathPost.py +++ b/src/Mod/Path/PathScripts/PathPost.py @@ -79,7 +79,7 @@ class DlgSelectPostProcessor: class CommandPathPost: def resolveFileName(self, job): - print("resolveFileName(%s)" % job.Label) + #print("resolveFileName(%s)" % job.Label) path = PathPreferences.defaultOutputFile() if job.OutputFile: path = job.OutputFile @@ -134,7 +134,7 @@ class CommandPathPost: else: filename = None - print("resolveFileName(%s, %s) -> '%s'" % (path, policy, filename)) + #print("resolveFileName(%s, %s) -> '%s'" % (path, policy, filename)) return filename def resolvePostProcessor(self, job): @@ -179,35 +179,44 @@ class CommandPathPost: job = PathUtils.findParentJob(obj) if job: jobs.add(job) + + fail = True + rc = '' if len(jobs) != 1: FreeCAD.Console.PrintError("Please select a single job or other path object\n") - FreeCAD.ActiveDocument.abortTransaction() else: job = jobs.pop() print("Job for selected objects = %s" % job.Name) + (fail, rc) = exportObjectsWith(selected, job) - # check if the user has a project and has set the default post and - # output filename - postArgs = PathPreferences.defaultPostProcessorArgs() - if hasattr(job, "PostProcessorArgs") and job.PostProcessorArgs: - postArgs = job.PostProcessorArgs - elif hasattr(job, "PostProcessor") and job.PostProcessor: - postArgs = '' - - postname = self.resolvePostProcessor(job) - if postname: - filename = self.resolveFileName(job) - - if postname and filename: - print("post: %s(%s, %s)" % (postname, filename, postArgs)) - processor = PostProcessor.load(postname) - processor.export(selected, filename, postArgs) - - FreeCAD.ActiveDocument.commitTransaction() - else: - FreeCAD.ActiveDocument.abortTransaction() + if fail: + FreeCAD.ActiveDocument.abortTransaction() + else: + FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() + def exportObjectsWith(self, objs, job, needFilename = True): + # check if the user has a project and has set the default post and + # output filename + postArgs = PathPreferences.defaultPostProcessorArgs() + if hasattr(job, "PostProcessorArgs") and job.PostProcessorArgs: + postArgs = job.PostProcessorArgs + elif hasattr(job, "PostProcessor") and job.PostProcessor: + postArgs = '' + + postname = self.resolvePostProcessor(job) + filename = '-' + if postname and needFilename: + filename = self.resolveFileName(job) + + if postname and filename: + print("post: %s(%s, %s)" % (postname, filename, postArgs)) + processor = PostProcessor.load(postname) + gcode = processor.export(objs, filename, postArgs) + return (False, gcode) + else: + return (True, '') + if FreeCAD.GuiUp: # register the FreeCAD command FreeCADGui.addCommand('Path_Post', CommandPathPost()) diff --git a/src/Mod/Path/PathScripts/PathPostProcessor.py b/src/Mod/Path/PathScripts/PathPostProcessor.py index d302ab161..618123fd3 100644 --- a/src/Mod/Path/PathScripts/PathPostProcessor.py +++ b/src/Mod/Path/PathScripts/PathPostProcessor.py @@ -78,4 +78,4 @@ class PostProcessor: self.script = script def export(self, obj, filename, args): - self.script.export(obj, filename, args) + return self.script.export(obj, filename, args) diff --git a/src/Mod/Path/PathScripts/linuxcnc_post.py b/src/Mod/Path/PathScripts/linuxcnc_post.py index 1d9f00ac3..4d625c5fe 100644 --- a/src/Mod/Path/PathScripts/linuxcnc_post.py +++ b/src/Mod/Path/PathScripts/linuxcnc_post.py @@ -38,6 +38,7 @@ Arguments for linuxcnc: --header,--no-header ... output headers (--header) --comments,--no-comments ... output comments (--comments) --line-numbers,--no-line-numbers ... prefix with line numbers (--no-lin-numbers) + --show-editor, --no-show-editor ... pop up editor before writing output(--show-editor) ''' import datetime @@ -90,6 +91,7 @@ def processArguments(argstring): global OUTPUT_HEADER global OUTPUT_COMMENTS global OUTPUT_LINE_NUMBERS + global SHOW_EDITOR for arg in argstring.split(): if arg == '--header': OUTPUT_HEADER = True @@ -103,6 +105,10 @@ def processArguments(argstring): OUTPUT_LINE_NUMBERS = True elif arg == '--no-line-numbers': OUTPUT_LINE_NUMBERS = False + elif arg == '--show-editor': + SHOW_EDITOR = True + elif arg == '--no-show-editor': + SHOW_EDITOR = False def export(objectslist, filename, argstring): processArguments(argstring) @@ -179,9 +185,12 @@ def export(objectslist, filename, argstring): print "done postprocessing." - gfile = pythonopen(filename, "wb") - gfile.write(gcode) - gfile.close() + if not filename == '-': + gfile = pythonopen(filename, "wb") + gfile.write(final) + gfile.close() + + return final def linenumber(): diff --git a/src/Mod/Path/PathTests/TestPathPost.py b/src/Mod/Path/PathTests/TestPathPost.py index 1907fccb1..1a2a384b3 100644 --- a/src/Mod/Path/PathTests/TestPathPost.py +++ b/src/Mod/Path/PathTests/TestPathPost.py @@ -23,10 +23,16 @@ # *************************************************************************** import FreeCAD +import Path +import PathScripts +import PathScripts.PathContour +import PathScripts.PathJob +import PathScripts.PathLoadTool +import PathScripts.PathPost +import PathScripts.PathUtils +import difflib import unittest -from PathScripts import PathPost - class PathPostTestCases(unittest.TestCase): def setUp(self): self.doc = FreeCAD.newDocument("PathPostTest") @@ -34,8 +40,58 @@ class PathPostTestCases(unittest.TestCase): def tearDown(self): FreeCAD.closeDocument("PathPostTest") - def testCommand(self): - self.box = self.doc.addObject("Part::Box", "Box") - print("Running command test") + def testLinuxCNC(self): + # first create something to generate a path for + box = self.doc.addObject("Part::Box", "Box") + + # Create job and setup tool library + default tool + job = self.doc.addObject("Path::FeatureCompoundPython", "Job") + PathScripts.PathJob.ObjectPathJob(job) + job.Base = self.doc.Box + PathScripts.PathLoadTool.CommandPathLoadTool.Create(job.Name, False) + toolLib = job.Group[0] + tool1 = Path.Tool() + tool1.Diameter = 5.0 + tool1.Name = "Default Tool" + tool1.CuttingEdgeHeight = 15.0 + tool1.ToolType = "EndMill" + tool1.Material = "HighSpeedSteel" + job.Tooltable.addTools(tool1) + toolLib.ToolNumber = 1 self.failUnless(True) + self.doc.getObject("TC").ToolNumber = 2 + self.doc.recompute() + + contour = self.doc.addObject("Path::FeaturePython", "Contour") + PathScripts.PathContour.ObjectContour(contour) + contour.Active = True + contour.ClearanceHeight = 20.0 + contour.StepDown = 1.0 + contour.StartDepth= 10.0 + contour.FinalDepth=0.0 + contour.SafeHeight = 12.0 + contour.OffsetExtra = 0.0 + contour.Direction = 'CW' + contour.UseComp = True + contour.PlungeAngle = 90.0 + PathScripts.PathUtils.addToJob(contour) + PathScripts.PathContour.ObjectContour.setDepths(contour.Proxy, contour) + self.doc.recompute() + + job.PostProcessor = 'linuxcnc' + job.PostProcessorArgs = '--no-header --no-line-numbers --no-comments --no-show-editor' + + post = PathScripts.PathPost.CommandPathPost() + (fail, gcode) = post.exportObjectsWith([job], job, False) + self.assertFalse(fail) + + referenceFile = FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_linuxcnc_00.ngc' + with open(referenceFile, 'r') as fp: + refGCode = fp.read() + + if gcode != refGCode: + msg = ''.join(difflib.ndiff(gcode.splitlines(True), refGCode.splitlines(True))) + self.fail("linuxcnc output doesn't match: " + msg) + + diff --git a/src/Mod/Path/PathTests/test_linuxcnc_00.ngc b/src/Mod/Path/PathTests/test_linuxcnc_00.ngc new file mode 100644 index 000000000..d1ac7f01b --- /dev/null +++ b/src/Mod/Path/PathTests/test_linuxcnc_00.ngc @@ -0,0 +1,62 @@ +G17 G90 +G21 +(Contour :TC) +(Uncompensated Tool Path) +G0 Z15.0000 +G00 X-0.2500 Y0.0000 +G00 Z23.0000 +G01 X-0.2500 Y0.0000 Z9.0000 F0.00 +G01 X-0.2500 Y10.0000 Z9.0000 F0.00 +G02 X0.2500 Y10.0000 Z9.0000 I0.2500 J0.0000 F0.00 +G01 X0.2500 Y0.0000 Z9.0000 F0.00 +G02 X-0.2500 Y0.0000 Z9.0000 I-0.2500 J0.0000 F0.00 +G01 X-0.2500 Y0.0000 Z8.0000 F0.00 +G01 X-0.2500 Y10.0000 Z8.0000 F0.00 +G02 X0.2500 Y10.0000 Z8.0000 I0.2500 J0.0000 F0.00 +G01 X0.2500 Y0.0000 Z8.0000 F0.00 +G02 X-0.2500 Y0.0000 Z8.0000 I-0.2500 J0.0000 F0.00 +G01 X-0.2500 Y0.0000 Z7.0000 F0.00 +G01 X-0.2500 Y10.0000 Z7.0000 F0.00 +G02 X0.2500 Y10.0000 Z7.0000 I0.2500 J0.0000 F0.00 +G01 X0.2500 Y0.0000 Z7.0000 F0.00 +G02 X-0.2500 Y0.0000 Z7.0000 I-0.2500 J0.0000 F0.00 +G01 X-0.2500 Y0.0000 Z6.0000 F0.00 +G01 X-0.2500 Y10.0000 Z6.0000 F0.00 +G02 X0.2500 Y10.0000 Z6.0000 I0.2500 J0.0000 F0.00 +G01 X0.2500 Y0.0000 Z6.0000 F0.00 +G02 X-0.2500 Y0.0000 Z6.0000 I-0.2500 J0.0000 F0.00 +G01 X-0.2500 Y0.0000 Z5.0000 F0.00 +G01 X-0.2500 Y10.0000 Z5.0000 F0.00 +G02 X0.2500 Y10.0000 Z5.0000 I0.2500 J0.0000 F0.00 +G01 X0.2500 Y0.0000 Z5.0000 F0.00 +G02 X-0.2500 Y0.0000 Z5.0000 I-0.2500 J0.0000 F0.00 +G01 X-0.2500 Y0.0000 Z4.0000 F0.00 +G01 X-0.2500 Y10.0000 Z4.0000 F0.00 +G02 X0.2500 Y10.0000 Z4.0000 I0.2500 J0.0000 F0.00 +G01 X0.2500 Y0.0000 Z4.0000 F0.00 +G02 X-0.2500 Y0.0000 Z4.0000 I-0.2500 J0.0000 F0.00 +G01 X-0.2500 Y0.0000 Z3.0000 F0.00 +G01 X-0.2500 Y10.0000 Z3.0000 F0.00 +G02 X0.2500 Y10.0000 Z3.0000 I0.2500 J0.0000 F0.00 +G01 X0.2500 Y0.0000 Z3.0000 F0.00 +G02 X-0.2500 Y0.0000 Z3.0000 I-0.2500 J0.0000 F0.00 +G01 X-0.2500 Y0.0000 Z2.0000 F0.00 +G01 X-0.2500 Y10.0000 Z2.0000 F0.00 +G02 X0.2500 Y10.0000 Z2.0000 I0.2500 J0.0000 F0.00 +G01 X0.2500 Y0.0000 Z2.0000 F0.00 +G02 X-0.2500 Y0.0000 Z2.0000 I-0.2500 J0.0000 F0.00 +G01 X-0.2500 Y0.0000 Z1.0000 F0.00 +G01 X-0.2500 Y10.0000 Z1.0000 F0.00 +G02 X0.2500 Y10.0000 Z1.0000 I0.2500 J0.0000 F0.00 +G01 X0.2500 Y0.0000 Z1.0000 F0.00 +G02 X-0.2500 Y0.0000 Z1.0000 I-0.2500 J0.0000 F0.00 +G01 X-0.2500 Y0.0000 Z0.0000 F0.00 +G01 X-0.2500 Y10.0000 Z0.0000 F0.00 +G02 X0.2500 Y10.0000 Z0.0000 I0.2500 J0.0000 F0.00 +G01 X0.2500 Y0.0000 Z0.0000 F0.00 +G02 X-0.2500 Y0.0000 Z0.0000 I-0.2500 J0.0000 F0.00 +G00 Z15.0000 +M05 +G00 X-1.0 Y1.0 +G17 G90 +M2 From f4480765dee4c1b3d97fbb20dc323b4c615753f6 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 18 Nov 2016 16:14:32 -0800 Subject: [PATCH 08/17] Fixed linuxcnc unit test --- src/Mod/Path/PathScripts/PathLoadTool.py | 3 +- src/Mod/Path/PathTests/TestPathPost.py | 7 +- src/Mod/Path/PathTests/test_linuxcnc_00.ngc | 103 ++++++++++++++------ 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathLoadTool.py b/src/Mod/Path/PathScripts/PathLoadTool.py index 41e47743f..046983da2 100644 --- a/src/Mod/Path/PathScripts/PathLoadTool.py +++ b/src/Mod/Path/PathScripts/PathLoadTool.py @@ -82,7 +82,8 @@ class LoadTool(): path = Path.Path(commands) obj.Path = path - obj.ViewObject.Visibility = True + if obj.ViewObject: + obj.ViewObject.Visibility = True def onChanged(self, obj, prop): mode = 2 diff --git a/src/Mod/Path/PathTests/TestPathPost.py b/src/Mod/Path/PathTests/TestPathPost.py index 1a2a384b3..0ea6ba81d 100644 --- a/src/Mod/Path/PathTests/TestPathPost.py +++ b/src/Mod/Path/PathTests/TestPathPost.py @@ -90,8 +90,13 @@ class PathPostTestCases(unittest.TestCase): with open(referenceFile, 'r') as fp: refGCode = fp.read() + # Use if this test fails in order to have a real good look at the changes + if False: + with open('tab.tmp', 'w') as fp: + fp.write(gcode) + + if gcode != refGCode: msg = ''.join(difflib.ndiff(gcode.splitlines(True), refGCode.splitlines(True))) self.fail("linuxcnc output doesn't match: " + msg) - diff --git a/src/Mod/Path/PathTests/test_linuxcnc_00.ngc b/src/Mod/Path/PathTests/test_linuxcnc_00.ngc index d1ac7f01b..1f6da3a5b 100644 --- a/src/Mod/Path/PathTests/test_linuxcnc_00.ngc +++ b/src/Mod/Path/PathTests/test_linuxcnc_00.ngc @@ -1,5 +1,8 @@ G17 G90 G21 +(TC: UNDEFINED TOOL) +M6 T2.0 +M3 S0.0000 (Contour :TC) (Uncompensated Tool Path) G0 Z15.0000 @@ -7,54 +10,94 @@ G00 X-0.2500 Y0.0000 G00 Z23.0000 G01 X-0.2500 Y0.0000 Z9.0000 F0.00 G01 X-0.2500 Y10.0000 Z9.0000 F0.00 -G02 X0.2500 Y10.0000 Z9.0000 I0.2500 J0.0000 F0.00 -G01 X0.2500 Y0.0000 Z9.0000 F0.00 -G02 X-0.2500 Y0.0000 Z9.0000 I-0.2500 J0.0000 F0.00 +G02 X0.0000 Y10.2500 Z9.0000 I0.2500 J0.0000 F0.00 +G01 X10.0000 Y10.2500 Z9.0000 F0.00 +G02 X10.2500 Y10.0000 Z9.0000 I0.0000 J-0.2500 F0.00 +G01 X10.2500 Y0.0000 Z9.0000 F0.00 +G02 X10.0000 Y-0.2500 Z9.0000 I-0.2500 J0.0000 F0.00 +G01 X0.0000 Y-0.2500 Z9.0000 F0.00 +G02 X-0.2500 Y0.0000 Z9.0000 I0.0000 J0.2500 F0.00 G01 X-0.2500 Y0.0000 Z8.0000 F0.00 G01 X-0.2500 Y10.0000 Z8.0000 F0.00 -G02 X0.2500 Y10.0000 Z8.0000 I0.2500 J0.0000 F0.00 -G01 X0.2500 Y0.0000 Z8.0000 F0.00 -G02 X-0.2500 Y0.0000 Z8.0000 I-0.2500 J0.0000 F0.00 +G02 X0.0000 Y10.2500 Z8.0000 I0.2500 J0.0000 F0.00 +G01 X10.0000 Y10.2500 Z8.0000 F0.00 +G02 X10.2500 Y10.0000 Z8.0000 I0.0000 J-0.2500 F0.00 +G01 X10.2500 Y0.0000 Z8.0000 F0.00 +G02 X10.0000 Y-0.2500 Z8.0000 I-0.2500 J0.0000 F0.00 +G01 X0.0000 Y-0.2500 Z8.0000 F0.00 +G02 X-0.2500 Y0.0000 Z8.0000 I0.0000 J0.2500 F0.00 G01 X-0.2500 Y0.0000 Z7.0000 F0.00 G01 X-0.2500 Y10.0000 Z7.0000 F0.00 -G02 X0.2500 Y10.0000 Z7.0000 I0.2500 J0.0000 F0.00 -G01 X0.2500 Y0.0000 Z7.0000 F0.00 -G02 X-0.2500 Y0.0000 Z7.0000 I-0.2500 J0.0000 F0.00 +G02 X0.0000 Y10.2500 Z7.0000 I0.2500 J0.0000 F0.00 +G01 X10.0000 Y10.2500 Z7.0000 F0.00 +G02 X10.2500 Y10.0000 Z7.0000 I0.0000 J-0.2500 F0.00 +G01 X10.2500 Y0.0000 Z7.0000 F0.00 +G02 X10.0000 Y-0.2500 Z7.0000 I-0.2500 J0.0000 F0.00 +G01 X0.0000 Y-0.2500 Z7.0000 F0.00 +G02 X-0.2500 Y0.0000 Z7.0000 I0.0000 J0.2500 F0.00 G01 X-0.2500 Y0.0000 Z6.0000 F0.00 G01 X-0.2500 Y10.0000 Z6.0000 F0.00 -G02 X0.2500 Y10.0000 Z6.0000 I0.2500 J0.0000 F0.00 -G01 X0.2500 Y0.0000 Z6.0000 F0.00 -G02 X-0.2500 Y0.0000 Z6.0000 I-0.2500 J0.0000 F0.00 +G02 X0.0000 Y10.2500 Z6.0000 I0.2500 J0.0000 F0.00 +G01 X10.0000 Y10.2500 Z6.0000 F0.00 +G02 X10.2500 Y10.0000 Z6.0000 I0.0000 J-0.2500 F0.00 +G01 X10.2500 Y0.0000 Z6.0000 F0.00 +G02 X10.0000 Y-0.2500 Z6.0000 I-0.2500 J0.0000 F0.00 +G01 X0.0000 Y-0.2500 Z6.0000 F0.00 +G02 X-0.2500 Y0.0000 Z6.0000 I0.0000 J0.2500 F0.00 G01 X-0.2500 Y0.0000 Z5.0000 F0.00 G01 X-0.2500 Y10.0000 Z5.0000 F0.00 -G02 X0.2500 Y10.0000 Z5.0000 I0.2500 J0.0000 F0.00 -G01 X0.2500 Y0.0000 Z5.0000 F0.00 -G02 X-0.2500 Y0.0000 Z5.0000 I-0.2500 J0.0000 F0.00 +G02 X0.0000 Y10.2500 Z5.0000 I0.2500 J0.0000 F0.00 +G01 X10.0000 Y10.2500 Z5.0000 F0.00 +G02 X10.2500 Y10.0000 Z5.0000 I0.0000 J-0.2500 F0.00 +G01 X10.2500 Y0.0000 Z5.0000 F0.00 +G02 X10.0000 Y-0.2500 Z5.0000 I-0.2500 J0.0000 F0.00 +G01 X0.0000 Y-0.2500 Z5.0000 F0.00 +G02 X-0.2500 Y0.0000 Z5.0000 I0.0000 J0.2500 F0.00 G01 X-0.2500 Y0.0000 Z4.0000 F0.00 G01 X-0.2500 Y10.0000 Z4.0000 F0.00 -G02 X0.2500 Y10.0000 Z4.0000 I0.2500 J0.0000 F0.00 -G01 X0.2500 Y0.0000 Z4.0000 F0.00 -G02 X-0.2500 Y0.0000 Z4.0000 I-0.2500 J0.0000 F0.00 +G02 X0.0000 Y10.2500 Z4.0000 I0.2500 J0.0000 F0.00 +G01 X10.0000 Y10.2500 Z4.0000 F0.00 +G02 X10.2500 Y10.0000 Z4.0000 I0.0000 J-0.2500 F0.00 +G01 X10.2500 Y0.0000 Z4.0000 F0.00 +G02 X10.0000 Y-0.2500 Z4.0000 I-0.2500 J0.0000 F0.00 +G01 X0.0000 Y-0.2500 Z4.0000 F0.00 +G02 X-0.2500 Y0.0000 Z4.0000 I0.0000 J0.2500 F0.00 G01 X-0.2500 Y0.0000 Z3.0000 F0.00 G01 X-0.2500 Y10.0000 Z3.0000 F0.00 -G02 X0.2500 Y10.0000 Z3.0000 I0.2500 J0.0000 F0.00 -G01 X0.2500 Y0.0000 Z3.0000 F0.00 -G02 X-0.2500 Y0.0000 Z3.0000 I-0.2500 J0.0000 F0.00 +G02 X0.0000 Y10.2500 Z3.0000 I0.2500 J0.0000 F0.00 +G01 X10.0000 Y10.2500 Z3.0000 F0.00 +G02 X10.2500 Y10.0000 Z3.0000 I0.0000 J-0.2500 F0.00 +G01 X10.2500 Y0.0000 Z3.0000 F0.00 +G02 X10.0000 Y-0.2500 Z3.0000 I-0.2500 J0.0000 F0.00 +G01 X0.0000 Y-0.2500 Z3.0000 F0.00 +G02 X-0.2500 Y0.0000 Z3.0000 I0.0000 J0.2500 F0.00 G01 X-0.2500 Y0.0000 Z2.0000 F0.00 G01 X-0.2500 Y10.0000 Z2.0000 F0.00 -G02 X0.2500 Y10.0000 Z2.0000 I0.2500 J0.0000 F0.00 -G01 X0.2500 Y0.0000 Z2.0000 F0.00 -G02 X-0.2500 Y0.0000 Z2.0000 I-0.2500 J0.0000 F0.00 +G02 X0.0000 Y10.2500 Z2.0000 I0.2500 J0.0000 F0.00 +G01 X10.0000 Y10.2500 Z2.0000 F0.00 +G02 X10.2500 Y10.0000 Z2.0000 I0.0000 J-0.2500 F0.00 +G01 X10.2500 Y0.0000 Z2.0000 F0.00 +G02 X10.0000 Y-0.2500 Z2.0000 I-0.2500 J0.0000 F0.00 +G01 X0.0000 Y-0.2500 Z2.0000 F0.00 +G02 X-0.2500 Y0.0000 Z2.0000 I0.0000 J0.2500 F0.00 G01 X-0.2500 Y0.0000 Z1.0000 F0.00 G01 X-0.2500 Y10.0000 Z1.0000 F0.00 -G02 X0.2500 Y10.0000 Z1.0000 I0.2500 J0.0000 F0.00 -G01 X0.2500 Y0.0000 Z1.0000 F0.00 -G02 X-0.2500 Y0.0000 Z1.0000 I-0.2500 J0.0000 F0.00 +G02 X0.0000 Y10.2500 Z1.0000 I0.2500 J0.0000 F0.00 +G01 X10.0000 Y10.2500 Z1.0000 F0.00 +G02 X10.2500 Y10.0000 Z1.0000 I0.0000 J-0.2500 F0.00 +G01 X10.2500 Y0.0000 Z1.0000 F0.00 +G02 X10.0000 Y-0.2500 Z1.0000 I-0.2500 J0.0000 F0.00 +G01 X0.0000 Y-0.2500 Z1.0000 F0.00 +G02 X-0.2500 Y0.0000 Z1.0000 I0.0000 J0.2500 F0.00 G01 X-0.2500 Y0.0000 Z0.0000 F0.00 G01 X-0.2500 Y10.0000 Z0.0000 F0.00 -G02 X0.2500 Y10.0000 Z0.0000 I0.2500 J0.0000 F0.00 -G01 X0.2500 Y0.0000 Z0.0000 F0.00 -G02 X-0.2500 Y0.0000 Z0.0000 I-0.2500 J0.0000 F0.00 +G02 X0.0000 Y10.2500 Z0.0000 I0.2500 J0.0000 F0.00 +G01 X10.0000 Y10.2500 Z0.0000 F0.00 +G02 X10.2500 Y10.0000 Z0.0000 I0.0000 J-0.2500 F0.00 +G01 X10.2500 Y0.0000 Z0.0000 F0.00 +G02 X10.0000 Y-0.2500 Z0.0000 I-0.2500 J0.0000 F0.00 +G01 X0.0000 Y-0.2500 Z0.0000 F0.00 +G02 X-0.2500 Y0.0000 Z0.0000 I0.0000 J0.2500 F0.00 G00 Z15.0000 M05 G00 X-1.0 Y1.0 From ca1fb1238e15da839de37d0dbfe033cbff67922d Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 18 Nov 2016 17:07:20 -0800 Subject: [PATCH 09/17] First Tag unit tests, including a fix for an encountered issue. --- src/Mod/Path/CMakeLists.txt | 3 + .../PathScripts/PathDressupHoldingTags.py | 4 +- .../PathTests/TestPathDressupHoldingTags.py | 88 +++++++++++++++++++ src/Mod/Path/TestPathApp.py | 1 + 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 src/Mod/Path/PathTests/TestPathDressupHoldingTags.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 283f87a05..ceeba426f 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -73,6 +73,9 @@ SET(PathScripts_SRCS PathScripts/DogboneDressup.py PathScripts/PathPreferencesPathJob.py PathScripts/PathPreferences.py + PathTests/__init__.py + PathTests/test_linuxcnc_00.ngc + PathTests/TestPathDressupHoldingTags.py PathTests/TestPathPost.py TestPathApp.py ) diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index 199f46cc0..82f5a3eb2 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -132,7 +132,7 @@ def pathCommandForEdge(edge): class Tag: - def __init__(self, x, y, width, height, angle, enabled): + def __init__(self, x, y, width, height, angle, enabled=True): self.x = x self.y = y self.width = math.fabs(width) @@ -151,7 +151,7 @@ class Tag: height = self.height if self.angle == 90 and height > 0: self.solid = Part.makeCylinder(r1, height) - self.core = self.solid + self.core = self.solid.copy() elif self.angle > 0.0 and height > 0.0: tangens = math.tan(math.radians(self.angle)) dr = height / tangens diff --git a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py new file mode 100644 index 000000000..85c724a7e --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2016 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 +import math +import unittest + +from PathScripts.PathDressupHoldingTags import Tag + +slack = 0.0000001 + +def pointsCoincide(pt1, pt2): + pt = pt1 - pt2 + if math.fabs(pt.x) > slack: + return False + if math.fabs(pt.y) > slack: + return False + if math.fabs(pt.z) > slack: + return False + return True + +class PathDressupHoldingTagsTestCases(unittest.TestCase): + """Unit tests for the HoldingTags dressup.""" + + def testTagBasics(self): + """Check Tag origin, serialization and de-serialization.""" + tag = Tag(77, 13, 4, 5, 90, True) + self.assertTrue(pointsCoincide(tag.originAt(3), FreeCAD.Vector(77, 13, 3))) + s = tag.toString() + tagCopy = Tag.FromString(s) + self.assertEqual(tag.x, tagCopy.x) + self.assertEqual(tag.y, tagCopy.y) + self.assertEqual(tag.height, tagCopy.height) + self.assertEqual(tag.width, tagCopy.width) + self.assertEqual(tag.enabled, tagCopy.enabled) + + + def testTagSolidBasic(self): + """For a 90 degree tag the core and solid are both defined and identical cylinders.""" + tag = Tag(100, 200, 4, 5, 90, True) + tag.createSolidsAt(17) + self.assertIsNotNone(tag.solid) + self.assertIsNotNone(tag.core) + + self.assertCylinderAt(tag.solid, FreeCAD.Vector(100, 200, 17), 2, 5) + self.assertCylinderAt(tag.core, FreeCAD.Vector(100, 200, 17), 2, 5) + + def assertCylinderAt(self, solid, pt, r, h): + """Verify that the argument is a cylinder at the specified location.""" + lid = solid.Edges[0].Curve + self.assertTrue(type(lid), Part.Circle) + self.assertEqual(lid.Center, FreeCAD.Vector(pt.x, pt.y, pt.z+h)) + self.assertEqual(lid.Radius, r) + + hull = solid.Edges[1].Curve + self.assertTrue(type(hull), Part.Line) + self.assertTrue(pointsCoincide(hull.StartPoint, FreeCAD.Vector(pt.x+r, pt.y, pt.z))) + self.assertTrue(pointsCoincide(hull.EndPoint, FreeCAD.Vector(pt.x+r, pt.y, pt.z+h))) + + base = solid.Edges[2].Curve + self.assertTrue(type(base), Part.Circle) + self.assertEqual(base.Center, FreeCAD.Vector(pt.x, pt.y, pt.z)) + self.assertEqual(base.Radius, r) + diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 88850ea8d..90640ab54 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -25,3 +25,4 @@ import TestApp from PathTests.TestPathPost import PathPostTestCases +from PathTests.TestPathDressupHoldingTags import PathDressupHoldingTagsTestCases From 74ac78276bc3fd18383841eb108fbee2b3b9eede Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 18 Nov 2016 17:52:29 -0800 Subject: [PATCH 10/17] Tests for all different shapes of Tags. --- .../PathTests/TestPathDressupHoldingTags.py | 98 +++++++++++++++---- 1 file changed, 80 insertions(+), 18 deletions(-) diff --git a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py index 85c724a7e..23554febf 100644 --- a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py +++ b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py @@ -29,6 +29,7 @@ import PathScripts import math import unittest +from FreeCAD import Vector from PathScripts.PathDressupHoldingTags import Tag slack = 0.0000001 @@ -49,7 +50,7 @@ class PathDressupHoldingTagsTestCases(unittest.TestCase): def testTagBasics(self): """Check Tag origin, serialization and de-serialization.""" tag = Tag(77, 13, 4, 5, 90, True) - self.assertTrue(pointsCoincide(tag.originAt(3), FreeCAD.Vector(77, 13, 3))) + self.assertCoincide(tag.originAt(3), Vector(77, 13, 3)) s = tag.toString() tagCopy = Tag.FromString(s) self.assertEqual(tag.x, tagCopy.x) @@ -63,26 +64,87 @@ class PathDressupHoldingTagsTestCases(unittest.TestCase): """For a 90 degree tag the core and solid are both defined and identical cylinders.""" tag = Tag(100, 200, 4, 5, 90, True) tag.createSolidsAt(17) - self.assertIsNotNone(tag.solid) - self.assertIsNotNone(tag.core) - self.assertCylinderAt(tag.solid, FreeCAD.Vector(100, 200, 17), 2, 5) - self.assertCylinderAt(tag.core, FreeCAD.Vector(100, 200, 17), 2, 5) + self.assertIsNotNone(tag.solid) + self.assertCylinderAt(tag.solid, Vector(100, 200, 17), 2, 5) + + self.assertIsNotNone(tag.core) + self.assertCylinderAt(tag.core, Vector(100, 200, 17), 2, 5) + + def testTagSolidFlatCone(self): + """Tests a Tag that has an angle leaving a flat face on top of the cone.""" + tag = Tag(0, 0, 18, 5, 45, True) + tag.createSolidsAt(0) + + self.assertIsNotNone(tag.solid) + self.assertConeAt(tag.solid, Vector(0,0,0), 9, 4, 5) + + self.assertIsNotNone(tag.core) + self.assertCylinderAt(tag.core, Vector(0,0,0), 4, 5) + + def testTagSolidCone(self): + """Tests a Tag who's angled sides coincide at the tag's height.""" + tag = Tag(0, 0, 10, 5, 45, True) + tag.createSolidsAt(0) + self.assertIsNotNone(tag.solid) + self.assertConeAt(tag.solid, Vector(0,0,0), 5, 0, 5) + + self.assertIsNone(tag.core) + + def testTagSolidShortCone(self): + """Tests a Tag that's not wide enough to reach full height.""" + tag = Tag(0, 0, 5, 17, 60, True) + tag.createSolidsAt(0) + self.assertIsNotNone(tag.solid) + self.assertConeAt(tag.solid, Vector(0,0,0), 2.5, 0, 2.5 * math.tan((60/180.0)*math.pi)) + + self.assertIsNone(tag.core) def assertCylinderAt(self, solid, pt, r, h): - """Verify that the argument is a cylinder at the specified location.""" - lid = solid.Edges[0].Curve - self.assertTrue(type(lid), Part.Circle) - self.assertEqual(lid.Center, FreeCAD.Vector(pt.x, pt.y, pt.z+h)) - self.assertEqual(lid.Radius, r) + """Verify that solid is a cylinder at the specified location.""" + self.assertEqual(len(solid.Edges), 3) - hull = solid.Edges[1].Curve - self.assertTrue(type(hull), Part.Line) - self.assertTrue(pointsCoincide(hull.StartPoint, FreeCAD.Vector(pt.x+r, pt.y, pt.z))) - self.assertTrue(pointsCoincide(hull.EndPoint, FreeCAD.Vector(pt.x+r, pt.y, pt.z+h))) + lid = solid.Edges[0] + hull = solid.Edges[1] + base = solid.Edges[2] - base = solid.Edges[2].Curve - self.assertTrue(type(base), Part.Circle) - self.assertEqual(base.Center, FreeCAD.Vector(pt.x, pt.y, pt.z)) - self.assertEqual(base.Radius, r) + self.assertCircle(lid, Vector(pt.x, pt.y, pt.z+h), r) + self.assertLine(hull, Vector(pt.x+r, pt.y, pt.z), Vector(pt.x+r, pt.y, pt.z+h)) + self.assertCircle(base, Vector(pt.x, pt.y, pt.z), r) + def assertConeAt(self, solid, pt, r1, r2, h): + """Verify that solid is a cone at the specified location.""" + self.assertEqual(len(solid.Edges), 3) + + lid = solid.Edges[0] + hull = solid.Edges[1] + base = solid.Edges[2] + + self.assertCircle(lid, Vector(pt.x, pt.y, pt.z+h), r2) + self.assertLine(hull, Vector(pt.x+r1, pt.y, pt.z), Vector(pt.x+r2, pt.y, pt.z+h)) + self.assertCircle(base, Vector(pt.x, pt.y, pt.z), r1) + + def assertCircle(self, edge, pt, r): + """Verivy that edge is a circle at given location.""" + curve = edge.Curve + self.assertTrue(type(curve), Part.Circle) + self.assertCoincide(curve.Center, Vector(pt.x, pt.y, pt.z)) + self.assertAbout(curve.Radius, r) + + def assertLine(self, edge, pt1, pt2): + """Verify that edge is a line from pt1 to pt2.""" + curve = edge.Curve + self.assertTrue(type(curve), Part.Line) + self.assertCoincide(curve.StartPoint, pt1) + self.assertCoincide(curve.EndPoint, pt2) + + def assertCoincide(self, pt1, pt2): + """Verify that 2 points coincide (with tolerance).""" + self.assertAbout(pt1.x, pt2.x) + self.assertAbout(pt1.y, pt2.y) + self.assertAbout(pt1.z, pt2.z) + + def assertAbout(self, v1, v2): + """Verify that 2 values are the same (accounting for float imprecision).""" + #print("assertAbout(%f, %f)" % (v1, v2)) + self.assertTrue(math.fabs(v1 - v2) < slack) From 8ce9c0c30588b2a45bae0cd0420a3a483fb17a8d Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 20 Nov 2016 14:53:03 -0800 Subject: [PATCH 11/17] Straight line intersection with square Tag. --- .../PathScripts/PathDressupHoldingTags.py | 188 +++++++++-- .../PathTests/TestPathDressupHoldingTags.py | 313 ++++++++++++++---- src/Mod/Path/TestPathApp.py | 3 +- 3 files changed, 426 insertions(+), 78 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index 82f5a3eb2..60e46af17 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -114,12 +114,12 @@ def testSide(): testPrintSide(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 0,-1, 0)) def pathCommandForEdge(edge): - pt = edge.valueAt(edge.LastParameter) + pt = edge.Curve.EndPoint params = {'X': pt.x, 'Y': pt.y, 'Z': pt.z} if type(edge.Curve) == Part.Line: return Part.Command('G1', params) - p1 = edge.valueAt(edge.FirstParameter) + p1 = edge.Curve.StartPoint p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter)/2) p3 = pt if Side.Left == Side.of(p2 - p1, p3 - p2): @@ -132,13 +132,25 @@ def pathCommandForEdge(edge): class Tag: - def __init__(self, x, y, width, height, angle, enabled=True): + + @classmethod + def FromString(cls, string): + try: + t = eval(string) + return Tag(t[0], t[1], t[2], t[3], t[4], t[5]) + except: + return None + + def __init__(self, x, y, width, height, angle, enabled=True, z=None): self.x = x self.y = y self.width = math.fabs(width) self.height = math.fabs(height) + self.actualHeight = self.height self.angle = math.fabs(angle) self.enabled = enabled + if z is not None: + self.createSolidsAt(z) def toString(self): return str((self.x, self.y, self.width, self.height, self.angle, self.enabled)) @@ -146,7 +158,14 @@ class Tag: def originAt(self, z): return FreeCAD.Vector(self.x, self.y, z) + def bottom(self): + return self.z + + def top(self): + return self.z + self.actualHeight + def createSolidsAt(self, z): + self.z = z r1 = self.width / 2 height = self.height if self.angle == 90 and height > 0: @@ -162,6 +181,7 @@ class Tag: r2 = 0 height = r1 * tangens self.core = None + self.actualHeight = height self.solid = Part.makeCone(r1, r2, height) else: # degenerated case - no tag @@ -171,14 +191,144 @@ class Tag: if self.core: self.core.translate(self.originAt(z)) - @classmethod - def FromString(cls, string): - try: - t = eval(string) - return Tag(t[0], t[1], t[2], t[3], t[4], t[5]) - except: - return None + class Intersection: + # An intersection with a tag has 4 markant points, where one might be optional. + # P1---P2 P2 + # / \ /\ + # / \ / \ + # / \ / \ + # ---P0 P3--- ---P0 P3--- + # If no intersection occured the Intersection can be viewed as being + # at P3 with no additional edges. + P0 = 2 + P1 = 3 + P2 = 4 + P3 = 5 + def __init__(self, tag): + self.tag = tag + self.state = self.P3 + self.edges = [] + self.tail = None + + def isComplete(self): + return self.state == self.P3 + + def moveEdgeToPlateau(self, edge): + if type(edge.Curve) is Part.Line: + pt1 = edge.Curve.StartPoint + pt2 = edge.Curve.EndPoint + pt1.z = self.tag.top() + pt2.z = self.tag.top() + #print("\nplateau= %s - %s" %(pt1, pt2)) + return Part.Edge(Part.Line(pt1, pt2)) + + + def intersect(self, edge): + #print("") + if self.state == self.P0: + #print("----- P0") + if self.tag.core: + self.state = self.P1 + i = self.tag.nextIntersectionClosestTo(edge, self.tag.core, edge.Curve.StartPoint) + if i: + if i == edge.Curve.StartPoint: + self.edges.append(Part.Edge(Part.Line(i, FreeCAD.Vector(i.x, i.y, self.tag.top())))) + else: + e, tail = self.tag.splitEdgeAt(edge, i) + self.edges.append(self.mapEdgeTo(e, self.tag.solid)) + edge = tail + else: + self.edges.append(self.mapEdgeTo(e, self.tag.solid)) + # we're done with this edge + return self + else: + p = self.tag.originAt(self.tag.bottom() + self.tag.actualHeight) + if DraftGeomUtils.isPtOnEdge(p, edge): + e, tail = self.tag.splitEdgeAt(edge, p) + self.edges.append(self.mapEdgeTo(e, self.tag.solid)) + edge = tail + self.state = self.P2 + else: + self.edges.append(self.mapEdgeTo(e, self.tag.solid)) + # we're done with this edge + return self + + if self.state == self.P1: + #print("----- P1") + # must have core, find end of plateau + i = self.tag.nextIntersectionClosestTo(edge, self.tag.core, edge.Curve.EndPoint) + if i and i != edge.Curve.StartPoint: + self.state = self.P2 + if i == edge.Curve.EndPoint: + self.edges.append(self.moveEdgeToPlateau(edge)) + # edge fully consumed + return self + else: + e, tail = self.tag.splitEdgeAt(edge, i) + self.edges.append(self.moveEdgeToPlateau(e)) + edge = tail + else: + self.edges.append(self.moveEdgeToPlateau(edge)) + # edge fully consumed, we're still in P1 + return self + + if self.state == self.P2: + #print("----- P2") + i = self.tag.nextIntersectionClosestTo(edge, self.tag.solid, edge.Curve.EndPoint) + if i: + self.state = self.P3 + #print("----- P3") + if i == edge.Curve.StartPoint: + self.edges.append(Part.Edge(Part.Line(FreeCAD.Vector(i.x, i.y, self.tag.top()), i))) + self.tail = edge + elif i == edge.Curve.EndPoint: + self.edges.append(self.mapEdgeTo(edge, self.tag.solid)) + self.tail = None + else: + e, tail = self.tag.splitEdgeAt(edge, i) + self.edges.append(self.mapEdgeTo(e, self.tag.solid)) + self.tail = tail + + return self + + + + def splitEdgeAt(self, edge, pt): + p = edge.Curve.parameter(pt) + wire = edge.split(p) + return wire.Edges + + + def nextIntersectionClosestTo(self, edge, solid, refPt): + pts = [] + for face in solid.Faces: + i = edge.Curve.intersect(face.Surface)[0] + pts.extend([FreeCAD.Vector(p.X, p.Y, p.Z) for p in i]) + if pts: + closest = sorted(pts, key=lambda pt: (pt - refPt).Length)[0] + return closest + return None + + def intersect(self, edge): + inters = self.Intersection(self) + if edge.Curve.StartPoint.z < self.top() or edge.Curve.EndPoint.z < self.top(): + i = self.nextIntersectionClosestTo(edge, self.solid, edge.Curve.StartPoint) + if i: + inters.state = self.Intersection.P0 + if i == edge.Curve.EndPoint: + inters.edges.append(edge) + return inters + if i == edge.Curve.StartPoint: + tail = edge + else: + e,tail = self.splitEdgeAt(edge, i) + inters.edges.append(e) + return inters.intersect(tail) + # if we get here there is no intersection with the tag + inters.state = self.Intersection.P3 + inters.tail = edge + return inters class PathData: def __init__(self, obj): @@ -235,16 +385,16 @@ class PathData: def sortedBase(self, base): # first find the exit point, where base wire is closed - edges = [e for e in self.edges if e.valueAt(e.FirstParameter).z == self.minZ and e.valueAt(e.LastParameter).z != self.maxZ] - exit = sorted(edges, key=lambda e: -e.valueAt(e.LastParameter).z)[0] - pt = exit.valueAt(exit.FirstParameter) + edges = [e for e in self.edges if e.Curve.StartPoint.z == self.minZ and e.Curve.EndPoint.z != self.maxZ] + exit = sorted(edges, key=lambda e: -e.Curve.EndPoint.z)[0] + pt = exit.Curve.StartPoint # then find the first base edge, and sort them until done ordered = [] while base: - edge = [e for e in base if e.valueAt(e.FirstParameter) == pt][0] + edge = [e for e in base if e.Curve.StartPoint == pt][0] ordered.append(edge) base.remove(edge) - pt = edge.valueAt(edge.LastParameter) + pt = edge.Curve.EndPoint return ordered @@ -364,7 +514,7 @@ class PathData: ordered = [] for edge in self.base.Edges: ts = [t for t in tags if DraftGeomUtils.isPtOnEdge(t.originAt(self.minZ), edge)] - for t in sorted(ts, key=lambda t: (t.originAt(self.minZ) - edge.valueAt(edge.FirstParameter)).Length): + for t in sorted(ts, key=lambda t: (t.originAt(self.minZ) - edge.Curve.StartPoint).Length): tags.remove(t) ordered.append(t) if tags: @@ -392,7 +542,7 @@ class ObjectDressup: def tagIntersection(self, face, edge): - p1 = edge.valueAt(edge.FirstParameter) + p1 = edge.Curve.StartPoint pts = edge.Curve.intersect(face.Surface) if pts[0]: closest = sorted(pts[0], key=lambda pt: (pt - p1).Length)[0] @@ -409,9 +559,9 @@ class ObjectDressup: for face in solid.Faces: pt = self.tagIntersection(face, edge) if pt: - if pt == edge.valueAt(edge.FirstParameter): + if pt == edge.Curve.StartPoint: pt - elif pt != edge.valueAt(edge.LastParameter): + elif pt != edge.Curve.EndPoint: parameter = edge.Curve.parameter(pt) wire = edge.split(parameter) commands.append(pathCommandForEdge(wire.Edges[0])) diff --git a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py index 23554febf..006a34f35 100644 --- a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py +++ b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py @@ -44,61 +44,8 @@ def pointsCoincide(pt1, pt2): return False return True -class PathDressupHoldingTagsTestCases(unittest.TestCase): - """Unit tests for the HoldingTags dressup.""" - - def testTagBasics(self): - """Check Tag origin, serialization and de-serialization.""" - tag = Tag(77, 13, 4, 5, 90, True) - self.assertCoincide(tag.originAt(3), Vector(77, 13, 3)) - s = tag.toString() - tagCopy = Tag.FromString(s) - self.assertEqual(tag.x, tagCopy.x) - self.assertEqual(tag.y, tagCopy.y) - self.assertEqual(tag.height, tagCopy.height) - self.assertEqual(tag.width, tagCopy.width) - self.assertEqual(tag.enabled, tagCopy.enabled) - - - def testTagSolidBasic(self): - """For a 90 degree tag the core and solid are both defined and identical cylinders.""" - tag = Tag(100, 200, 4, 5, 90, True) - tag.createSolidsAt(17) - - self.assertIsNotNone(tag.solid) - self.assertCylinderAt(tag.solid, Vector(100, 200, 17), 2, 5) - - self.assertIsNotNone(tag.core) - self.assertCylinderAt(tag.core, Vector(100, 200, 17), 2, 5) - - def testTagSolidFlatCone(self): - """Tests a Tag that has an angle leaving a flat face on top of the cone.""" - tag = Tag(0, 0, 18, 5, 45, True) - tag.createSolidsAt(0) - - self.assertIsNotNone(tag.solid) - self.assertConeAt(tag.solid, Vector(0,0,0), 9, 4, 5) - - self.assertIsNotNone(tag.core) - self.assertCylinderAt(tag.core, Vector(0,0,0), 4, 5) - - def testTagSolidCone(self): - """Tests a Tag who's angled sides coincide at the tag's height.""" - tag = Tag(0, 0, 10, 5, 45, True) - tag.createSolidsAt(0) - self.assertIsNotNone(tag.solid) - self.assertConeAt(tag.solid, Vector(0,0,0), 5, 0, 5) - - self.assertIsNone(tag.core) - - def testTagSolidShortCone(self): - """Tests a Tag that's not wide enough to reach full height.""" - tag = Tag(0, 0, 5, 17, 60, True) - tag.createSolidsAt(0) - self.assertIsNotNone(tag.solid) - self.assertConeAt(tag.solid, Vector(0,0,0), 2.5, 0, 2.5 * math.tan((60/180.0)*math.pi)) - - self.assertIsNone(tag.core) +class TagTestCaseBase(unittest.TestCase): + """Base class for all tag test cases providing additional assert functions.""" def assertCylinderAt(self, solid, pt, r, h): """Verify that solid is a cylinder at the specified location.""" @@ -127,14 +74,14 @@ class PathDressupHoldingTagsTestCases(unittest.TestCase): def assertCircle(self, edge, pt, r): """Verivy that edge is a circle at given location.""" curve = edge.Curve - self.assertTrue(type(curve), Part.Circle) + self.assertIs(type(curve), Part.Circle) self.assertCoincide(curve.Center, Vector(pt.x, pt.y, pt.z)) self.assertAbout(curve.Radius, r) def assertLine(self, edge, pt1, pt2): """Verify that edge is a line from pt1 to pt2.""" curve = edge.Curve - self.assertTrue(type(curve), Part.Line) + self.assertIs(type(curve), Part.Line) self.assertCoincide(curve.StartPoint, pt1) self.assertCoincide(curve.EndPoint, pt2) @@ -147,4 +94,254 @@ class PathDressupHoldingTagsTestCases(unittest.TestCase): def assertAbout(self, v1, v2): """Verify that 2 values are the same (accounting for float imprecision).""" #print("assertAbout(%f, %f)" % (v1, v2)) - self.assertTrue(math.fabs(v1 - v2) < slack) + if math.fabs(v1 - v2) > slack: + self.fail("%f != %f" % (v1, v2)) + + def assertTrapezoid(self, edgs, tail, spec): + """Check that there are 5 edges forming a trapezoid.""" + edges = list(edgs) + if tail: + edges.append(tail) + self.assertEqual(len(edges), 5) + + p0 = spec[0] + p1 = Vector(spec[1], p0.y, p0.z) + p2 = Vector(p1.x, p1.y, spec[2]) + p3 = Vector(-p2.x, p2.y, p2.z) + p4 = Vector(p3.x, p3.y, p0.z) + p5 = spec[3] + + self.assertLine(edges[0], p0, p1) + self.assertLine(edges[1], p1, p2) + self.assertLine(edges[2], p2, p3) + self.assertLine(edges[3], p3, p4) + self.assertLine(edges[4], p4, p5) + + +class TagTestCases(TagTestCaseBase): # ============= + """Unit tests for the HoldingTags dressup.""" + + def testTagBasics(self): + #"""Check Tag origin, serialization and de-serialization.""" + tag = Tag(77, 13, 4, 5, 90, True) + self.assertCoincide(tag.originAt(3), Vector(77, 13, 3)) + s = tag.toString() + tagCopy = Tag.FromString(s) + self.assertEqual(tag.x, tagCopy.x) + self.assertEqual(tag.y, tagCopy.y) + self.assertEqual(tag.height, tagCopy.height) + self.assertEqual(tag.width, tagCopy.width) + self.assertEqual(tag.enabled, tagCopy.enabled) + + + def testTagSolidBasic(self): + #"""For a 90 degree tag the core and solid are both defined and identical cylinders.""" + tag = Tag(100, 200, 4, 5, 90, True) + tag.createSolidsAt(17) + + self.assertIsNotNone(tag.solid) + self.assertCylinderAt(tag.solid, Vector(100, 200, 17), 2, 5) + + self.assertIsNotNone(tag.core) + self.assertCylinderAt(tag.core, Vector(100, 200, 17), 2, 5) + + def testTagSolidFlatCone(self): + #"""Tests a Tag that has an angle leaving a flat face on top of the cone.""" + tag = Tag(0, 0, 18, 5, 45, True) + tag.createSolidsAt(0) + + self.assertIsNotNone(tag.solid) + self.assertConeAt(tag.solid, Vector(0,0,0), 9, 4, 5) + + self.assertIsNotNone(tag.core) + self.assertCylinderAt(tag.core, Vector(0,0,0), 4, 5) + + def testTagSolidCone(self): + #"""Tests a Tag who's angled sides coincide at the tag's height.""" + tag = Tag(0, 0, 10, 5, 45, True) + tag.createSolidsAt(0) + self.assertIsNotNone(tag.solid) + self.assertConeAt(tag.solid, Vector(0,0,0), 5, 0, 5) + + self.assertIsNone(tag.core) + + def testTagSolidShortCone(self): + #"""Tests a Tag that's not wide enough to reach full height.""" + tag = Tag(0, 0, 5, 17, 60, True) + tag.createSolidsAt(0) + self.assertIsNotNone(tag.solid) + self.assertConeAt(tag.solid, Vector(0,0,0), 2.5, 0, 2.5 * math.tan((60/180.0)*math.pi)) + + self.assertIsNone(tag.core) + +class SquareTagTestCases(TagTestCaseBase): # ============= + """Unit tests for square tags.""" + + def testTagNoIntersect(self): + #"""Check that the returned tail if no intersection occurs matches the input.""" + tag = Tag( 0, 0, 4, 7, 90, True, 0) + pt1 = Vector(+5, 3, 0) + pt2 = Vector(-5, 3, 0) + edge = Part.Edge(Part.Line(pt1, pt2)) + + i = tag.intersect(edge) + self.assertIsNotNone(i) + self.assertTrue(i.isComplete()) + self.assertIsNotNone(i.edges) + self.assertFalse(i.edges) + self.assertLine(i.tail, pt1, pt2) + + def testTagIntersectLine(self): + #"""Test that a straight line passing through a cylindrical tag is split up into 5 segments.""" + tag = Tag( 0, 0, 4, 7, 90, True, 0) + pt1 = Vector(+5, 0, 0) + pt2 = Vector(-5, 0, 0) + edge = Part.Edge(Part.Line(pt1, pt2)) + + i = tag.intersect(edge) + self.assertIsNotNone(i) + self.assertTrue(i.isComplete()) + + pt0a = Vector(+2, 0, 0) + pt0b = Vector(+2, 0, 7) + pt0c = Vector(-2, 0, 7) + pt0d = Vector(-2, 0, 0) + + self.assertEqual(len(i.edges), 4) + self.assertLine(i.edges[0], pt1, pt0a) + self.assertLine(i.edges[1], pt0a, pt0b) + self.assertLine(i.edges[2], pt0b, pt0c) + self.assertLine(i.edges[3], pt0c, pt0d) + self.assertLine(i.tail, pt0d, pt2) + + + def testTagIntersectPartialLineP0(self): + #"""Make sure line is accounted for if it reaches P0.""" + tag = Tag( 0, 0, 4, 7, 90, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(2, 0, 0))) + + i = tag.intersect(edge) + self.assertFalse(i.isComplete()) + + self.assertEqual(len(i.edges), 1) + self.assertLine(i.edges[0], edge.Curve.StartPoint, edge.Curve.EndPoint) + self.assertIsNone(i.tail) + + + def testTagIntersectPartialLineP1(self): + #"""Make sure line is accounted for if it reaches beyond P1.""" + tag = Tag( 0, 0, 4, 7, 90, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(1, 0, 0))) + + i = tag.intersect(edge) + self.assertFalse(i.isComplete()) + + pt0a = Vector(+2, 0, 0) + pt0b = Vector(+2, 0, 7) + pt1a = Vector(+1, 0, 7) + + self.assertEqual(len(i.edges), 3) + self.assertLine(i.edges[0], edge.Curve.StartPoint, pt0a) + self.assertLine(i.edges[1], pt0a, pt0b) + self.assertLine(i.edges[2], pt0b, pt1a) + self.assertIsNone(i.tail) + + + def testTagIntersectPartialLineP2(self): + #"""Make sure line is accounted for if it reaches beyond P2.""" + tag = Tag( 0, 0, 4, 7, 90, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-1, 0, 0))) + + i = tag.intersect(edge) + self.assertFalse(i.isComplete()) + + pt0a = Vector(+2, 0, 0) + pt0b = Vector(+2, 0, 7) + pt1a = Vector(-1, 0, 7) + + self.assertEqual(len(i.edges), 3) + self.assertLine(i.edges[0], edge.Curve.StartPoint, pt0a) + self.assertLine(i.edges[1], pt0a, pt0b) + self.assertLine(i.edges[2], pt0b, pt1a) + self.assertIsNone(i.tail) + + def testTagIntersectPartialLineP11(self): + #"""Make sure a line is accounted for if it lies entirely between P1 and P2.""" + tag = Tag( 0, 0, 4, 7, 90, True, 0) + e1 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+1, 0, 0))) + + i = tag.intersect(e1) + self.assertFalse(i.isComplete()) + + e2 = Part.Edge(Part.Line(e1.Curve.EndPoint, Vector(0,0,0))) + i = i.intersect(e2) + + pt0a = Vector(+2, 0, 0) + pt0b = Vector(+2, 0, 7) + pt1a = Vector(+1, 0, 7) + pt1b = Vector( 0, 0, 7) + + self.assertEqual(len(i.edges), 4) + self.assertLine(i.edges[0], e1.Curve.StartPoint, pt0a) + self.assertLine(i.edges[1], pt0a, pt0b) + self.assertLine(i.edges[2], pt0b, pt1a) + self.assertLine(i.edges[3], pt1a, pt1b) + self.assertIsNone(i.tail) + + def testTagIntersectPartialLinesP11223(self): + #"""Verify all lines between P0 and P3 are added.""" + tag = Tag( 0, 0, 4, 7, 90, True, 0) + e0 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+2, 0, 0))) + e1 = Part.Edge(Part.Line(e0.Curve.EndPoint, Vector(+1, 0, 0))) + e2 = Part.Edge(Part.Line(e1.Curve.EndPoint, Vector(+0.5, 0, 0))) + e3 = Part.Edge(Part.Line(e2.Curve.EndPoint, Vector(-0.5, 0, 0))) + e4 = Part.Edge(Part.Line(e3.Curve.EndPoint, Vector(-1, 0, 0))) + e5 = Part.Edge(Part.Line(e4.Curve.EndPoint, Vector(-2, 0, 0))) + e6 = Part.Edge(Part.Line(e5.Curve.EndPoint, Vector(-5, 0, 0))) + + i = tag + for e in [e0, e1, e2, e3, e4, e5]: + i = i.intersect(e) + self.assertFalse(i.isComplete()) + i = i.intersect(e6) + self.assertTrue(i.isComplete()) + + pt0 = Vector(2, 0, 0) + pt1 = Vector(2, 0, 7) + pt2 = Vector(1, 0, 7) + pt3 = Vector(0.5, 0, 7) + pt4 = Vector(-0.5, 0, 7) + pt5 = Vector(-1, 0, 7) + pt6 = Vector(-2, 0, 7) + + self.assertEqual(len(i.edges), 8) + + self.assertLine(i.edges[0], e0.Curve.StartPoint, pt0) + self.assertLine(i.edges[1], pt0, pt1) + self.assertLine(i.edges[2], pt1, pt2) + self.assertLine(i.edges[3], pt2, pt3) + self.assertLine(i.edges[4], pt3, pt4) + self.assertLine(i.edges[5], pt4, pt5) + self.assertLine(i.edges[6], pt5, pt6) + self.assertLine(i.edges[7], pt6, e5.Curve.EndPoint) + self.assertTrue(i.isComplete()) + + self.assertIsNotNone(i.tail) + self.assertLine(i.tail, e6.Curve.StartPoint, e6.Curve.EndPoint) + + def testTagIntersectLineAt(self): + tag = Tag( 0, 0, 4, 7, 90, True, 0) + # for all lines below 7 we get the trapezoid + for i in range(0, 7): + edge = Part.Edge(Part.Line(Vector(5, 0, i), Vector(-5, 0, i))) + s = tag.intersect(edge) + self.assertTrue(s.isComplete()) + self.assertTrapezoid(s.edges, s.tail, [edge.Curve.StartPoint, 2, 7, edge.Curve.EndPoint]) + + # for all edges at height or above the original line is used + for i in range(7, 9): + edge = Part.Edge(Part.Line(Vector(5, 0, i), Vector(-5, 0, i))) + s = tag.intersect(edge) + self.assertTrue(s.isComplete()) + self.assertLine(s.tail, edge.Curve.StartPoint, edge.Curve.EndPoint) + diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 90640ab54..c719142d1 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -25,4 +25,5 @@ import TestApp from PathTests.TestPathPost import PathPostTestCases -from PathTests.TestPathDressupHoldingTags import PathDressupHoldingTagsTestCases +from PathTests.TestPathDressupHoldingTags import TagTestCases +from PathTests.TestPathDressupHoldingTags import SquareTagTestCases From 1fd4c49fca7b5f513718ff00c40dba8b5753bb12 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 21 Nov 2016 19:51:54 -0800 Subject: [PATCH 12/17] Test cases for basic path generation for tags. --- .../PathScripts/PathDressupHoldingTags.py | 223 +++---- .../PathTests/TestPathDressupHoldingTags.py | 544 ++++++++++++++++-- src/Mod/Path/TestPathApp.py | 8 +- 3 files changed, 629 insertions(+), 146 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index 60e46af17..cc204d7bc 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -61,6 +61,14 @@ movecw = ['G2', 'G02'] moveccw = ['G3', 'G03'] movearc = movecw + moveccw +slack = 0.0000001 + +def isAbout(v1, v2): + return math.fabs(v1 - v2) < slack + +def pointsCoincide(p1, p2): + return isAbout(p1.x, p2.x) and isAbout(p1.y, p2.y) and isAbout(p1.z, p2.z) + def getAngle(v): a = v.getAngle(FreeCAD.Vector(1,0,0)) if v.y < 0: @@ -90,29 +98,6 @@ class Side: return cls.Right return cls.Straight -def testPrintAngle(v): - print("(%+.2f, %+.2f, %+.2f): %+.2f" % (v.x, v.y, v.z, getAngle(v)/math.pi)) - -def testAngle(x=1, y=1): - testPrintAngle(FreeCAD.Vector( 1*x, 0*y, 0)) - testPrintAngle(FreeCAD.Vector( 1*x, 1*y, 0)) - testPrintAngle(FreeCAD.Vector( 0*x, 1*y, 0)) - testPrintAngle(FreeCAD.Vector(-1*x, 1*y, 0)) - testPrintAngle(FreeCAD.Vector(-1*x, 0*y, 0)) - testPrintAngle(FreeCAD.Vector(-1*x,-1*y, 0)) - testPrintAngle(FreeCAD.Vector( 0*x,-1*y, 0)) - testPrintAngle(FreeCAD.Vector( 1*x,-1*y, 0)) - - -def testPrintSide(pt1, pt2): - print('(%.2f, %.2f) - (%.2f, %.2f) -> %s' % (pt1.x, pt1.y, pt2.x, pt2.y, Side.toString(Side.of(pt1, pt2)))) - -def testSide(): - testPrintSide(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 1, 0, 0)) - testPrintSide(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector(-1, 0, 0)) - testPrintSide(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 0, 1, 0)) - testPrintSide(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 0,-1, 0)) - def pathCommandForEdge(edge): pt = edge.Curve.EndPoint params = {'X': pt.x, 'Y': pt.y, 'Z': pt.z} @@ -193,11 +178,13 @@ class Tag: class Intersection: # An intersection with a tag has 4 markant points, where one might be optional. - # P1---P2 P2 - # / \ /\ - # / \ / \ - # / \ / \ - # ---P0 P3--- ---P0 P3--- + # + # P1---P2 P1---P2 P2 + # | | / \ /\ + # | | / \ / \ + # | | / \ / \ + # ---P0 P3--- ---P0 P3--- ---P0 P3--- + # # If no intersection occured the Intersection can be viewed as being # at P3 with no additional edges. P0 = 2 @@ -223,90 +210,131 @@ class Tag: #print("\nplateau= %s - %s" %(pt1, pt2)) return Part.Edge(Part.Line(pt1, pt2)) + def intersectP0(self, edge): + #print("----- P0 (%s - %s)" % (edge.Curve.StartPoint, edge.Curve.EndPoint)) + + i = self.tag.nextIntersectionClosestTo(edge, self.tag.core, edge.Curve.StartPoint) + if i: + if pointsCoincide(i, edge.Curve.StartPoint): + # if P0 and P1 are the same, we need to insert a segment for the rise + self.edges.append(Part.Edge(Part.Line(i, FreeCAD.Vector(i.x, i.y, self.tag.top())))) + self.p1 = i + self.state = self.P1 + return edge + if pointsCoincide(i, edge.Curve.EndPoint): + e = edge + tail = None + else: + e, tail = self.tag.splitEdgeAt(edge, i) + self.p1 = e.Curve.EndPoint + self.edges.append(self.tag.mapEdgeToSolid(e)) + self.state = self.P1 + return tail + # no intersection, the entire edge fits between P0 and P1 + self.edges.append(self.tag.mapEdgeToSolid(edge)) + return None + + def intersectP1(self, edge): + #print("----- P1 (%s - %s)" % (edge.Curve.StartPoint, edge.Curve.EndPoint)) + i = self.tag.nextIntersectionClosestTo(edge, self.tag.core, edge.Curve.EndPoint) + if i: + if pointsCoincide(i, edge.Curve.StartPoint): + self.edges.append(self.tag.mapEdgeToSolid(edge)) + return self + if pointsCoincide(i, edge.Curve.EndPoint): + e = edge + tail = None + else: + e, tail = self.tag.splitEdgeAt(edge, i) + self.p2 = e.Curve.EndPoint + self.state = self.P2 + else: + e = edge + tail = None + self.edges.append(self.moveEdgeToPlateau(e)) + return tail + + def intersectP2(self, edge): + #print("----- P2 (%s - %s)" % (edge.Curve.StartPoint, edge.Curve.EndPoint)) + i = self.tag.nextIntersectionClosestTo(edge, self.tag.solid, edge.Curve.EndPoint) + if i: + if pointsCoincide(i, edge.Curve.StartPoint): + #print("------- insert exit plunge (%s)" % i) + self.edges.append(Part.Edge(Part.Line(FreeCAD.Vector(i.x, i.y, self.tag.top()), i))) + e = None + tail = edge + elif pointsCoincide(i, edge.Curve.EndPoint): + #print("------- entire segment added (%s)" % i) + e = edge + tail = None + else: + e, tail = self.tag.splitEdgeAt(edge, i) + #if tail: + # print("----- P3 (%s - %s)" % (tail.Curve.StartPoint, tail.Curve.EndPoint)) + #else: + # print("----- P3 (---)") + self.state = self.P3 + self.tail = tail + else: + e = edge + tail = None + if e: + self.edges.append(self.tag.mapEdgeToSolid(e)) + return tail def intersect(self, edge): #print("") - if self.state == self.P0: - #print("----- P0") - if self.tag.core: - self.state = self.P1 - i = self.tag.nextIntersectionClosestTo(edge, self.tag.core, edge.Curve.StartPoint) - if i: - if i == edge.Curve.StartPoint: - self.edges.append(Part.Edge(Part.Line(i, FreeCAD.Vector(i.x, i.y, self.tag.top())))) - else: - e, tail = self.tag.splitEdgeAt(edge, i) - self.edges.append(self.mapEdgeTo(e, self.tag.solid)) - edge = tail - else: - self.edges.append(self.mapEdgeTo(e, self.tag.solid)) - # we're done with this edge - return self - else: - p = self.tag.originAt(self.tag.bottom() + self.tag.actualHeight) - if DraftGeomUtils.isPtOnEdge(p, edge): - e, tail = self.tag.splitEdgeAt(edge, p) - self.edges.append(self.mapEdgeTo(e, self.tag.solid)) - edge = tail - self.state = self.P2 - else: - self.edges.append(self.mapEdgeTo(e, self.tag.solid)) - # we're done with this edge - return self - - if self.state == self.P1: - #print("----- P1") - # must have core, find end of plateau - i = self.tag.nextIntersectionClosestTo(edge, self.tag.core, edge.Curve.EndPoint) - if i and i != edge.Curve.StartPoint: - self.state = self.P2 - if i == edge.Curve.EndPoint: - self.edges.append(self.moveEdgeToPlateau(edge)) - # edge fully consumed - return self - else: - e, tail = self.tag.splitEdgeAt(edge, i) - self.edges.append(self.moveEdgeToPlateau(e)) - edge = tail - else: - self.edges.append(self.moveEdgeToPlateau(edge)) - # edge fully consumed, we're still in P1 - return self - - if self.state == self.P2: - #print("----- P2") - i = self.tag.nextIntersectionClosestTo(edge, self.tag.solid, edge.Curve.EndPoint) - if i: - self.state = self.P3 - #print("----- P3") - if i == edge.Curve.StartPoint: - self.edges.append(Part.Edge(Part.Line(FreeCAD.Vector(i.x, i.y, self.tag.top()), i))) - self.tail = edge - elif i == edge.Curve.EndPoint: - self.edges.append(self.mapEdgeTo(edge, self.tag.solid)) - self.tail = None - else: - e, tail = self.tag.splitEdgeAt(edge, i) - self.edges.append(self.mapEdgeTo(e, self.tag.solid)) - self.tail = tail - + #print(" >>> (%s - %s)" % (edge.Curve.StartPoint, edge.Curve.EndPoint)) + if edge and self.state == self.P0: + edge = self.intersectP0(edge) + if edge and self.state == self.P1: + edge = self.intersectP1(edge) + if edge and self.state == self.P2: + edge = self.intersectP2(edge) return self - def splitEdgeAt(self, edge, pt): p = edge.Curve.parameter(pt) wire = edge.split(p) return wire.Edges + def mapEdgeToSolid(self, edge): + #print("mapEdgeToSolid: (%s %s)" % (edge.Curve.StartPoint, edge.Curve.EndPoint)) + p1a = edge.Curve.StartPoint + p1b = FreeCAD.Vector(p1a.x, p1a.y, p1a.z + self.height) + e1 = Part.Edge(Part.Line(p1a, p1b)) + p1 = self.nextIntersectionClosestTo(e1, self.solid, p1b) # top most intersection + #print(" p1: (%s %s) -> %s" % (p1a, p1b, p1)) + + p2a = edge.Curve.EndPoint + p2b = FreeCAD.Vector(p2a.x, p2a.y, p2a.z + self.height) + e2 = Part.Edge(Part.Line(p2a, p2b)) + p2 = self.nextIntersectionClosestTo(e2, self.solid, p2b) # top most intersection + #print(" p2: (%s %s) -> %s" % (p2a, p2b, p2)) + + if type(edge.Curve) == Part.Line: + return Part.Edge(Part.Line(p1, p2)) + + def filterIntersections(self, pts, face): + if type(face.Surface) == Part.Cone or type(face.Surface) == Part.Cylinder: + return filter(lambda pt: pt.z >= self.bottom() and pt.z <= self.top(), pts) + if type(face.Surface) == Part.Plane: + c = face.Edges[0].Curve + if (type(c) == Part.Circle): + return filter(lambda pt: (pt - c.Center).Length <= c.Radius, pts) + print("==== we got a %s" % face.Surface) + def nextIntersectionClosestTo(self, edge, solid, refPt): pts = [] - for face in solid.Faces: + for index, face in enumerate(solid.Faces): i = edge.Curve.intersect(face.Surface)[0] - pts.extend([FreeCAD.Vector(p.X, p.Y, p.Z) for p in i]) + ps = self.filterIntersections([FreeCAD.Vector(p.X, p.Y, p.Z) for p in i], face) + pts.extend(ps) if pts: closest = sorted(pts, key=lambda pt: (pt - refPt).Length)[0] + #print("--pts: %s -> %s" % (pts, closest)) return closest return None @@ -316,10 +344,11 @@ class Tag: i = self.nextIntersectionClosestTo(edge, self.solid, edge.Curve.StartPoint) if i: inters.state = self.Intersection.P0 - if i == edge.Curve.EndPoint: + inters.p0 = i + if pointsCoincide(i, edge.Curve.EndPoint): inters.edges.append(edge) return inters - if i == edge.Curve.StartPoint: + if pointsCoincide(i, edge.Curve.StartPoint): tail = edge else: e,tail = self.splitEdgeAt(edge, i) diff --git a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py index 006a34f35..b4776f22e 100644 --- a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py +++ b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py @@ -30,9 +30,7 @@ import math import unittest from FreeCAD import Vector -from PathScripts.PathDressupHoldingTags import Tag - -slack = 0.0000001 +from PathScripts.PathDressupHoldingTags import * def pointsCoincide(pt1, pt2): pt = pt1 - pt2 @@ -93,36 +91,49 @@ class TagTestCaseBase(unittest.TestCase): def assertAbout(self, v1, v2): """Verify that 2 values are the same (accounting for float imprecision).""" - #print("assertAbout(%f, %f)" % (v1, v2)) if math.fabs(v1 - v2) > slack: self.fail("%f != %f" % (v1, v2)) - def assertTrapezoid(self, edgs, tail, spec): + def assertTrapezoid(self, edgs, tail, points): """Check that there are 5 edges forming a trapezoid.""" edges = list(edgs) if tail: edges.append(tail) self.assertEqual(len(edges), 5) - p0 = spec[0] - p1 = Vector(spec[1], p0.y, p0.z) - p2 = Vector(p1.x, p1.y, spec[2]) - p3 = Vector(-p2.x, p2.y, p2.z) - p4 = Vector(p3.x, p3.y, p0.z) - p5 = spec[3] + self.assertLine(edges[0], points[0], points[1]) + self.assertLine(edges[1], points[1], points[2]) + self.assertLine(edges[2], points[2], points[3]) + self.assertLine(edges[3], points[3], points[4]) + self.assertLine(edges[4], points[4], points[5]) - self.assertLine(edges[0], p0, p1) - self.assertLine(edges[1], p1, p2) - self.assertLine(edges[2], p2, p3) - self.assertLine(edges[3], p3, p4) - self.assertLine(edges[4], p4, p5) +class TestTag00BasicHolding(TagTestCaseBase): + """Some basid test cases.""" + + def test00(self,x=1, y=1): + """Test getAngle.""" + self.assertAbout(getAngle(FreeCAD.Vector( 1*x, 0*y, 0)), 0) + self.assertAbout(getAngle(FreeCAD.Vector( 1*x, 1*y, 0)), math.pi/4) + self.assertAbout(getAngle(FreeCAD.Vector( 0*x, 1*y, 0)), math.pi/2) + self.assertAbout(getAngle(FreeCAD.Vector(-1*x, 1*y, 0)), 3*math.pi/4) + self.assertAbout(getAngle(FreeCAD.Vector(-1*x, 0*y, 0)), math.pi) + self.assertAbout(getAngle(FreeCAD.Vector(-1*x,-1*y, 0)), -3*math.pi/4) + self.assertAbout(getAngle(FreeCAD.Vector( 0*x,-1*y, 0)), -math.pi/2) + self.assertAbout(getAngle(FreeCAD.Vector( 1*x,-1*y, 0)), -math.pi/4) + + def test01(self): + """Test class Side.""" + self.assertEqual(Side.of(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 1, 0, 0)), Side.On) + self.assertEqual(Side.of(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector(-1, 0, 0)), Side.On) + self.assertEqual(Side.of(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 0, 1, 0)), Side.Left) + self.assertEqual(Side.of(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 0,-1, 0)), Side.Right) -class TagTestCases(TagTestCaseBase): # ============= +class TestTag01BasicTag(TagTestCaseBase): # ============= """Unit tests for the HoldingTags dressup.""" - def testTagBasics(self): - #"""Check Tag origin, serialization and de-serialization.""" + def test00(self): + """Check Tag origin, serialization and de-serialization.""" tag = Tag(77, 13, 4, 5, 90, True) self.assertCoincide(tag.originAt(3), Vector(77, 13, 3)) s = tag.toString() @@ -134,8 +145,8 @@ class TagTestCases(TagTestCaseBase): # ============= self.assertEqual(tag.enabled, tagCopy.enabled) - def testTagSolidBasic(self): - #"""For a 90 degree tag the core and solid are both defined and identical cylinders.""" + def test01(self): + """Verify solid and core for a 90 degree tag are identical cylinders.""" tag = Tag(100, 200, 4, 5, 90, True) tag.createSolidsAt(17) @@ -145,8 +156,8 @@ class TagTestCases(TagTestCaseBase): # ============= self.assertIsNotNone(tag.core) self.assertCylinderAt(tag.core, Vector(100, 200, 17), 2, 5) - def testTagSolidFlatCone(self): - #"""Tests a Tag that has an angle leaving a flat face on top of the cone.""" + def test02(self): + """Verify an angled tag has a cone shape with a lid, and cylinder core.""" tag = Tag(0, 0, 18, 5, 45, True) tag.createSolidsAt(0) @@ -156,8 +167,8 @@ class TagTestCases(TagTestCaseBase): # ============= self.assertIsNotNone(tag.core) self.assertCylinderAt(tag.core, Vector(0,0,0), 4, 5) - def testTagSolidCone(self): - #"""Tests a Tag who's angled sides coincide at the tag's height.""" + def test03(self): + """Verify pointy cone shape of tag with pointy end if width, angle and height match up.""" tag = Tag(0, 0, 10, 5, 45, True) tag.createSolidsAt(0) self.assertIsNotNone(tag.solid) @@ -165,8 +176,8 @@ class TagTestCases(TagTestCaseBase): # ============= self.assertIsNone(tag.core) - def testTagSolidShortCone(self): - #"""Tests a Tag that's not wide enough to reach full height.""" + def test04(self): + """Verify height adjustment if tag isn't wide eough for angle.""" tag = Tag(0, 0, 5, 17, 60, True) tag.createSolidsAt(0) self.assertIsNotNone(tag.solid) @@ -174,14 +185,267 @@ class TagTestCases(TagTestCaseBase): # ============= self.assertIsNone(tag.core) -class SquareTagTestCases(TagTestCaseBase): # ============= + def test10(self): + """Verify intersection of square tag with line ending at tag start.""" + tag = Tag( 0, 0, 8, 3, 90, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(4, 0, 0))) + + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P0) + self.assertEqual(len(i.edges), 1) + self.assertLine(i.edges[0], edge.Curve.StartPoint, edge.Curve.EndPoint) + self.assertIsNone(i.tail) + + def test11(self): + """Verify intersection of square tag with line ending between P1 and P2.""" + tag = Tag( 0, 0, 8, 3, 90, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(1, 0, 0))) + + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P1) + self.assertEqual(len(i.edges), 3) + p1 = Vector(4, 0, 0) + p2 = Vector(4, 0, 3) + p3 = Vector(1, 0, 3) + self.assertLine(i.edges[0], edge.Curve.StartPoint, p1) + self.assertLine(i.edges[1], p1, p2) + self.assertLine(i.edges[2], p2, p3) + self.assertIsNone(i.tail) + + # verify we stay in P1 if we add another segment + edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(0, 0, 0))) + i = i.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P1) + self.assertEqual(len(i.edges), 4) + p4 = Vector(0, 0, 3) + self.assertLine(i.edges[3], p3, p4) + self.assertIsNone(i.tail) + + def test12(self): + """Verify intesection of square tag with line ending on P2.""" + tag = Tag( 0, 0, 8, 3, 90, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-4, 0, 0))) + + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P2) + self.assertEqual(len(i.edges), 3) + p0 = edge.Curve.StartPoint + p1 = Vector( 4, 0, 0) + p2 = Vector( 4, 0, 3) + p3 = Vector(-4, 0, 3) + self.assertLine(i.edges[0], p0, p1) + self.assertLine(i.edges[1], p1, p2) + self.assertLine(i.edges[2], p2, p3) + self.assertIsNone(i.tail) + + # make sure it also works if we get there not directly + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(0, 0, 0))) + i = tag.intersect(edge) + edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(-4, 0, 0))) + i = i.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P2) + self.assertEqual(len(i.edges), 4) + p2a = Vector( 0, 0, 3) + self.assertLine(i.edges[0], p0, p1) + self.assertLine(i.edges[1], p1, p2) + self.assertLine(i.edges[2], p2, p2a) + self.assertLine(i.edges[3], p2a, p3) + self.assertIsNone(i.tail) + + def test13(self): + """Verify plunge down is inserted for square tag on exit.""" + tag = Tag( 0, 0, 8, 3, 90, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-5, 0, 0))) + + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P3) + self.assertTrue(i.isComplete()) + self.assertEqual(len(i.edges), 4) + p0 = edge.Curve.StartPoint + p1 = Vector( 4, 0, 0) + p2 = Vector( 4, 0, 3) + p3 = Vector(-4, 0, 3) + p4 = Vector(-4, 0, 0) + p5 = edge.Curve.EndPoint + self.assertLine(i.edges[0], p0, p1) + self.assertLine(i.edges[1], p1, p2) + self.assertLine(i.edges[2], p2, p3) + self.assertLine(i.edges[3], p3, p4) + self.assertIsNotNone(i.tail) + self.assertLine(i.tail, p4, p5) + + def test20(self): + """Veify intersection of angled tag with line ending before P1.""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(4, 0, 0))) + + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P0) + self.assertEqual(len(i.edges), 1) + self.assertLine(i.edges[0], edge.Curve.StartPoint, edge.Curve.EndPoint) + self.assertIsNone(i.tail) + + # now add another segment that doesn't reach the top of the cone + edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(3, 0, 0))) + i = i.intersect(edge) + # still a P0 and edge fully consumed + p1 = Vector(edge.Curve.StartPoint) + p1.z = 0 + p2 = Vector(edge.Curve.EndPoint) + p2.z = 1 # height of cone @ (3,0) + self.assertEqual(i.state, Tag.Intersection.P0) + self.assertEqual(len(i.edges), 2) + self.assertLine(i.edges[1], p1, p2) + self.assertIsNone(i.tail) + + # add another segment to verify starting point offset + edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(2, 0, 0))) + i = i.intersect(edge) + # still a P0 and edge fully consumed + p3 = Vector(edge.Curve.EndPoint) + p3.z = 2 # height of cone @ (2,0) + self.assertEqual(i.state, Tag.Intersection.P0) + self.assertEqual(len(i.edges), 3) + self.assertLine(i.edges[2], p2, p3) + self.assertIsNone(i.tail) + + def test21(self): + """Verify intersection of angled tag with line ending between P1 and P2""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(1, 0, 0))) + + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P1) + self.assertEqual(len(i.edges), 2) + p1 = Vector(4, 0, 0) + p2 = Vector(1, 0, 3) + self.assertLine(i.edges[0], edge.Curve.StartPoint, p1) + self.assertLine(i.edges[1], p1, p2) + self.assertIsNone(i.tail) + + # verify we stay in P1 if we add another segment + edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(0, 0, 0))) + i = i.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P1) + self.assertEqual(len(i.edges), 3) + p3 = Vector(0, 0, 3) + self.assertLine(i.edges[2], p2, p3) + self.assertIsNone(i.tail) + + def test22(self): + """Verify intersection of angled tag with edge ending on P2.""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-1, 0, 0))) + + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P2) + self.assertEqual(len(i.edges), 3) + p0 = Vector(edge.Curve.StartPoint) + p1 = Vector(4, 0, 0) + p2 = Vector(1, 0, 3) + p3 = Vector(-1, 0, 3) + self.assertLine(i.edges[0], p0, p1) + self.assertLine(i.edges[1], p1, p2) + self.assertLine(i.edges[2], p2, p3) + self.assertIsNone(i.tail) + + # make sure we get the same result if there's another edge + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(1, 0, 0))) + i = tag.intersect(edge) + edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(-1, 0, 0))) + i = i.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P2) + self.assertEqual(len(i.edges), 3) + self.assertLine(i.edges[0], p0, p1) + self.assertLine(i.edges[1], p1, p2) + self.assertLine(i.edges[2], p2, p3) + self.assertIsNone(i.tail) + + # and also if the last segment doesn't cross the entire plateau + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(0.5, 0, 0))) + i = tag.intersect(edge) + edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(-1, 0, 0))) + i = i.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P2) + self.assertEqual(len(i.edges), 4) + p2a = Vector(0.5, 0, 3) + self.assertLine(i.edges[0], p0, p1) + self.assertLine(i.edges[1], p1, p2) + self.assertLine(i.edges[2], p2, p2a) + self.assertLine(i.edges[3], p2a, p3) + self.assertIsNone(i.tail) + + def test23(self): + """Verify proper down plunge on angled tag exit.""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-2, 0, 0))) + + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P2) + self.assertEqual(len(i.edges), 4) + p0 = Vector(5, 0, 0) + p1 = Vector(4, 0, 0) + p2 = Vector(1, 0, 3) + p3 = Vector(-1, 0, 3) + p4 = Vector(-2, 0, 2) + self.assertLine(i.edges[0], p0, p1) + self.assertLine(i.edges[1], p1, p2) + self.assertLine(i.edges[2], p2, p3) + self.assertLine(i.edges[3], p3, p4) + self.assertIsNone(i.tail) + + # make sure adding another segment doesn't change the state + edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(-3, 0, 0))) + i = i.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P2) + self.assertEqual(len(i.edges), 5) + p5 = Vector(-3, 0, 1) + self.assertLine(i.edges[4], p4, p5) + self.assertIsNone(i.tail) + + # now if we complete to P3 .... + edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(-4, 0, 0))) + i = i.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P3) + self.assertTrue(i.isComplete()) + self.assertEqual(len(i.edges), 6) + p6 = Vector(-4, 0, 0) + self.assertLine(i.edges[5], p5, p6) + self.assertIsNone(i.tail) + + # verify proper operation if there is a single edge going through all + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-4, 0, 0))) + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P3) + self.assertTrue(i.isComplete()) + self.assertEqual(len(i.edges), 4) + self.assertLine(i.edges[0], p0, p1) + self.assertLine(i.edges[1], p1, p2) + self.assertLine(i.edges[2], p2, p3) + self.assertLine(i.edges[3], p3, p6) + self.assertIsNone(i.tail) + + # verify tail is added as well + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-5, 0, 0))) + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P3) + self.assertTrue(i.isComplete()) + self.assertEqual(len(i.edges), 4) + self.assertLine(i.edges[0], p0, p1) + self.assertLine(i.edges[1], p1, p2) + self.assertLine(i.edges[2], p2, p3) + self.assertLine(i.edges[3], p3, p6) + self.assertIsNotNone(i.tail) + self.assertLine(i.tail, p6, edge.Curve.EndPoint) + +class TestTag02SquareTag(TagTestCaseBase): # ============= """Unit tests for square tags.""" - def testTagNoIntersect(self): - #"""Check that the returned tail if no intersection occurs matches the input.""" + def test00(self): + """Verify no intersection.""" tag = Tag( 0, 0, 4, 7, 90, True, 0) - pt1 = Vector(+5, 3, 0) - pt2 = Vector(-5, 3, 0) + pt1 = Vector(+5, 5, 0) + pt2 = Vector(-5, 5, 0) edge = Part.Edge(Part.Line(pt1, pt2)) i = tag.intersect(edge) @@ -191,8 +455,8 @@ class SquareTagTestCases(TagTestCaseBase): # ============= self.assertFalse(i.edges) self.assertLine(i.tail, pt1, pt2) - def testTagIntersectLine(self): - #"""Test that a straight line passing through a cylindrical tag is split up into 5 segments.""" + def test01(self): + """Verify a straight line passing through tag is split up into 5 segments.""" tag = Tag( 0, 0, 4, 7, 90, True, 0) pt1 = Vector(+5, 0, 0) pt2 = Vector(-5, 0, 0) @@ -215,8 +479,8 @@ class SquareTagTestCases(TagTestCaseBase): # ============= self.assertLine(i.tail, pt0d, pt2) - def testTagIntersectPartialLineP0(self): - #"""Make sure line is accounted for if it reaches P0.""" + def test02(self): + """Verify line is accounted for if it reaches P0.""" tag = Tag( 0, 0, 4, 7, 90, True, 0) edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(2, 0, 0))) @@ -228,8 +492,8 @@ class SquareTagTestCases(TagTestCaseBase): # ============= self.assertIsNone(i.tail) - def testTagIntersectPartialLineP1(self): - #"""Make sure line is accounted for if it reaches beyond P1.""" + def test03(self): + """Verify line is accounted for if it reaches beyond P1.""" tag = Tag( 0, 0, 4, 7, 90, True, 0) edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(1, 0, 0))) @@ -247,8 +511,8 @@ class SquareTagTestCases(TagTestCaseBase): # ============= self.assertIsNone(i.tail) - def testTagIntersectPartialLineP2(self): - #"""Make sure line is accounted for if it reaches beyond P2.""" + def test04(self): + """Verify line is accounted for if it reaches beyond P2.""" tag = Tag( 0, 0, 4, 7, 90, True, 0) edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-1, 0, 0))) @@ -265,8 +529,8 @@ class SquareTagTestCases(TagTestCaseBase): # ============= self.assertLine(i.edges[2], pt0b, pt1a) self.assertIsNone(i.tail) - def testTagIntersectPartialLineP11(self): - #"""Make sure a line is accounted for if it lies entirely between P1 and P2.""" + def test05(self): + """Verify line is accounted for if it lies entirely between P1 and P2.""" tag = Tag( 0, 0, 4, 7, 90, True, 0) e1 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+1, 0, 0))) @@ -288,8 +552,8 @@ class SquareTagTestCases(TagTestCaseBase): # ============= self.assertLine(i.edges[3], pt1a, pt1b) self.assertIsNone(i.tail) - def testTagIntersectPartialLinesP11223(self): - #"""Verify all lines between P0 and P3 are added.""" + def test06(self): + """Verify all lines between P0 and P3 are added.""" tag = Tag( 0, 0, 4, 7, 90, True, 0) e0 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+2, 0, 0))) e1 = Part.Edge(Part.Line(e0.Curve.EndPoint, Vector(+1, 0, 0))) @@ -329,14 +593,21 @@ class SquareTagTestCases(TagTestCaseBase): # ============= self.assertIsNotNone(i.tail) self.assertLine(i.tail, e6.Curve.StartPoint, e6.Curve.EndPoint) - def testTagIntersectLineAt(self): + def test07(self): + """Verify intersection of different z levels.""" tag = Tag( 0, 0, 4, 7, 90, True, 0) # for all lines below 7 we get the trapezoid for i in range(0, 7): - edge = Part.Edge(Part.Line(Vector(5, 0, i), Vector(-5, 0, i))) + p0 = Vector(5, 0, i) + p1 = Vector(2, 0, i) + p2 = Vector(2, 0, 7) + p3 = Vector(-2, 0, 7) + p4 = Vector(-2, 0, i) + p5 = Vector(-5, 0, i) + edge = Part.Edge(Part.Line(p0, p5)) s = tag.intersect(edge) self.assertTrue(s.isComplete()) - self.assertTrapezoid(s.edges, s.tail, [edge.Curve.StartPoint, 2, 7, edge.Curve.EndPoint]) + self.assertTrapezoid(s.edges, s.tail, [p0, p1, p2, p3, p4, p5]) # for all edges at height or above the original line is used for i in range(7, 9): @@ -345,3 +616,182 @@ class SquareTagTestCases(TagTestCaseBase): # ============= self.assertTrue(s.isComplete()) self.assertLine(s.tail, edge.Curve.StartPoint, edge.Curve.EndPoint) +class TestTag03AngledTag(TagTestCaseBase): # ============= + """Unit tests for trapezoid tags.""" + + def test00(self): + """Verify no intersection.""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + pt1 = Vector(+5, 5, 0) + pt2 = Vector(-5, 5, 0) + edge = Part.Edge(Part.Line(pt1, pt2)) + + i = tag.intersect(edge) + self.assertIsNotNone(i) + self.assertTrue(i.isComplete()) + self.assertIsNotNone(i.edges) + self.assertFalse(i.edges) + self.assertLine(i.tail, pt1, pt2) + + def test01(self): + """Verify a straight line passing through tag is split into 5 segments.""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + pt1 = Vector(+5, 0, 0) + pt2 = Vector(-5, 0, 0) + edge = Part.Edge(Part.Line(pt1, pt2)) + + i = tag.intersect(edge) + self.assertIsNotNone(i) + self.assertTrue(i.isComplete()) + + pt0a = Vector(+4, 0, 0) + pt0b = Vector(+1, 0, 3) + pt0c = Vector(-1, 0, 3) + pt0d = Vector(-4, 0, 0) + + self.assertEqual(len(i.edges), 4) + self.assertLine(i.edges[0], pt1, pt0a) + self.assertLine(i.edges[1], pt0a, pt0b) + self.assertLine(i.edges[2], pt0b, pt0c) + self.assertLine(i.edges[3], pt0c, pt0d) + self.assertLine(i.tail, pt0d, pt2) + + + def test02(self): + """Verify line is accounted for if it reaches P0.""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(4, 0, 0))) + + i = tag.intersect(edge) + self.assertFalse(i.isComplete()) + + self.assertEqual(len(i.edges), 1) + self.assertLine(i.edges[0], edge.Curve.StartPoint, edge.Curve.EndPoint) + self.assertIsNone(i.tail) + + + def test03(self): + """Verify line is accounted for if it reaches beyond P1.""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(0.5, 0, 0))) + + i = tag.intersect(edge) + self.assertFalse(i.isComplete()) + + pt0a = Vector(+4, 0, 0) + pt0b = Vector(+1, 0, 3) + pt1a = Vector(+0.5, 0, 3) + + self.assertEqual(len(i.edges), 3) + self.assertLine(i.edges[0], edge.Curve.StartPoint, pt0a) + self.assertLine(i.edges[1], pt0a, pt0b) + self.assertLine(i.edges[2], pt0b, pt1a) + self.assertIsNone(i.tail) + + + def test04(self): + """Verify line is accounted for if it reaches beyond P2.""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-1, 0, 0))) + + i = tag.intersect(edge) + self.assertFalse(i.isComplete()) + + pt0a = Vector(+4, 0, 0) + pt0b = Vector(+1, 0, 3) + pt1a = Vector(-1, 0, 3) + + self.assertEqual(len(i.edges), 3) + self.assertLine(i.edges[0], edge.Curve.StartPoint, pt0a) + self.assertLine(i.edges[1], pt0a, pt0b) + self.assertLine(i.edges[2], pt0b, pt1a) + self.assertIsNone(i.tail) + + def test05(self): + """Verify a line is accounted for if it lies entirely between P1 and P2.""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + e1 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+0.5, 0, 0))) + + i = tag.intersect(e1) + self.assertFalse(i.isComplete()) + + e2 = Part.Edge(Part.Line(e1.Curve.EndPoint, Vector(0,0,0))) + i = i.intersect(e2) + + pt0a = Vector(+4, 0, 0) + pt0b = Vector(+1, 0, 3) + pt1a = Vector(+0.5, 0, 3) + pt1b = Vector( 0, 0, 3) + + self.assertEqual(len(i.edges), 4) + self.assertLine(i.edges[0], e1.Curve.StartPoint, pt0a) + self.assertLine(i.edges[1], pt0a, pt0b) + self.assertLine(i.edges[2], pt0b, pt1a) + self.assertLine(i.edges[3], pt1a, pt1b) + self.assertIsNone(i.tail) + + def test06(self): + """Verify all lines between P0 and P3 are added.""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + e0 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+4, 0, 0))) + e1 = Part.Edge(Part.Line(e0.Curve.EndPoint, Vector(+2, 0, 0))) + e2 = Part.Edge(Part.Line(e1.Curve.EndPoint, Vector(+0.5, 0, 0))) + e3 = Part.Edge(Part.Line(e2.Curve.EndPoint, Vector(-0.5, 0, 0))) + e4 = Part.Edge(Part.Line(e3.Curve.EndPoint, Vector(-1, 0, 0))) + e5 = Part.Edge(Part.Line(e4.Curve.EndPoint, Vector(-2, 0, 0))) + e6 = Part.Edge(Part.Line(e5.Curve.EndPoint, Vector(-5, 0, 0))) + + i = tag + for e in [e0, e1, e2, e3, e4, e5]: + i = i.intersect(e) + self.assertFalse(i.isComplete()) + i = i.intersect(e6) + self.assertTrue(i.isComplete()) + + pt0 = Vector(4, 0, 0) + pt1 = Vector(2, 0, 2) + pt2 = Vector(1, 0, 3) + pt3 = Vector(0.5, 0, 3) + pt4 = Vector(-0.5, 0, 3) + pt5 = Vector(-1, 0, 3) + pt6 = Vector(-2, 0, 2) + pt7 = Vector(-4, 0, 0) + + #self.assertEqual(len(i.edges), 8) + + self.assertLine(i.edges[0], e0.Curve.StartPoint, pt0) + self.assertLine(i.edges[1], pt0, pt1) + self.assertLine(i.edges[2], pt1, pt2) + self.assertLine(i.edges[3], pt2, pt3) + self.assertLine(i.edges[4], pt3, pt4) + self.assertLine(i.edges[5], pt4, pt5) + self.assertLine(i.edges[6], pt5, pt6) + self.assertLine(i.edges[7], pt6, pt7) + self.assertTrue(i.isComplete()) + + self.assertIsNotNone(i.tail) + self.assertLine(i.tail, pt7, e6.Curve.EndPoint) + + def test07(self): + """Verify intersection for different z levels.""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + # for all lines below 3 we get the trapezoid + for i in range(0, 3): + p0 = Vector(5, 0, i) + p1 = Vector(4-i, 0, i) + p2 = Vector(1, 0, 3) + p3 = Vector(-1, 0, 3) + p4 = Vector(-4+i, 0, i) + p5 = Vector(-5, 0, i) + edge = Part.Edge(Part.Line(p0, p5)) + s = tag.intersect(edge) + self.assertTrue(s.isComplete()) + self.assertTrapezoid(s.edges, s.tail, [p0, p1, p2, p3, p4, p5]) + + # for all edges at height or above the original line is used + for i in range(3, 5): + edge = Part.Edge(Part.Line(Vector(5, 0, i), Vector(-5, 0, i))) + s = tag.intersect(edge) + self.assertTrue(s.isComplete()) + self.assertLine(s.tail, edge.Curve.StartPoint, edge.Curve.EndPoint) + diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index c719142d1..0d3a7818b 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -25,5 +25,9 @@ import TestApp from PathTests.TestPathPost import PathPostTestCases -from PathTests.TestPathDressupHoldingTags import TagTestCases -from PathTests.TestPathDressupHoldingTags import SquareTagTestCases + +from PathTests.TestPathDressupHoldingTags import TestTag00BasicHolding +from PathTests.TestPathDressupHoldingTags import TestTag01BasicTag +from PathTests.TestPathDressupHoldingTags import TestTag02SquareTag +from PathTests.TestPathDressupHoldingTags import TestTag03AngledTag + From 3ce5ea6b35392347b2a98c3929e9d88293220ae9 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 21 Nov 2016 21:53:35 -0800 Subject: [PATCH 13/17] Tests and fixes for triangular shaped tags. --- .../PathScripts/PathDressupHoldingTags.py | 32 +- .../PathTests/TestPathDressupHoldingTags.py | 570 +++++++----------- src/Mod/Path/TestPathApp.py | 3 +- 3 files changed, 239 insertions(+), 366 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index cc204d7bc..2202b5b3a 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -149,6 +149,9 @@ class Tag: def top(self): return self.z + self.actualHeight + def centerLine(self): + return Part.Line(self.originAt(self.bottom()), self.originAt(self.top())) + def createSolidsAt(self, z): self.z = z r1 = self.width / 2 @@ -210,30 +213,57 @@ class Tag: #print("\nplateau= %s - %s" %(pt1, pt2)) return Part.Edge(Part.Line(pt1, pt2)) - def intersectP0(self, edge): + def intersectP0Core(self, edge): #print("----- P0 (%s - %s)" % (edge.Curve.StartPoint, edge.Curve.EndPoint)) i = self.tag.nextIntersectionClosestTo(edge, self.tag.core, edge.Curve.StartPoint) if i: if pointsCoincide(i, edge.Curve.StartPoint): # if P0 and P1 are the same, we need to insert a segment for the rise + #print("------- insert vertical rise (%s)" % i) self.edges.append(Part.Edge(Part.Line(i, FreeCAD.Vector(i.x, i.y, self.tag.top())))) self.p1 = i self.state = self.P1 return edge if pointsCoincide(i, edge.Curve.EndPoint): + #print("------- consumed (%s)" % i) e = edge tail = None else: + #print("------- split at (%s)" % i) e, tail = self.tag.splitEdgeAt(edge, i) self.p1 = e.Curve.EndPoint self.edges.append(self.tag.mapEdgeToSolid(e)) self.state = self.P1 return tail # no intersection, the entire edge fits between P0 and P1 + #print("------- no intersection") self.edges.append(self.tag.mapEdgeToSolid(edge)) return None + def intersectP0(self, edge): + if self.tag.core: + return self.intersectP0Core(edge) + # if we have no core the tip is the origin of the Tag + line = Part.Edge(self.tag.centerLine()) + i = DraftGeomUtils.findIntersection(line, edge) + if i: + if pointsCoincide(i[0], edge.Curve.EndPoint): + e = edge + tail = None + else: + e, tail = self.tag.splitEdgeAt(edge, i[0]) + self.state = self.P2 # P1 and P2 are identical for triangular tags + self.p1 = i[0] + self.p2 = i[0] + else: + e = edge + tail = None + self.edges.append(self.tag.mapEdgeToSolid(e)) + return tail + + + def intersectP1(self, edge): #print("----- P1 (%s - %s)" % (edge.Curve.StartPoint, edge.Curve.EndPoint)) i = self.tag.nextIntersectionClosestTo(edge, self.tag.core, edge.Curve.EndPoint) diff --git a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py index b4776f22e..d6adb06d0 100644 --- a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py +++ b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py @@ -94,18 +94,15 @@ class TagTestCaseBase(unittest.TestCase): if math.fabs(v1 - v2) > slack: self.fail("%f != %f" % (v1, v2)) - def assertTrapezoid(self, edgs, tail, points): + def assertLines(self, edgs, tail, points): """Check that there are 5 edges forming a trapezoid.""" edges = list(edgs) if tail: edges.append(tail) - self.assertEqual(len(edges), 5) + self.assertEqual(len(edges), len(points) - 1) - self.assertLine(edges[0], points[0], points[1]) - self.assertLine(edges[1], points[1], points[2]) - self.assertLine(edges[2], points[2], points[3]) - self.assertLine(edges[3], points[3], points[4]) - self.assertLine(edges[4], points[4], points[5]) + for i in range(0, len(edges)): + self.assertLine(edges[i], points[i], points[i+1]) class TestTag00BasicHolding(TagTestCaseBase): """Some basid test cases.""" @@ -157,7 +154,7 @@ class TestTag01BasicTag(TagTestCaseBase): # ============= self.assertCylinderAt(tag.core, Vector(100, 200, 17), 2, 5) def test02(self): - """Verify an angled tag has a cone shape with a lid, and cylinder core.""" + """Verify trapezoidal tag has a cone shape with a lid, and cylinder core.""" tag = Tag(0, 0, 18, 5, 45, True) tag.createSolidsAt(0) @@ -185,7 +182,24 @@ class TestTag01BasicTag(TagTestCaseBase): # ============= self.assertIsNone(tag.core) - def test10(self): +class TestTag02SquareTag(TagTestCaseBase): # ============= + """Unit tests for square tags.""" + + def test00(self): + """Verify no intersection.""" + tag = Tag( 0, 0, 4, 7, 90, True, 0) + pt1 = Vector(+5, 5, 0) + pt2 = Vector(-5, 5, 0) + edge = Part.Edge(Part.Line(pt1, pt2)) + + i = tag.intersect(edge) + self.assertIsNotNone(i) + self.assertTrue(i.isComplete()) + self.assertIsNotNone(i.edges) + self.assertFalse(i.edges) + self.assertLine(i.tail, pt1, pt2) + + def test01(self): """Verify intersection of square tag with line ending at tag start.""" tag = Tag( 0, 0, 8, 3, 90, True, 0) edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(4, 0, 0))) @@ -196,7 +210,7 @@ class TestTag01BasicTag(TagTestCaseBase): # ============= self.assertLine(i.edges[0], edge.Curve.StartPoint, edge.Curve.EndPoint) self.assertIsNone(i.tail) - def test11(self): + def test02(self): """Verify intersection of square tag with line ending between P1 and P2.""" tag = Tag( 0, 0, 8, 3, 90, True, 0) edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(1, 0, 0))) @@ -221,7 +235,7 @@ class TestTag01BasicTag(TagTestCaseBase): # ============= self.assertLine(i.edges[3], p3, p4) self.assertIsNone(i.tail) - def test12(self): + def test03(self): """Verify intesection of square tag with line ending on P2.""" tag = Tag( 0, 0, 8, 3, 90, True, 0) edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-4, 0, 0))) @@ -252,7 +266,7 @@ class TestTag01BasicTag(TagTestCaseBase): # ============= self.assertLine(i.edges[3], p2a, p3) self.assertIsNone(i.tail) - def test13(self): + def test04(self): """Verify plunge down is inserted for square tag on exit.""" tag = Tag( 0, 0, 8, 3, 90, True, 0) edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-5, 0, 0))) @@ -274,8 +288,78 @@ class TestTag01BasicTag(TagTestCaseBase): # ============= self.assertIsNotNone(i.tail) self.assertLine(i.tail, p4, p5) - def test20(self): - """Veify intersection of angled tag with line ending before P1.""" + def test05(self): + """Verify all lines between P0 and P3 are added.""" + tag = Tag( 0, 0, 4, 7, 90, True, 0) + e0 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+2, 0, 0))) + e1 = Part.Edge(Part.Line(e0.Curve.EndPoint, Vector(+1, 0, 0))) + e2 = Part.Edge(Part.Line(e1.Curve.EndPoint, Vector(+0.5, 0, 0))) + e3 = Part.Edge(Part.Line(e2.Curve.EndPoint, Vector(-0.5, 0, 0))) + e4 = Part.Edge(Part.Line(e3.Curve.EndPoint, Vector(-1, 0, 0))) + e5 = Part.Edge(Part.Line(e4.Curve.EndPoint, Vector(-2, 0, 0))) + e6 = Part.Edge(Part.Line(e5.Curve.EndPoint, Vector(-5, 0, 0))) + + i = tag + for e in [e0, e1, e2, e3, e4, e5]: + i = i.intersect(e) + self.assertFalse(i.isComplete()) + i = i.intersect(e6) + self.assertTrue(i.isComplete()) + + pt0 = Vector(2, 0, 0) + pt1 = Vector(2, 0, 7) + pt2 = Vector(1, 0, 7) + pt3 = Vector(0.5, 0, 7) + pt4 = Vector(-0.5, 0, 7) + pt5 = Vector(-1, 0, 7) + pt6 = Vector(-2, 0, 7) + + self.assertEqual(len(i.edges), 8) + self.assertLines(i.edges, i.tail, [e0.Curve.StartPoint, pt0, pt1, pt2, pt3, pt4, pt5, pt6, e6.Curve.StartPoint, e6.Curve.EndPoint]) + self.assertIsNotNone(i.tail) + + def test06(self): + """Verify intersection of different z levels.""" + tag = Tag( 0, 0, 4, 7, 90, True, 0) + # for all lines below 7 we get the trapezoid + for i in range(0, 7): + p0 = Vector(5, 0, i) + p1 = Vector(2, 0, i) + p2 = Vector(2, 0, 7) + p3 = Vector(-2, 0, 7) + p4 = Vector(-2, 0, i) + p5 = Vector(-5, 0, i) + edge = Part.Edge(Part.Line(p0, p5)) + s = tag.intersect(edge) + self.assertTrue(s.isComplete()) + self.assertLines(s.edges, s.tail, [p0, p1, p2, p3, p4, p5]) + + # for all edges at height or above the original line is used + for i in range(7, 9): + edge = Part.Edge(Part.Line(Vector(5, 0, i), Vector(-5, 0, i))) + s = tag.intersect(edge) + self.assertTrue(s.isComplete()) + self.assertLine(s.tail, edge.Curve.StartPoint, edge.Curve.EndPoint) + +class TestTag03TrapezoidTag(TagTestCaseBase): # ============= + """Unit tests for trapezoid tags.""" + + def test00(self): + """Verify no intersection.""" + tag = Tag( 0, 0, 8, 3, 45, True, 0) + pt1 = Vector(+5, 5, 0) + pt2 = Vector(-5, 5, 0) + edge = Part.Edge(Part.Line(pt1, pt2)) + + i = tag.intersect(edge) + self.assertIsNotNone(i) + self.assertTrue(i.isComplete()) + self.assertIsNotNone(i.edges) + self.assertFalse(i.edges) + self.assertLine(i.tail, pt1, pt2) + + def test01(self): + """Veify intersection of trapezoid tag with line ending before P1.""" tag = Tag( 0, 0, 8, 3, 45, True, 0) edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(4, 0, 0))) @@ -309,8 +393,8 @@ class TestTag01BasicTag(TagTestCaseBase): # ============= self.assertLine(i.edges[2], p2, p3) self.assertIsNone(i.tail) - def test21(self): - """Verify intersection of angled tag with line ending between P1 and P2""" + def test02(self): + """Verify intersection of trapezoid tag with line ending between P1 and P2""" tag = Tag( 0, 0, 8, 3, 45, True, 0) edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(1, 0, 0))) @@ -332,21 +416,18 @@ class TestTag01BasicTag(TagTestCaseBase): # ============= self.assertLine(i.edges[2], p2, p3) self.assertIsNone(i.tail) - def test22(self): - """Verify intersection of angled tag with edge ending on P2.""" + def test03(self): + """Verify intersection of trapezoid tag with edge ending on P2.""" tag = Tag( 0, 0, 8, 3, 45, True, 0) edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-1, 0, 0))) i = tag.intersect(edge) self.assertEqual(i.state, Tag.Intersection.P2) - self.assertEqual(len(i.edges), 3) p0 = Vector(edge.Curve.StartPoint) p1 = Vector(4, 0, 0) p2 = Vector(1, 0, 3) p3 = Vector(-1, 0, 3) - self.assertLine(i.edges[0], p0, p1) - self.assertLine(i.edges[1], p1, p2) - self.assertLine(i.edges[2], p2, p3) + self.assertLines(i.edges, i.tail, [p0, p1, p2, p3]) self.assertIsNone(i.tail) # make sure we get the same result if there's another edge @@ -355,10 +436,7 @@ class TestTag01BasicTag(TagTestCaseBase): # ============= edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(-1, 0, 0))) i = i.intersect(edge) self.assertEqual(i.state, Tag.Intersection.P2) - self.assertEqual(len(i.edges), 3) - self.assertLine(i.edges[0], p0, p1) - self.assertLine(i.edges[1], p1, p2) - self.assertLine(i.edges[2], p2, p3) + self.assertLines(i.edges, i.tail, [p0, p1, p2, p3]) self.assertIsNone(i.tail) # and also if the last segment doesn't cross the entire plateau @@ -367,31 +445,23 @@ class TestTag01BasicTag(TagTestCaseBase): # ============= edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(-1, 0, 0))) i = i.intersect(edge) self.assertEqual(i.state, Tag.Intersection.P2) - self.assertEqual(len(i.edges), 4) p2a = Vector(0.5, 0, 3) - self.assertLine(i.edges[0], p0, p1) - self.assertLine(i.edges[1], p1, p2) - self.assertLine(i.edges[2], p2, p2a) - self.assertLine(i.edges[3], p2a, p3) + self.assertLines(i.edges, i.tail, [p0, p1, p2, p2a, p3]) self.assertIsNone(i.tail) - def test23(self): - """Verify proper down plunge on angled tag exit.""" + def test04(self): + """Verify proper down plunge on trapezoid tag exit.""" tag = Tag( 0, 0, 8, 3, 45, True, 0) edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-2, 0, 0))) i = tag.intersect(edge) self.assertEqual(i.state, Tag.Intersection.P2) - self.assertEqual(len(i.edges), 4) p0 = Vector(5, 0, 0) p1 = Vector(4, 0, 0) p2 = Vector(1, 0, 3) p3 = Vector(-1, 0, 3) p4 = Vector(-2, 0, 2) - self.assertLine(i.edges[0], p0, p1) - self.assertLine(i.edges[1], p1, p2) - self.assertLine(i.edges[2], p2, p3) - self.assertLine(i.edges[3], p3, p4) + self.assertLines(i.edges, i.tail, [p0, p1, p2, p3, p4]) self.assertIsNone(i.tail) # make sure adding another segment doesn't change the state @@ -418,11 +488,7 @@ class TestTag01BasicTag(TagTestCaseBase): # ============= i = tag.intersect(edge) self.assertEqual(i.state, Tag.Intersection.P3) self.assertTrue(i.isComplete()) - self.assertEqual(len(i.edges), 4) - self.assertLine(i.edges[0], p0, p1) - self.assertLine(i.edges[1], p1, p2) - self.assertLine(i.edges[2], p2, p3) - self.assertLine(i.edges[3], p3, p6) + self.assertLines(i.edges, i.tail, [p0, p1, p2, p3, p6]) self.assertIsNone(i.tail) # verify tail is added as well @@ -430,307 +496,10 @@ class TestTag01BasicTag(TagTestCaseBase): # ============= i = tag.intersect(edge) self.assertEqual(i.state, Tag.Intersection.P3) self.assertTrue(i.isComplete()) - self.assertEqual(len(i.edges), 4) - self.assertLine(i.edges[0], p0, p1) - self.assertLine(i.edges[1], p1, p2) - self.assertLine(i.edges[2], p2, p3) - self.assertLine(i.edges[3], p3, p6) + self.assertLines(i.edges, i.tail, [p0, p1, p2, p3, p6, edge.Curve.EndPoint]) self.assertIsNotNone(i.tail) - self.assertLine(i.tail, p6, edge.Curve.EndPoint) - -class TestTag02SquareTag(TagTestCaseBase): # ============= - """Unit tests for square tags.""" - - def test00(self): - """Verify no intersection.""" - tag = Tag( 0, 0, 4, 7, 90, True, 0) - pt1 = Vector(+5, 5, 0) - pt2 = Vector(-5, 5, 0) - edge = Part.Edge(Part.Line(pt1, pt2)) - - i = tag.intersect(edge) - self.assertIsNotNone(i) - self.assertTrue(i.isComplete()) - self.assertIsNotNone(i.edges) - self.assertFalse(i.edges) - self.assertLine(i.tail, pt1, pt2) - - def test01(self): - """Verify a straight line passing through tag is split up into 5 segments.""" - tag = Tag( 0, 0, 4, 7, 90, True, 0) - pt1 = Vector(+5, 0, 0) - pt2 = Vector(-5, 0, 0) - edge = Part.Edge(Part.Line(pt1, pt2)) - - i = tag.intersect(edge) - self.assertIsNotNone(i) - self.assertTrue(i.isComplete()) - - pt0a = Vector(+2, 0, 0) - pt0b = Vector(+2, 0, 7) - pt0c = Vector(-2, 0, 7) - pt0d = Vector(-2, 0, 0) - - self.assertEqual(len(i.edges), 4) - self.assertLine(i.edges[0], pt1, pt0a) - self.assertLine(i.edges[1], pt0a, pt0b) - self.assertLine(i.edges[2], pt0b, pt0c) - self.assertLine(i.edges[3], pt0c, pt0d) - self.assertLine(i.tail, pt0d, pt2) - - - def test02(self): - """Verify line is accounted for if it reaches P0.""" - tag = Tag( 0, 0, 4, 7, 90, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(2, 0, 0))) - - i = tag.intersect(edge) - self.assertFalse(i.isComplete()) - - self.assertEqual(len(i.edges), 1) - self.assertLine(i.edges[0], edge.Curve.StartPoint, edge.Curve.EndPoint) - self.assertIsNone(i.tail) - - - def test03(self): - """Verify line is accounted for if it reaches beyond P1.""" - tag = Tag( 0, 0, 4, 7, 90, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(1, 0, 0))) - - i = tag.intersect(edge) - self.assertFalse(i.isComplete()) - - pt0a = Vector(+2, 0, 0) - pt0b = Vector(+2, 0, 7) - pt1a = Vector(+1, 0, 7) - - self.assertEqual(len(i.edges), 3) - self.assertLine(i.edges[0], edge.Curve.StartPoint, pt0a) - self.assertLine(i.edges[1], pt0a, pt0b) - self.assertLine(i.edges[2], pt0b, pt1a) - self.assertIsNone(i.tail) - - - def test04(self): - """Verify line is accounted for if it reaches beyond P2.""" - tag = Tag( 0, 0, 4, 7, 90, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-1, 0, 0))) - - i = tag.intersect(edge) - self.assertFalse(i.isComplete()) - - pt0a = Vector(+2, 0, 0) - pt0b = Vector(+2, 0, 7) - pt1a = Vector(-1, 0, 7) - - self.assertEqual(len(i.edges), 3) - self.assertLine(i.edges[0], edge.Curve.StartPoint, pt0a) - self.assertLine(i.edges[1], pt0a, pt0b) - self.assertLine(i.edges[2], pt0b, pt1a) - self.assertIsNone(i.tail) def test05(self): - """Verify line is accounted for if it lies entirely between P1 and P2.""" - tag = Tag( 0, 0, 4, 7, 90, True, 0) - e1 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+1, 0, 0))) - - i = tag.intersect(e1) - self.assertFalse(i.isComplete()) - - e2 = Part.Edge(Part.Line(e1.Curve.EndPoint, Vector(0,0,0))) - i = i.intersect(e2) - - pt0a = Vector(+2, 0, 0) - pt0b = Vector(+2, 0, 7) - pt1a = Vector(+1, 0, 7) - pt1b = Vector( 0, 0, 7) - - self.assertEqual(len(i.edges), 4) - self.assertLine(i.edges[0], e1.Curve.StartPoint, pt0a) - self.assertLine(i.edges[1], pt0a, pt0b) - self.assertLine(i.edges[2], pt0b, pt1a) - self.assertLine(i.edges[3], pt1a, pt1b) - self.assertIsNone(i.tail) - - def test06(self): - """Verify all lines between P0 and P3 are added.""" - tag = Tag( 0, 0, 4, 7, 90, True, 0) - e0 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+2, 0, 0))) - e1 = Part.Edge(Part.Line(e0.Curve.EndPoint, Vector(+1, 0, 0))) - e2 = Part.Edge(Part.Line(e1.Curve.EndPoint, Vector(+0.5, 0, 0))) - e3 = Part.Edge(Part.Line(e2.Curve.EndPoint, Vector(-0.5, 0, 0))) - e4 = Part.Edge(Part.Line(e3.Curve.EndPoint, Vector(-1, 0, 0))) - e5 = Part.Edge(Part.Line(e4.Curve.EndPoint, Vector(-2, 0, 0))) - e6 = Part.Edge(Part.Line(e5.Curve.EndPoint, Vector(-5, 0, 0))) - - i = tag - for e in [e0, e1, e2, e3, e4, e5]: - i = i.intersect(e) - self.assertFalse(i.isComplete()) - i = i.intersect(e6) - self.assertTrue(i.isComplete()) - - pt0 = Vector(2, 0, 0) - pt1 = Vector(2, 0, 7) - pt2 = Vector(1, 0, 7) - pt3 = Vector(0.5, 0, 7) - pt4 = Vector(-0.5, 0, 7) - pt5 = Vector(-1, 0, 7) - pt6 = Vector(-2, 0, 7) - - self.assertEqual(len(i.edges), 8) - - self.assertLine(i.edges[0], e0.Curve.StartPoint, pt0) - self.assertLine(i.edges[1], pt0, pt1) - self.assertLine(i.edges[2], pt1, pt2) - self.assertLine(i.edges[3], pt2, pt3) - self.assertLine(i.edges[4], pt3, pt4) - self.assertLine(i.edges[5], pt4, pt5) - self.assertLine(i.edges[6], pt5, pt6) - self.assertLine(i.edges[7], pt6, e5.Curve.EndPoint) - self.assertTrue(i.isComplete()) - - self.assertIsNotNone(i.tail) - self.assertLine(i.tail, e6.Curve.StartPoint, e6.Curve.EndPoint) - - def test07(self): - """Verify intersection of different z levels.""" - tag = Tag( 0, 0, 4, 7, 90, True, 0) - # for all lines below 7 we get the trapezoid - for i in range(0, 7): - p0 = Vector(5, 0, i) - p1 = Vector(2, 0, i) - p2 = Vector(2, 0, 7) - p3 = Vector(-2, 0, 7) - p4 = Vector(-2, 0, i) - p5 = Vector(-5, 0, i) - edge = Part.Edge(Part.Line(p0, p5)) - s = tag.intersect(edge) - self.assertTrue(s.isComplete()) - self.assertTrapezoid(s.edges, s.tail, [p0, p1, p2, p3, p4, p5]) - - # for all edges at height or above the original line is used - for i in range(7, 9): - edge = Part.Edge(Part.Line(Vector(5, 0, i), Vector(-5, 0, i))) - s = tag.intersect(edge) - self.assertTrue(s.isComplete()) - self.assertLine(s.tail, edge.Curve.StartPoint, edge.Curve.EndPoint) - -class TestTag03AngledTag(TagTestCaseBase): # ============= - """Unit tests for trapezoid tags.""" - - def test00(self): - """Verify no intersection.""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - pt1 = Vector(+5, 5, 0) - pt2 = Vector(-5, 5, 0) - edge = Part.Edge(Part.Line(pt1, pt2)) - - i = tag.intersect(edge) - self.assertIsNotNone(i) - self.assertTrue(i.isComplete()) - self.assertIsNotNone(i.edges) - self.assertFalse(i.edges) - self.assertLine(i.tail, pt1, pt2) - - def test01(self): - """Verify a straight line passing through tag is split into 5 segments.""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - pt1 = Vector(+5, 0, 0) - pt2 = Vector(-5, 0, 0) - edge = Part.Edge(Part.Line(pt1, pt2)) - - i = tag.intersect(edge) - self.assertIsNotNone(i) - self.assertTrue(i.isComplete()) - - pt0a = Vector(+4, 0, 0) - pt0b = Vector(+1, 0, 3) - pt0c = Vector(-1, 0, 3) - pt0d = Vector(-4, 0, 0) - - self.assertEqual(len(i.edges), 4) - self.assertLine(i.edges[0], pt1, pt0a) - self.assertLine(i.edges[1], pt0a, pt0b) - self.assertLine(i.edges[2], pt0b, pt0c) - self.assertLine(i.edges[3], pt0c, pt0d) - self.assertLine(i.tail, pt0d, pt2) - - - def test02(self): - """Verify line is accounted for if it reaches P0.""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(4, 0, 0))) - - i = tag.intersect(edge) - self.assertFalse(i.isComplete()) - - self.assertEqual(len(i.edges), 1) - self.assertLine(i.edges[0], edge.Curve.StartPoint, edge.Curve.EndPoint) - self.assertIsNone(i.tail) - - - def test03(self): - """Verify line is accounted for if it reaches beyond P1.""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(0.5, 0, 0))) - - i = tag.intersect(edge) - self.assertFalse(i.isComplete()) - - pt0a = Vector(+4, 0, 0) - pt0b = Vector(+1, 0, 3) - pt1a = Vector(+0.5, 0, 3) - - self.assertEqual(len(i.edges), 3) - self.assertLine(i.edges[0], edge.Curve.StartPoint, pt0a) - self.assertLine(i.edges[1], pt0a, pt0b) - self.assertLine(i.edges[2], pt0b, pt1a) - self.assertIsNone(i.tail) - - - def test04(self): - """Verify line is accounted for if it reaches beyond P2.""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-1, 0, 0))) - - i = tag.intersect(edge) - self.assertFalse(i.isComplete()) - - pt0a = Vector(+4, 0, 0) - pt0b = Vector(+1, 0, 3) - pt1a = Vector(-1, 0, 3) - - self.assertEqual(len(i.edges), 3) - self.assertLine(i.edges[0], edge.Curve.StartPoint, pt0a) - self.assertLine(i.edges[1], pt0a, pt0b) - self.assertLine(i.edges[2], pt0b, pt1a) - self.assertIsNone(i.tail) - - def test05(self): - """Verify a line is accounted for if it lies entirely between P1 and P2.""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - e1 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+0.5, 0, 0))) - - i = tag.intersect(e1) - self.assertFalse(i.isComplete()) - - e2 = Part.Edge(Part.Line(e1.Curve.EndPoint, Vector(0,0,0))) - i = i.intersect(e2) - - pt0a = Vector(+4, 0, 0) - pt0b = Vector(+1, 0, 3) - pt1a = Vector(+0.5, 0, 3) - pt1b = Vector( 0, 0, 3) - - self.assertEqual(len(i.edges), 4) - self.assertLine(i.edges[0], e1.Curve.StartPoint, pt0a) - self.assertLine(i.edges[1], pt0a, pt0b) - self.assertLine(i.edges[2], pt0b, pt1a) - self.assertLine(i.edges[3], pt1a, pt1b) - self.assertIsNone(i.tail) - - def test06(self): """Verify all lines between P0 and P3 are added.""" tag = Tag( 0, 0, 8, 3, 45, True, 0) e0 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+4, 0, 0))) @@ -748,31 +517,19 @@ class TestTag03AngledTag(TagTestCaseBase): # ============= i = i.intersect(e6) self.assertTrue(i.isComplete()) - pt0 = Vector(4, 0, 0) - pt1 = Vector(2, 0, 2) - pt2 = Vector(1, 0, 3) - pt3 = Vector(0.5, 0, 3) - pt4 = Vector(-0.5, 0, 3) - pt5 = Vector(-1, 0, 3) - pt6 = Vector(-2, 0, 2) - pt7 = Vector(-4, 0, 0) - - #self.assertEqual(len(i.edges), 8) - - self.assertLine(i.edges[0], e0.Curve.StartPoint, pt0) - self.assertLine(i.edges[1], pt0, pt1) - self.assertLine(i.edges[2], pt1, pt2) - self.assertLine(i.edges[3], pt2, pt3) - self.assertLine(i.edges[4], pt3, pt4) - self.assertLine(i.edges[5], pt4, pt5) - self.assertLine(i.edges[6], pt5, pt6) - self.assertLine(i.edges[7], pt6, pt7) - self.assertTrue(i.isComplete()) + p0 = Vector(4, 0, 0) + p1 = Vector(2, 0, 2) + p2 = Vector(1, 0, 3) + p3 = Vector(0.5, 0, 3) + p4 = Vector(-0.5, 0, 3) + p5 = Vector(-1, 0, 3) + p6 = Vector(-2, 0, 2) + p7 = Vector(-4, 0, 0) + self.assertLines(i.edges, i.tail, [e0.Curve.StartPoint, p0, p1, p2, p3, p4, p5, p6, p7, e6.Curve.EndPoint]) self.assertIsNotNone(i.tail) - self.assertLine(i.tail, pt7, e6.Curve.EndPoint) - def test07(self): + def test06(self): """Verify intersection for different z levels.""" tag = Tag( 0, 0, 8, 3, 45, True, 0) # for all lines below 3 we get the trapezoid @@ -786,7 +543,7 @@ class TestTag03AngledTag(TagTestCaseBase): # ============= edge = Part.Edge(Part.Line(p0, p5)) s = tag.intersect(edge) self.assertTrue(s.isComplete()) - self.assertTrapezoid(s.edges, s.tail, [p0, p1, p2, p3, p4, p5]) + self.assertLines(s.edges, s.tail, [p0, p1, p2, p3, p4, p5]) # for all edges at height or above the original line is used for i in range(3, 5): @@ -795,3 +552,88 @@ class TestTag03AngledTag(TagTestCaseBase): # ============= self.assertTrue(s.isComplete()) self.assertLine(s.tail, edge.Curve.StartPoint, edge.Curve.EndPoint) + +class TestTag04TriangularTag(TagTestCaseBase): # ======================== + """Unit tests for tags that take on a triangular shape.""" + + def test00(self): + """Verify intersection of triangular tag with line ending at tag start.""" + tag = Tag( 0, 0, 8, 7, 45, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(4, 0, 0))) + + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P0) + self.assertEqual(len(i.edges), 1) + self.assertLine(i.edges[0], edge.Curve.StartPoint, edge.Curve.EndPoint) + self.assertIsNone(i.tail) + + def test01(self): + """Verify intersection of triangular tag with line ending between P0 and P1.""" + tag = Tag( 0, 0, 8, 7, 45, True, 0) + edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(3, 0, 0))) + + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P0) + p1 = Vector(4, 0, 0) + p2 = Vector(3, 0, 1) + self.assertLines(i.edges, i.tail, [edge.Curve.StartPoint, p1, p2]) + self.assertIsNone(i.tail) + + # verify we stay in P1 if we add another segment + edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(1, 0, 0))) + i = i.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P0) + self.assertEqual(len(i.edges), 3) + p3 = Vector(1, 0, 3) + self.assertLine(i.edges[2], p2, p3) + self.assertIsNone(i.tail) + + def test02(self): + """Verify proper down plunge on exit of triangular tag.""" + tag = Tag( 0, 0, 8, 7, 45, True, 0) + + p0 = Vector(5, 0, 0) + p1 = Vector(4, 0, 0) + p2 = Vector(0, 0, 4) + edge = Part.Edge(Part.Line(p0, FreeCAD.Vector(0,0,0))) + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P2) + self.assertEqual(len(i.edges), 2) + self.assertLines(i.edges, i.tail, [p0, p1, p2]) + + # adding another segment doesn't make a difference + edge = Part.Edge(Part.Line(edge.Curve.EndPoint, FreeCAD.Vector(-3,0,0))) + i = i.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P2) + self.assertEqual(len(i.edges), 3) + p3 = Vector(-3, 0, 1) + self.assertLines(i.edges, i.tail, [p0, p1, p2, p3]) + + # same result if all is one line + edge = Part.Edge(Part.Line(p0, edge.Curve.EndPoint)) + i = tag.intersect(edge) + self.assertEqual(i.state, Tag.Intersection.P2) + self.assertLines(i.edges, i.tail, [p0, p1, p2, p3]) + + def test03(self): + """Verify triangular tag shap on intersection.""" + tag = Tag( 0, 0, 8, 7, 45, True, 0) + + p0 = Vector(5, 0, 0) + p1 = Vector(4, 0, 0) + p2 = Vector(0, 0, 4) + p3 = Vector(-4, 0, 0) + edge = Part.Edge(Part.Line(p0, p3)) + i = tag.intersect(edge) + self.assertTrue(i.isComplete()) + self.assertLines(i.edges, i.tail, [p0, p1, p2, p3]) + self.assertIsNone(i.tail) + + # this should also work if there is some excess, aka tail + p4 = Vector(-5, 0, 0) + edge = Part.Edge(Part.Line(p0, p4)) + i = tag.intersect(edge) + self.assertTrue(i.isComplete()) + self.assertLines(i.edges, i.tail, [p0, p1, p2, p3, p4]) + self.assertIsNotNone(i.tail) + diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 0d3a7818b..53da7186f 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -29,5 +29,6 @@ from PathTests.TestPathPost import PathPostTestCases from PathTests.TestPathDressupHoldingTags import TestTag00BasicHolding from PathTests.TestPathDressupHoldingTags import TestTag01BasicTag from PathTests.TestPathDressupHoldingTags import TestTag02SquareTag -from PathTests.TestPathDressupHoldingTags import TestTag03AngledTag +from PathTests.TestPathDressupHoldingTags import TestTag03TrapezoidTag +from PathTests.TestPathDressupHoldingTags import TestTag04TriangularTag From fcc187880e03bd363050fdae97b11ac3d1517619 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Nov 2016 15:10:25 -0800 Subject: [PATCH 14/17] Enhance makeHelix to also support helixes that spiral downwards. --- src/Mod/Part/App/TopoShape.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Mod/Part/App/TopoShape.cpp b/src/Mod/Part/App/TopoShape.cpp index b67187db3..d8d053175 100644 --- a/src/Mod/Part/App/TopoShape.cpp +++ b/src/Mod/Part/App/TopoShape.cpp @@ -1659,12 +1659,15 @@ TopoDS_Shape TopoShape::makeHelix(Standard_Real pitch, Standard_Real height, Standard_Boolean leftHanded, Standard_Boolean newStyle) const { - if (pitch < Precision::Confusion()) + if (fabs(pitch) < Precision::Confusion()) Standard_Failure::Raise("Pitch of helix too small"); - if (height < Precision::Confusion()) + if (fabs(height) < Precision::Confusion()) Standard_Failure::Raise("Height of helix too small"); + if ((height > 0 && pitch < 0) || (height < 0 && pitch > 0)) + Standard_Failure::Raise("Pitch and height of helix not compatible"); + gp_Ax2 cylAx2(gp_Pnt(0.0,0.0,0.0) , gp::DZ()); Handle_Geom_Surface surf; if (angle < Precision::Confusion()) { From fbc75d979703413c68866be0fae2b7ae288bb800 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Nov 2016 15:11:24 -0800 Subject: [PATCH 15/17] Split out PathGeom and created test cases for it. --- src/Mod/Path/CMakeLists.txt | 99 +++++------ .../PathScripts/PathDressupHoldingTags.py | 2 +- src/Mod/Path/PathScripts/PathGeom.py | 159 ++++++++++++++++++ src/Mod/Path/PathTests/PathTestUtils.py | 75 +++++++++ src/Mod/Path/PathTests/TestPathGeom.py | 123 ++++++++++++++ src/Mod/Path/TestPathApp.py | 1 + 6 files changed, 409 insertions(+), 50 deletions(-) create mode 100644 src/Mod/Path/PathScripts/PathGeom.py create mode 100644 src/Mod/Path/PathTests/PathTestUtils.py create mode 100644 src/Mod/Path/PathTests/TestPathGeom.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index ceeba426f..e92a43661 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -16,67 +16,68 @@ INSTALL( SET(PathScripts_SRCS PathCommands.py - PathScripts/__init__.py - PathScripts/PostUtils.py - PathScripts/example_pre.py - PathScripts/opensbp_pre.py - PathScripts/opensbp_post.py - PathScripts/example_post.py - PathScripts/linuxcnc_post.py - PathScripts/centroid_post.py - PathScripts/comparams_post.py - PathScripts/dynapath_post.py - PathScripts/generic_post.py - PathScripts/dumper_post.py - PathScripts/rml_post.py - PathScripts/TooltableEditor.py - PathScripts/PathProfile.py - PathScripts/PathProfileEdges.py - PathScripts/PathContour.py - PathScripts/PathMillFace.py - PathScripts/PathPocket.py - PathScripts/PathDrilling.py - PathScripts/PathDressup.py + PathScripts/DogboneDressup.py PathScripts/DragknifeDressup.py - PathScripts/PathDressupHoldingTags.py - PathScripts/PathHop.py - PathScripts/PathUtils.py - PathScripts/PathSelection.py - PathScripts/PathFixture.py - PathScripts/PathCopy.py + PathScripts/PathAreaUtils.py + PathScripts/PathArray.py + PathScripts/PathComment.py PathScripts/PathCompoundExtended.py + PathScripts/PathContour.py + PathScripts/PathCopy.py + PathScripts/PathCustom.py + PathScripts/PathDressup.py + PathScripts/PathDressupHoldingTags.py + PathScripts/PathDrilling.py + PathScripts/PathEngrave.py + PathScripts/PathFacePocket.py + PathScripts/PathFaceProfile.py + PathScripts/PathFixture.py + PathScripts/PathFromShape.py + PathScripts/PathGeom.py + PathScripts/PathHop.py + PathScripts/PathInspect.py PathScripts/PathJob.py - PathScripts/PathStock.py + PathScripts/PathKurveUtils.py + PathScripts/PathLoadTool.py + PathScripts/PathMillFace.py PathScripts/PathPlane.py PathScripts/PathPocket.py PathScripts/PathPost.py PathScripts/PathPostProcessor.py - PathScripts/PathLoadTool.py - PathScripts/PathToolLenOffset.py - PathScripts/PathComment.py - PathScripts/PathStop.py - PathScripts/PathFromShape.py - PathScripts/PathKurveUtils.py - PathScripts/PathAreaUtils.py - PathScripts/slic3r_pre.py - PathScripts/PathFaceProfile.py - PathScripts/PathFacePocket.py - PathScripts/PathArray.py - PathScripts/PathCustom.py - PathScripts/PathInspect.py - PathScripts/PathSimpleCopy.py - PathScripts/PathEngrave.py - PathScripts/PathSurface.py + PathScripts/PathPreferences.py + PathScripts/PathPreferencesPathJob.py + PathScripts/PathProfile.py + PathScripts/PathProfileEdges.py PathScripts/PathRemote.py PathScripts/PathSanity.py + PathScripts/PathSelection.py + PathScripts/PathSimpleCopy.py + PathScripts/PathStock.py + PathScripts/PathStop.py + PathScripts/PathSurface.py + PathScripts/PathToolLenOffset.py PathScripts/PathToolLibraryManager.py - PathScripts/DogboneDressup.py - PathScripts/PathPreferencesPathJob.py - PathScripts/PathPreferences.py + PathScripts/PathUtils.py + PathScripts/PostUtils.py + PathScripts/TooltableEditor.py + PathScripts/__init__.py + PathScripts/centroid_post.py + PathScripts/comparams_post.py + PathScripts/dumper_post.py + PathScripts/dynapath_post.py + PathScripts/example_post.py + PathScripts/example_pre.py + PathScripts/generic_post.py + PathScripts/linuxcnc_post.py + PathScripts/opensbp_post.py + PathScripts/opensbp_pre.py + PathScripts/rml_post.py + PathScripts/slic3r_pre.py + PathTests/TestPathDressupHoldingTags.py + PathTests/TestPathGeom.py + PathTests/TestPathPost.py PathTests/__init__.py PathTests/test_linuxcnc_00.ngc - PathTests/TestPathDressupHoldingTags.py - PathTests/TestPathPost.py TestPathApp.py ) diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index 2202b5b3a..b88ae712d 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -2,7 +2,7 @@ # *************************************************************************** # * * -# * Copyright (c) 2014 Yorik van Havre * +# * Copyright (c) 2016 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) * diff --git a/src/Mod/Path/PathScripts/PathGeom.py b/src/Mod/Path/PathScripts/PathGeom.py new file mode 100644 index 000000000..e4d176717 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathGeom.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2016 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 math +import Part +import Path + +from FreeCAD import Vector + +class Side: + """Class to determine and define the side a Path is on or Vectors are in relation to each other.""" + Left = +1 + """Representing the left side.""" + Right = -1 + """Representing the right side.""" + Straight = 0 + """Used if two vectors form a line.""" + On = 0 + """Synonym for Straight.""" + + @classmethod + def toString(cls, side): + """Returns a string representation of the enum value.""" + if side == cls.Left: + return 'Left' + if side == cls.Right: + return 'Right' + return 'On' + + @classmethod + def of(cls, ptRef, pt): + """Determine the side of pt in relation to ptRef. + If both Points are viewed as vectors with their origin in (0,0,0) + then the two vectors are either form a straigt line (On) or pt + lies in the left or right hemishpere in regards to ptRef.""" + d = -ptRef.x*pt.y + ptRef.y*pt.x + if d < 0: + return cls.Left + if d > 0: + return cls.Right + return cls.Straight + +class PathGeom: + """Class to transform Path Commands into Edges and Wire and back again. + The interface might eventuallly become part of Path itself.""" + CmdMoveStraight = ['G1', 'G01'] + CmdMoveCW = ['G2', 'G02'] + CmdMoveCCW = ['G3', 'G03'] + CmdMoveArc = CmdMoveCW + CmdMoveCCW + CmdMove = CmdMoveStraight + CmdMoveArc + + @classmethod + def getAngle(cls, vertex): + """Returns the angle [-pi,pi] of a vertex using the X-axis as the reference. + Positive angles for vertexes in the upper hemishpere (positive y values) + and negative angles for the lower hemishpere.""" + a = vertex.getAngle(FreeCAD.Vector(1,0,0)) + if vertex.y < 0: + return -a + return a + + @classmethod + def diffAngle(cls, a1, a2, direction = 'CW'): + """Returns the difference between two angles (a1 -> a2) into a given direction.""" + if direction == 'CW': + while a1 < a2: + a1 += 2*math.pi + a = a1 - a2 + else: + while a2 < a1: + a2 += 2*math.pi + a = a2 - a1 + return a + + @classmethod + def commandEndPoint(cls, cmd, defaultPoint = Vector(), X='X', Y='Y', Z='Z'): + """Extracts the end point from a Path Command.""" + x = cmd.Parameters.get(X, defaultPoint.x) + y = cmd.Parameters.get(Y, defaultPoint.y) + z = cmd.Parameters.get(Z, defaultPoint.z) + return FreeCAD.Vector(x, y, z) + + @classmethod + def xy(cls, pt): + """Conveninience function to return the projection of the Vector in the XY-plane.""" + return Vector(pt.x, pt.y, 0) + + @classmethod + def edgeForCmd(cls, cmd, startPoint): + """Returns a Curve representing the givne command, assuming a given startinPoint.""" + + endPoint = cls.commandEndPoint(cmd, startPoint) + if cmd.Name in cls.CmdMoveStraight: + return Part.Edge(Part.Line(startPoint, endPoint)) + + if cmd.Name in cls.CmdMoveArc: + center = startPoint + cls.commandEndPoint(cmd, Vector(0,0,0), 'I', 'J', 'K') + A = cls.xy(startPoint - center) + B = cls.xy(endPoint - center) + d = -B.x * A.y + B.y * A.x + + if d == 0: + # we're dealing with half a circle here + angle = cls.getAngle(A) + math.pi/2 + if cmd.Name in cls.CmdMoveCW: + angle -= math.pi + else: + C = A + B + angle = cls.getAngle(C) + + R = A.Length + #print("arc: p1=(%.2f, %.2f) p2=(%.2f, %.2f) -> center=(%.2f, %.2f)" % (startPoint.x, startPoint.y, endPoint.x, endPoint.y, center.x, center.y)) + #print("arc: A=(%.2f, %.2f) B=(%.2f, %.2f) -> d=%.2f" % (A.x, A.y, B.x, B.y, d)) + #print("arc: R=%.2f angle=%.2f" % (R, angle/math.pi)) + if startPoint.z == endPoint.z: + midPoint = center + FreeCAD.Vector(math.cos(angle), math.sin(angle), 0) * R + return Part.Edge(Part.Arc(startPoint, midPoint, endPoint)) + + # It's a Helix + #print('angle: A=%.2f B=%.2f' % (cls.getAngle(A)/math.pi, cls.getAngle(B)/math.pi)) + if cmd.Name in cls.CmdMoveCW: + cw = True + else: + cw = False + angle = cls.diffAngle(cls.getAngle(A), cls.getAngle(B), 'CW' if cw else 'CCW') + height = endPoint.z - startPoint.z + pitch = height * math.fabs(2 * math.pi / angle) + if angle > 0: + cw = not cw + #print("Helix: R=%.2f h=%.2f angle=%.2f pitch=%.2f" % (R, height, angle/math.pi, pitch)) + helix = Part.makeHelix(pitch, height, R, 0, not cw) + helix.rotate(Vector(), Vector(0,0,1), 180 * cls.getAngle(A) / math.pi) + e = helix.Edges[0] + helix.translate(startPoint - e.valueAt(e.FirstParameter)) + return helix.Edges[0] + + diff --git a/src/Mod/Path/PathTests/PathTestUtils.py b/src/Mod/Path/PathTests/PathTestUtils.py new file mode 100644 index 000000000..35cc71bfd --- /dev/null +++ b/src/Mod/Path/PathTests/PathTestUtils.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2016 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 math +import unittest + +from PathScripts.PathGeom import Side + +class PathTestBase(unittest.TestCase): + """Base test class with some addtional asserts.""" + + def assertRoughly(self, f1, f2): + """Verify that two float values are approximately the same.""" + self.assertTrue(math.fabs(f1 - f2) < 0.00001, "%f != %f" % (f1, f2)) + + def assertCoincide(self, pt1, pt2): + """Verify that two points coincide - roughly speaking.""" + self.assertRoughly(pt1.x, pt2.x) + self.assertRoughly(pt1.y, pt2.y) + self.assertRoughly(pt1.z, pt2.z) + + def assertLine(self, edge, pt1, pt2): + """Verify that edge is a line from pt1 to pt2.""" + self.assertIs(type(edge.Curve), Part.Line) + self.assertCoincide(edge.Curve.StartPoint, pt1) + self.assertCoincide(edge.Curve.EndPoint, pt2) + + def assertArc(self, edge, pt1, pt2, direction = 'CW'): + """Verify that edge is an arc between pt1 and pt2 with the given direction.""" + # If an Arc is wrapped into edge, then it's curve is represented as a circle + # and not as an Arc (GeomTrimmedCurve) + #self.assertIs(type(edge.Curve), Part.Arc) + self.assertIs(type(edge.Curve), Part.Circle) + self.assertCoincide(edge.valueAt(edge.FirstParameter), pt1) + self.assertCoincide(edge.valueAt(edge.LastParameter), pt2) + ptm = edge.valueAt((edge.LastParameter + edge.FirstParameter)/2) + side = Side.of(pt2 - pt1, ptm - pt1) + #print("(%.2f, %.2f) (%.2f, %.2f) (%.2f, %.2f)" % (pt1.x, pt1.y, ptm.x, ptm.y, pt2.x, pt2.y)) + #print(" (%.2f, %.2f) (%.2f, %.2f) -> %s" % ((pt2-pt1).x, (pt2-pt1).y, (ptm-pt1).x, (ptm-pt1).y, Side.toString(side))) + #print(" (%.2f, %.2f) (%.2f, %.2f) -> (%.2f, %.2f)" % (pf.x,pf.y, pl.x,pl.y, pm.x, pmy)) + if 'CW' == direction: + self.assertEqual(side, Side.Left) + else: + self.assertEqual(side, Side.Right) + + + def assertCurve(self, edge, p1, p2, p3): + """Verify that the edge goes through the given 3 points, representing start, mid and end point respectively.""" + self.assertCoincide(edge.valueAt(edge.FirstParameter), p1) + self.assertCoincide(edge.valueAt(edge.LastParameter), p3) + self.assertCoincide(edge.valueAt((edge.FirstParameter + edge.LastParameter)/2), p2) + diff --git a/src/Mod/Path/PathTests/TestPathGeom.py b/src/Mod/Path/PathTests/TestPathGeom.py new file mode 100644 index 000000000..fbbc5def3 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathGeom.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2016 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 +import math +import unittest + +from FreeCAD import Vector +from PathScripts.PathDressupHoldingTags import * +from PathScripts.PathGeom import PathGeom +from PathTests.PathTestUtils import PathTestBase + +class TestPathGeom(PathTestBase): + """Test Path <-> Wire conversion.""" + + def test00(self): + """Verify getAngle functionality.""" + self.assertRoughly(PathGeom.getAngle(Vector(1, 0, 0)), 0) + self.assertRoughly(PathGeom.getAngle(Vector(1, 1, 0)), math.pi/4) + self.assertRoughly(PathGeom.getAngle(Vector(0, 1, 0)), math.pi/2) + self.assertRoughly(PathGeom.getAngle(Vector(-1, 1, 0)), 3*math.pi/4) + self.assertRoughly(PathGeom.getAngle(Vector(-1, 0, 0)), math.pi) + self.assertRoughly(PathGeom.getAngle(Vector(-1, -1, 0)), -3*math.pi/4) + self.assertRoughly(PathGeom.getAngle(Vector(0, -1, 0)), -math.pi/2) + self.assertRoughly(PathGeom.getAngle(Vector(1, -1, 0)), -math.pi/4) + + def test01(self): + """Verify diffAngle functionality.""" + self.assertRoughly(PathGeom.diffAngle(0, +0*math.pi/4, 'CW') / math.pi, 0/4.) + self.assertRoughly(PathGeom.diffAngle(0, +3*math.pi/4, 'CW') / math.pi, 5/4.) + self.assertRoughly(PathGeom.diffAngle(0, -3*math.pi/4, 'CW') / math.pi, 3/4.) + self.assertRoughly(PathGeom.diffAngle(0, +4*math.pi/4, 'CW') / math.pi, 4/4.) + self.assertRoughly(PathGeom.diffAngle(0, +0*math.pi/4, 'CCW')/ math.pi, 0/4.) + self.assertRoughly(PathGeom.diffAngle(0, +3*math.pi/4, 'CCW')/ math.pi, 3/4.) + self.assertRoughly(PathGeom.diffAngle(0, -3*math.pi/4, 'CCW')/ math.pi, 5/4.) + self.assertRoughly(PathGeom.diffAngle(0, +4*math.pi/4, 'CCW')/ math.pi, 4/4.) + + self.assertRoughly(PathGeom.diffAngle(+math.pi/4, +0*math.pi/4, 'CW') / math.pi, 1/4.) + self.assertRoughly(PathGeom.diffAngle(+math.pi/4, +3*math.pi/4, 'CW') / math.pi, 6/4.) + self.assertRoughly(PathGeom.diffAngle(+math.pi/4, -1*math.pi/4, 'CW') / math.pi, 2/4.) + self.assertRoughly(PathGeom.diffAngle(-math.pi/4, +0*math.pi/4, 'CW') / math.pi, 7/4.) + self.assertRoughly(PathGeom.diffAngle(-math.pi/4, +3*math.pi/4, 'CW') / math.pi, 4/4.) + self.assertRoughly(PathGeom.diffAngle(-math.pi/4, -1*math.pi/4, 'CW') / math.pi, 0/4.) + + self.assertRoughly(PathGeom.diffAngle(+math.pi/4, +0*math.pi/4, 'CCW') / math.pi, 7/4.) + self.assertRoughly(PathGeom.diffAngle(+math.pi/4, +3*math.pi/4, 'CCW') / math.pi, 2/4.) + self.assertRoughly(PathGeom.diffAngle(+math.pi/4, -1*math.pi/4, 'CCW') / math.pi, 6/4.) + self.assertRoughly(PathGeom.diffAngle(-math.pi/4, +0*math.pi/4, 'CCW') / math.pi, 1/4.) + self.assertRoughly(PathGeom.diffAngle(-math.pi/4, +3*math.pi/4, 'CCW') / math.pi, 4/4.) + self.assertRoughly(PathGeom.diffAngle(-math.pi/4, -1*math.pi/4, 'CCW') / math.pi, 0/4.) + + def test10(self): + """Verify proper geometry objects for G1 and G01 commands are created.""" + spt = Vector(1,2,3) + self.assertLine(PathGeom.edgeForCmd(Path.Command('G1', {'X': 7, 'Y': 2, 'Z': 3}), spt), spt, Vector(7, 2, 3)) + self.assertLine(PathGeom.edgeForCmd(Path.Command('G01', {'X': 1, 'Y': 3, 'Z': 5}), spt), spt, Vector(1, 3, 5)) + + def test20(self): + """Verfiy proper geometry for arcs in the XY-plane are created.""" + p1 = Vector(0, -1, 2) + p2 = Vector(-1, 0, 2) + self.assertArc( + PathGeom.edgeForCmd( + Path.Command('G2', {'X': p2.x, 'Y': p2.y, 'Z': p2.z, 'I': 0, 'J': 1, 'K': 0}), p1), + p1, p2, 'CW') + self.assertArc( + PathGeom.edgeForCmd( + Path.Command('G3', {'X': p1.x, 'Y': p1.y, 'z': p1.z, 'I': -1, 'J': 0, 'K': 0}), p2), + p2, p1, 'CCW') + + def test30(self): + """Verify proper geometry for arcs with rising and fall ing Z-axis are created.""" + #print("------ rising helix -------") + p1 = Vector(0, 1, 0) + p2 = Vector(1, 0, 2) + self.assertCurve( + PathGeom.edgeForCmd( + Path.Command('G2', {'X': p2.x, 'Y': p2.y, 'Z': p2.z, 'I': 0, 'J': -1, 'K': 1}), p1), + p1, Vector(1/math.sqrt(2), 1/math.sqrt(2), 1), p2) + p1 = Vector(-1, 0, 0) + p2 = Vector(0, -1, 2) + self.assertCurve( + PathGeom.edgeForCmd( + Path.Command('G3', {'X': p2.x, 'Y': p2.y, 'Z': p2.z, 'I': 1, 'J': 0, 'K': 1}), p1), + p1, Vector(-1/math.sqrt(2), -1/math.sqrt(2), 1), p2) + + #print("------ falling helix -------") + p1 = Vector(0, -1, 2) + p2 = Vector(-1, 0, 0) + self.assertCurve( + PathGeom.edgeForCmd( + Path.Command('G2', {'X': p2.x, 'Y': p2.y, 'Z': p2.z, 'I': 0, 'J': 1, 'K': -1}), p1), + p1, Vector(-1/math.sqrt(2), -1/math.sqrt(2), 1), p2) + p1 = Vector(-1, 0, 2) + p2 = Vector(0, -1, 0) + self.assertCurve( + PathGeom.edgeForCmd( + Path.Command('G3', {'X': p2.x, 'Y': p2.y, 'Z': p2.z, 'I': 1, 'J': 0, 'K': -1}), p1), + p1, Vector(-1/math.sqrt(2), -1/math.sqrt(2), 1), p2) diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 53da7186f..83e8a2bc0 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -32,3 +32,4 @@ from PathTests.TestPathDressupHoldingTags import TestTag02SquareTag from PathTests.TestPathDressupHoldingTags import TestTag03TrapezoidTag from PathTests.TestPathDressupHoldingTags import TestTag04TriangularTag +from PathTests.TestPathGeom import TestPathGeom From 0807eaf5973470bf842820510c5025c402de18a1 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Nov 2016 15:52:17 -0800 Subject: [PATCH 16/17] Added wire(s)ForPath functions with test. --- src/Mod/Path/CMakeLists.txt | 2 - src/Mod/Path/Gui/Resources/Path.qrc | 1 - .../Gui/Resources/panels/HoldingTagsEdit.ui | 240 ---- src/Mod/Path/InitGui.py | 3 +- .../PathScripts/PathDressupHoldingTags.py | 1011 ----------------- src/Mod/Path/PathScripts/PathGeom.py | 43 +- .../PathTests/TestPathDressupHoldingTags.py | 639 ----------- src/Mod/Path/PathTests/TestPathGeom.py | 24 + src/Mod/Path/TestPathApp.py | 6 - 9 files changed, 62 insertions(+), 1907 deletions(-) delete mode 100644 src/Mod/Path/Gui/Resources/panels/HoldingTagsEdit.ui delete mode 100644 src/Mod/Path/PathScripts/PathDressupHoldingTags.py delete mode 100644 src/Mod/Path/PathTests/TestPathDressupHoldingTags.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index e92a43661..8c52a7fc3 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -26,7 +26,6 @@ SET(PathScripts_SRCS PathScripts/PathCopy.py PathScripts/PathCustom.py PathScripts/PathDressup.py - PathScripts/PathDressupHoldingTags.py PathScripts/PathDrilling.py PathScripts/PathEngrave.py PathScripts/PathFacePocket.py @@ -73,7 +72,6 @@ SET(PathScripts_SRCS PathScripts/opensbp_pre.py PathScripts/rml_post.py PathScripts/slic3r_pre.py - PathTests/TestPathDressupHoldingTags.py PathTests/TestPathGeom.py PathTests/TestPathPost.py PathTests/__init__.py diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index 5be33f8ea..bb147e140 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -53,7 +53,6 @@ panels/DogboneEdit.ui panels/DrillingEdit.ui panels/EngraveEdit.ui - panels/HoldingTagsEdit.ui panels/JobEdit.ui panels/MillFaceEdit.ui panels/PocketEdit.ui diff --git a/src/Mod/Path/Gui/Resources/panels/HoldingTagsEdit.ui b/src/Mod/Path/Gui/Resources/panels/HoldingTagsEdit.ui deleted file mode 100644 index 465b0ad6e..000000000 --- a/src/Mod/Path/Gui/Resources/panels/HoldingTagsEdit.ui +++ /dev/null @@ -1,240 +0,0 @@ - - - TaskPanel - - - - 0 - 0 - 352 - 387 - - - - Holding Tags - - - - - - QFrame::NoFrame - - - 0 - - - - - 0 - 0 - 334 - 311 - - - - Tags - - - - - - - 0 - 0 - - - - true - - - 80 - - - false - - - - X - - - - - Y - - - - - Width - - - - - Height - - - - - Angle - - - - - - - - - - - Delete - - - - - - - Disable - - - - - - - Add - - - - - - - - - - - - 0 - 0 - 334 - 311 - - - - Generate - - - - - - Width - - - - - - - <html><head/><body><p>Width of each tag.</p></body></html> - - - - - - - Height - - - - - - - <html><head/><body><p>The height of the holding tag measured from the bottom of the path. By default this is set to the (estimated) height of the path.</p></body></html> - - - - - - - true - - - Angle - - - - - - - true - - - <html><head/><body><p>Angle of tag walls.</p></body></html> - - - - - - - QDialogButtonBox::Apply|QDialogButtonBox::Ok - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Layout - - - - - - Count - - - - - - - <html><head/><body><p>Enter the number of tags you wish to have.</p><p><br/></p><p>Note that sometimes it's necessary to enter a larger than desired count number and disable the ones tags you don't want in order to get the holding tag layout you want.</p></body></html> - - - - - - - Spacing - - - - - - - - - - - - - Auto Apply - - - - - - - - - - - - diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index e2b69f6b7..7c34fd592 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -74,7 +74,6 @@ class PathWorkbench (Workbench): from PathScripts import PathProfileEdges from PathScripts import DogboneDressup from PathScripts import PathMillFace - from PathScripts import PathDressupHoldingTags import PathCommands # build commands list @@ -84,7 +83,7 @@ class PathWorkbench (Workbench): twodopcmdlist = ["Path_Contour", "Path_Profile", "Path_Profile_Edges", "Path_Pocket", "Path_Drilling", "Path_Engrave", "Path_MillFace"] threedopcmdlist = ["Path_Surfacing"] modcmdlist = ["Path_Copy", "Path_CompoundExtended", "Path_Array", "Path_SimpleCopy" ] - dressupcmdlist = ["Dogbone_Dressup", "DragKnife_Dressup", "PathDressup_HoldingTags"] + dressupcmdlist = ["Dogbone_Dressup", "DragKnife_Dressup"] extracmdlist = ["Path_SelectLoop"] #modcmdmore = ["Path_Hop",] #remotecmdlist = ["Path_Remote"] diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py deleted file mode 100644 index b88ae712d..000000000 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ /dev/null @@ -1,1011 +0,0 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2016 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 Path -from PathScripts import PathUtils -from PySide import QtCore, QtGui -import math -import Part -import DraftGeomUtils - -"""Holding Tags Dressup object and FreeCAD command""" - -# Qt tanslation handling -try: - _encoding = QtGui.QApplication.UnicodeUTF8 - - def translate(context, text, disambig=None): - return QtGui.QApplication.translate(context, text, disambig, _encoding) - -except AttributeError: - - def translate(context, text, disambig=None): - return QtGui.QApplication.translate(context, text, disambig) - -debugDressup = True - -def debugMarker(vector, label, color = None, radius = 0.5): - if debugDressup: - obj = FreeCAD.ActiveDocument.addObject("Part::Sphere", label) - obj.Label = label - obj.Radius = radius - obj.Placement = FreeCAD.Placement(vector, FreeCAD.Rotation(FreeCAD.Vector(0,0,1), 0)) - if color: - obj.ViewObject.ShapeColor = color - -movecommands = ['G0', 'G00', 'G1', 'G01', 'G2', 'G02', 'G3', 'G03'] -movestraight = ['G1', 'G01'] -movecw = ['G2', 'G02'] -moveccw = ['G3', 'G03'] -movearc = movecw + moveccw - -slack = 0.0000001 - -def isAbout(v1, v2): - return math.fabs(v1 - v2) < slack - -def pointsCoincide(p1, p2): - return isAbout(p1.x, p2.x) and isAbout(p1.y, p2.y) and isAbout(p1.z, p2.z) - -def getAngle(v): - a = v.getAngle(FreeCAD.Vector(1,0,0)) - if v.y < 0: - return -a - return a - -class Side: - Left = +1 - Right = -1 - Straight = 0 - On = 0 - - @classmethod - def toString(cls, side): - if side == cls.Left: - return 'Left' - if side == cls.Right: - return 'Right' - return 'On' - - @classmethod - def of(cls, ptRef, pt): - d = -ptRef.x*pt.y + ptRef.y*pt.x - if d < 0: - return cls.Left - if d > 0: - return cls.Right - return cls.Straight - -def pathCommandForEdge(edge): - pt = edge.Curve.EndPoint - params = {'X': pt.x, 'Y': pt.y, 'Z': pt.z} - if type(edge.Curve) == Part.Line: - return Part.Command('G1', params) - - p1 = edge.Curve.StartPoint - p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter)/2) - p3 = pt - if Side.Left == Side.of(p2 - p1, p3 - p2): - cmd = 'G3' - else: - cmd = 'G2' - offset = pt1 - edge.Curve.Center - params.update({'I': offset.x, 'J': offset.y, 'K': offset.z}) - return Part.Command(cmd, params) - - -class Tag: - - @classmethod - def FromString(cls, string): - try: - t = eval(string) - return Tag(t[0], t[1], t[2], t[3], t[4], t[5]) - except: - return None - - def __init__(self, x, y, width, height, angle, enabled=True, z=None): - self.x = x - self.y = y - self.width = math.fabs(width) - self.height = math.fabs(height) - self.actualHeight = self.height - self.angle = math.fabs(angle) - self.enabled = enabled - if z is not None: - self.createSolidsAt(z) - - def toString(self): - return str((self.x, self.y, self.width, self.height, self.angle, self.enabled)) - - def originAt(self, z): - return FreeCAD.Vector(self.x, self.y, z) - - def bottom(self): - return self.z - - def top(self): - return self.z + self.actualHeight - - def centerLine(self): - return Part.Line(self.originAt(self.bottom()), self.originAt(self.top())) - - def createSolidsAt(self, z): - self.z = z - r1 = self.width / 2 - height = self.height - if self.angle == 90 and height > 0: - self.solid = Part.makeCylinder(r1, height) - self.core = self.solid.copy() - elif self.angle > 0.0 and height > 0.0: - tangens = math.tan(math.radians(self.angle)) - dr = height / tangens - if dr < r1: - r2 = r1 - dr - self.core = Part.makeCylinder(r2, height) - else: - r2 = 0 - height = r1 * tangens - self.core = None - self.actualHeight = height - self.solid = Part.makeCone(r1, r2, height) - else: - # degenerated case - no tag - self.solid = Part.makeSphere(r1 / 10000) - self.core = None - self.solid.translate(self.originAt(z)) - if self.core: - self.core.translate(self.originAt(z)) - - class Intersection: - # An intersection with a tag has 4 markant points, where one might be optional. - # - # P1---P2 P1---P2 P2 - # | | / \ /\ - # | | / \ / \ - # | | / \ / \ - # ---P0 P3--- ---P0 P3--- ---P0 P3--- - # - # If no intersection occured the Intersection can be viewed as being - # at P3 with no additional edges. - P0 = 2 - P1 = 3 - P2 = 4 - P3 = 5 - - def __init__(self, tag): - self.tag = tag - self.state = self.P3 - self.edges = [] - self.tail = None - - def isComplete(self): - return self.state == self.P3 - - def moveEdgeToPlateau(self, edge): - if type(edge.Curve) is Part.Line: - pt1 = edge.Curve.StartPoint - pt2 = edge.Curve.EndPoint - pt1.z = self.tag.top() - pt2.z = self.tag.top() - #print("\nplateau= %s - %s" %(pt1, pt2)) - return Part.Edge(Part.Line(pt1, pt2)) - - def intersectP0Core(self, edge): - #print("----- P0 (%s - %s)" % (edge.Curve.StartPoint, edge.Curve.EndPoint)) - - i = self.tag.nextIntersectionClosestTo(edge, self.tag.core, edge.Curve.StartPoint) - if i: - if pointsCoincide(i, edge.Curve.StartPoint): - # if P0 and P1 are the same, we need to insert a segment for the rise - #print("------- insert vertical rise (%s)" % i) - self.edges.append(Part.Edge(Part.Line(i, FreeCAD.Vector(i.x, i.y, self.tag.top())))) - self.p1 = i - self.state = self.P1 - return edge - if pointsCoincide(i, edge.Curve.EndPoint): - #print("------- consumed (%s)" % i) - e = edge - tail = None - else: - #print("------- split at (%s)" % i) - e, tail = self.tag.splitEdgeAt(edge, i) - self.p1 = e.Curve.EndPoint - self.edges.append(self.tag.mapEdgeToSolid(e)) - self.state = self.P1 - return tail - # no intersection, the entire edge fits between P0 and P1 - #print("------- no intersection") - self.edges.append(self.tag.mapEdgeToSolid(edge)) - return None - - def intersectP0(self, edge): - if self.tag.core: - return self.intersectP0Core(edge) - # if we have no core the tip is the origin of the Tag - line = Part.Edge(self.tag.centerLine()) - i = DraftGeomUtils.findIntersection(line, edge) - if i: - if pointsCoincide(i[0], edge.Curve.EndPoint): - e = edge - tail = None - else: - e, tail = self.tag.splitEdgeAt(edge, i[0]) - self.state = self.P2 # P1 and P2 are identical for triangular tags - self.p1 = i[0] - self.p2 = i[0] - else: - e = edge - tail = None - self.edges.append(self.tag.mapEdgeToSolid(e)) - return tail - - - - def intersectP1(self, edge): - #print("----- P1 (%s - %s)" % (edge.Curve.StartPoint, edge.Curve.EndPoint)) - i = self.tag.nextIntersectionClosestTo(edge, self.tag.core, edge.Curve.EndPoint) - if i: - if pointsCoincide(i, edge.Curve.StartPoint): - self.edges.append(self.tag.mapEdgeToSolid(edge)) - return self - if pointsCoincide(i, edge.Curve.EndPoint): - e = edge - tail = None - else: - e, tail = self.tag.splitEdgeAt(edge, i) - self.p2 = e.Curve.EndPoint - self.state = self.P2 - else: - e = edge - tail = None - self.edges.append(self.moveEdgeToPlateau(e)) - return tail - - def intersectP2(self, edge): - #print("----- P2 (%s - %s)" % (edge.Curve.StartPoint, edge.Curve.EndPoint)) - i = self.tag.nextIntersectionClosestTo(edge, self.tag.solid, edge.Curve.EndPoint) - if i: - if pointsCoincide(i, edge.Curve.StartPoint): - #print("------- insert exit plunge (%s)" % i) - self.edges.append(Part.Edge(Part.Line(FreeCAD.Vector(i.x, i.y, self.tag.top()), i))) - e = None - tail = edge - elif pointsCoincide(i, edge.Curve.EndPoint): - #print("------- entire segment added (%s)" % i) - e = edge - tail = None - else: - e, tail = self.tag.splitEdgeAt(edge, i) - #if tail: - # print("----- P3 (%s - %s)" % (tail.Curve.StartPoint, tail.Curve.EndPoint)) - #else: - # print("----- P3 (---)") - self.state = self.P3 - self.tail = tail - else: - e = edge - tail = None - if e: - self.edges.append(self.tag.mapEdgeToSolid(e)) - return tail - - def intersect(self, edge): - #print("") - #print(" >>> (%s - %s)" % (edge.Curve.StartPoint, edge.Curve.EndPoint)) - if edge and self.state == self.P0: - edge = self.intersectP0(edge) - if edge and self.state == self.P1: - edge = self.intersectP1(edge) - if edge and self.state == self.P2: - edge = self.intersectP2(edge) - return self - - - def splitEdgeAt(self, edge, pt): - p = edge.Curve.parameter(pt) - wire = edge.split(p) - return wire.Edges - - def mapEdgeToSolid(self, edge): - #print("mapEdgeToSolid: (%s %s)" % (edge.Curve.StartPoint, edge.Curve.EndPoint)) - p1a = edge.Curve.StartPoint - p1b = FreeCAD.Vector(p1a.x, p1a.y, p1a.z + self.height) - e1 = Part.Edge(Part.Line(p1a, p1b)) - p1 = self.nextIntersectionClosestTo(e1, self.solid, p1b) # top most intersection - #print(" p1: (%s %s) -> %s" % (p1a, p1b, p1)) - - p2a = edge.Curve.EndPoint - p2b = FreeCAD.Vector(p2a.x, p2a.y, p2a.z + self.height) - e2 = Part.Edge(Part.Line(p2a, p2b)) - p2 = self.nextIntersectionClosestTo(e2, self.solid, p2b) # top most intersection - #print(" p2: (%s %s) -> %s" % (p2a, p2b, p2)) - - if type(edge.Curve) == Part.Line: - return Part.Edge(Part.Line(p1, p2)) - - def filterIntersections(self, pts, face): - if type(face.Surface) == Part.Cone or type(face.Surface) == Part.Cylinder: - return filter(lambda pt: pt.z >= self.bottom() and pt.z <= self.top(), pts) - if type(face.Surface) == Part.Plane: - c = face.Edges[0].Curve - if (type(c) == Part.Circle): - return filter(lambda pt: (pt - c.Center).Length <= c.Radius, pts) - print("==== we got a %s" % face.Surface) - - - def nextIntersectionClosestTo(self, edge, solid, refPt): - pts = [] - for index, face in enumerate(solid.Faces): - i = edge.Curve.intersect(face.Surface)[0] - ps = self.filterIntersections([FreeCAD.Vector(p.X, p.Y, p.Z) for p in i], face) - pts.extend(ps) - if pts: - closest = sorted(pts, key=lambda pt: (pt - refPt).Length)[0] - #print("--pts: %s -> %s" % (pts, closest)) - return closest - return None - - def intersect(self, edge): - inters = self.Intersection(self) - if edge.Curve.StartPoint.z < self.top() or edge.Curve.EndPoint.z < self.top(): - i = self.nextIntersectionClosestTo(edge, self.solid, edge.Curve.StartPoint) - if i: - inters.state = self.Intersection.P0 - inters.p0 = i - if pointsCoincide(i, edge.Curve.EndPoint): - inters.edges.append(edge) - return inters - if pointsCoincide(i, edge.Curve.StartPoint): - tail = edge - else: - e,tail = self.splitEdgeAt(edge, i) - inters.edges.append(e) - return inters.intersect(tail) - # if we get here there is no intersection with the tag - inters.state = self.Intersection.P3 - inters.tail = edge - return inters - -class PathData: - def __init__(self, obj): - self.obj = obj - self.edges = [] - lastPt = FreeCAD.Vector(0, 0, 0) - for cmd in obj.Base.Path.Commands: - if cmd.Name in movecommands: - pt = self.point(cmd, lastPt) - if cmd.Name in movestraight: - self.edges.append(Part.Edge(Part.Line(lastPt, pt))) - elif cmd.Name in movearc: - center = lastPt + self.point(cmd, FreeCAD.Vector(0,0,0), 'I', 'J', 'K') - A = lastPt - center - B = pt - center - d = -B.x * A.y + B.y * A.x - - if d == 0: - # we're dealing with half an circle here - angle = getAngle(A) + math.pi/2 - if cmd.Name in movecw: - angle -= math.pi - else: - #print("(%.2f, %.2f) (%.2f, %.2f)" % (A.x, A.y, B.x, B.y)) - C = A + B - angle = getAngle(C) - - R = (lastPt - center).Length - ptm = center + FreeCAD.Vector(math.cos(angle), math.sin(angle), 0) * R - - self.edges.append(Part.Edge(Part.Arc(lastPt, ptm, pt))) - lastPt = pt - self.base = self.findBottomWire(self.edges) - # determine overall length - self.length = self.base.Length - - def point(self, cmd, pt, X='X', Y='Y', Z='Z'): - x = cmd.Parameters.get(X, pt.x) - y = cmd.Parameters.get(Y, pt.y) - z = cmd.Parameters.get(Z, pt.z) - return FreeCAD.Vector(x, y, z) - - def findBottomWire(self, edges): - (minZ, maxZ) = self.findZLimits(edges) - self.minZ = minZ - self.maxZ = maxZ - bottom = [e for e in edges if e.Vertexes[0].Point.z == minZ and e.Vertexes[1].Point.z == minZ] - wire = Part.Wire(bottom) - if wire.isClosed(): - return Part.Wire(self.sortedBase(bottom)) - # if we get here there are already holding tags, or we're not looking at a profile - # let's try and insert the missing pieces - another day - raise ValueError("Selected path doesn't seem to be a Profile operation.") - - def sortedBase(self, base): - # first find the exit point, where base wire is closed - edges = [e for e in self.edges if e.Curve.StartPoint.z == self.minZ and e.Curve.EndPoint.z != self.maxZ] - exit = sorted(edges, key=lambda e: -e.Curve.EndPoint.z)[0] - pt = exit.Curve.StartPoint - # then find the first base edge, and sort them until done - ordered = [] - while base: - edge = [e for e in base if e.Curve.StartPoint == pt][0] - ordered.append(edge) - base.remove(edge) - pt = edge.Curve.EndPoint - return ordered - - - def findZLimits(self, edges): - # not considering arcs and spheres in Z direction, find the highes and lowest Z values - minZ = edges[0].Vertexes[0].Point.z - maxZ = minZ - for e in edges: - for v in e.Vertexes: - if v.Point.z < minZ: - minZ = v.Point.z - if v.Point.z > maxZ: - maxZ = v.Point.z - return (minZ, maxZ) - - def shortestAndLongestPathEdge(self): - edges = sorted(self.base.Edges, key=lambda e: e.Length) - return (edges[0], edges[-1]) - - def generateTags(self, obj, count=None, width=None, height=None, angle=90, spacing=None): - #print("generateTags(%s, %s, %s, %s, %s)" % (count, width, height, angle, spacing)) - #for e in self.base.Edges: - # debugMarker(e.Vertexes[0].Point, 'base', (0.0, 1.0, 1.0), 0.2) - - if spacing: - tagDistance = spacing - else: - if count: - tagDistance = self.base.Length / count - else: - tagDistance = self.base.Length / 4 - if width: - W = width - else: - W = self.tagWidth() - if height: - H = height - else: - H = self.tagHeight() - - - # start assigning tags on the longest segment - (shortestEdge, longestEdge) = self.shortestAndLongestPathEdge() - startIndex = 0 - for i in range(0, len(self.base.Edges)): - edge = self.base.Edges[i] - if edge.Length == longestEdge.Length: - startIndex = i - break - - startEdge = self.base.Edges[startIndex] - startCount = int(startEdge.Length / tagDistance) - if (longestEdge.Length - shortestEdge.Length) > shortestEdge.Length: - startCount = int(startEdge.Length / tagDistance) + 1 - - lastTagLength = (startEdge.Length + (startCount - 1) * tagDistance) / 2 - currentLength = startEdge.Length - - minLength = min(2. * W, longestEdge.Length) - - #print("length=%.2f shortestEdge=%.2f(%.2f) longestEdge=%.2f(%.2f)" % (self.base.Length, shortestEdge.Length, shortestEdge.Length/self.base.Length, longestEdge.Length, longestEdge.Length / self.base.Length)) - #print(" start: index=%-2d count=%d (length=%.2f, distance=%.2f)" % (startIndex, startCount, startEdge.Length, tagDistance)) - #print(" -> lastTagLength=%.2f)" % lastTagLength) - #print(" -> currentLength=%.2f)" % currentLength) - - edgeDict = { startIndex: startCount } - - for i in range(startIndex + 1, len(self.base.Edges)): - edge = self.base.Edges[i] - (currentLength, lastTagLength) = self.processEdge(i, edge, currentLength, lastTagLength, tagDistance, minLength, edgeDict) - for i in range(0, startIndex): - edge = self.base.Edges[i] - (currentLength, lastTagLength) = self.processEdge(i, edge, currentLength, lastTagLength, tagDistance, minLength, edgeDict) - - tags = [] - - for (i, count) in edgeDict.iteritems(): - edge = self.base.Edges[i] - #print(" %d: %d" % (i, count)) - #debugMarker(edge.Vertexes[0].Point, 'base', (1.0, 0.0, 0.0), 0.2) - #debugMarker(edge.Vertexes[1].Point, 'base', (0.0, 1.0, 0.0), 0.2) - distance = (edge.LastParameter - edge.FirstParameter) / count - for j in range(0, count): - tag = edge.Curve.value((j+0.5) * distance) - tags.append(Tag(tag.x, tag.y, W, H, angle, True)) - - return tags - - def processEdge(self, index, edge, currentLength, lastTagLength, tagDistance, minLength, edgeDict): - tagCount = 0 - currentLength += edge.Length - if edge.Length > minLength: - while lastTagLength + tagDistance < currentLength: - tagCount += 1 - lastTagLength += tagDistance - if tagCount > 0: - #print(" index=%d -> count=%d" % (index, tagCount)) - edgeDict[index] = tagCount - #else: - #print(" skipping=%-2d (%.2f)" % (index, edge.Length)) - - return (currentLength, lastTagLength) - - def tagHeight(self): - return self.maxZ - self.minZ - - def tagWidth(self): - return self.shortestAndLongestPathEdge()[1].Length / 10 - - def tagAngle(self): - return 90 - - def pathLength(self): - return self.base.Length - - def sortedTags(self, tags): - ordered = [] - for edge in self.base.Edges: - ts = [t for t in tags if DraftGeomUtils.isPtOnEdge(t.originAt(self.minZ), edge)] - for t in sorted(ts, key=lambda t: (t.originAt(self.minZ) - edge.Curve.StartPoint).Length): - tags.remove(t) - ordered.append(t) - if tags: - raise ValueError("There's something really wrong here") - return ordered - - -class ObjectDressup: - - def __init__(self, obj): - self.obj = obj - obj.addProperty("App::PropertyLink", "Base","Base", QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTags", "The base path to modify")) - obj.addProperty("App::PropertyStringList", "Tags", "Tag", QtCore.QT_TRANSLATE_NOOP("PathDressup_holdingTags", "Inserted tags")) - obj.setEditorMode("Tags", 2) - obj.Proxy = self - - def __getstate__(self): - return None - - def __setstate__(self, state): - return None - - def generateTags(self, obj, count=None, width=None, height=None, angle=90, spacing=None): - return self.pathData.generateTags(obj, count, width, height, angle, spacing) - - - def tagIntersection(self, face, edge): - p1 = edge.Curve.StartPoint - pts = edge.Curve.intersect(face.Surface) - if pts[0]: - closest = sorted(pts[0], key=lambda pt: (pt - p1).Length)[0] - return closest - return None - - def createPath(self, edges, tagSolids): - commands = [] - i = 0 - while i != len(edges): - edge = edges[i] - while edge: - for solid in tagSolids: - for face in solid.Faces: - pt = self.tagIntersection(face, edge) - if pt: - if pt == edge.Curve.StartPoint: - pt - elif pt != edge.Curve.EndPoint: - parameter = edge.Curve.parameter(pt) - wire = edge.split(parameter) - commands.append(pathCommandForEdge(wire.Edges[0])) - edge = wire.Edges[1] - break; - else: - commands.append(pathCommandForEdge(edge)) - edge = None - i += 1 - break - if not edge: - break - if edge: - commands.append(pathCommandForEdge(edge)) - edge = None - return self.obj.Path - - - def execute(self, obj): - if not obj.Base: - return - if not obj.Base.isDerivedFrom("Path::Feature"): - return - if not obj.Base.Path: - return - if not obj.Base.Path.Commands: - return - - pathData = self.setup(obj) - if not pathData: - print("execute - no pathData") - return - - if hasattr(obj, 'Tags') and obj.Tags: - if self.fingerprint == obj.Tags: - print("execute - cache valid") - return - print("execute - tags from property") - tags = [Tag.FromString(tag) for tag in obj.Tags] - else: - print("execute - default tags") - tags = self.generateTags(obj, 4.) - - if not tags: - print("execute - no tags") - self.tags = [] - obj.Path = obj.Base.Path - return - - tagID = 0 - for tag in tags: - tagID += 1 - if tag.enabled: - #print("x=%s, y=%s, z=%s" % (tag.x, tag.y, pathData.minZ)) - debugMarker(FreeCAD.Vector(tag.x, tag.y, pathData.minZ), "tag-%02d" % tagID , (1.0, 0.0, 1.0), 0.5) - - tags = pathData.sortedTags(tags) - for tag in tags: - tag.createSolidsAt(pathData.minZ) - - self.fingerprint = [tag.toString() for tag in tags] - self.tags = tags - - #obj.Path = self.createPath(pathData.edges, tags) - obj.Path = self.Base.Path - - def setTags(self, obj, tags): - obj.Tags = [tag.toString() for tag in tags] - self.execute(obj) - - def getTags(self, obj): - if hasattr(self, 'tags'): - return self.tags - return self.setup(obj).generateTags(obj, 4) - - def setup(self, obj): - if not hasattr(self, "pathData") or not self.pathData: - try: - pathData = PathData(obj) - except ValueError: - FreeCAD.Console.PrintError(translate("PathDressup_HoldingTags", "Cannot insert holding tags for this path - please select a Profile path\n")) - return None - - ## setup the object's properties, in case they're not set yet - #obj.Count = self.tagCount(obj) - #obj.Angle = self.tagAngle(obj) - #obj.Blacklist = self.tagBlacklist(obj) - - # if the heigt isn't set, use the height of the path - #if not hasattr(obj, "Height") or not obj.Height: - # obj.Height = pathData.maxZ - pathData.minZ - # try and take an educated guess at the width - #if not hasattr(obj, "Width") or not obj.Width: - # width = sorted(pathData.base.Edges, key=lambda e: -e.Length)[0].Length / 10 - # while obj.Count > len([e for e in pathData.base.Edges if e.Length > 3*width]): - # width = widht / 2 - # obj.Width = width - - # and the tool radius, not sure yet if it's needed - #self.toolRadius = 5 - #toolLoad = PathUtils.getLastToolLoad(obj) - #if toolLoad is None or toolLoad.ToolNumber == 0: - # self.toolRadius = 5 - #else: - # tool = PathUtils.getTool(obj, toolLoad.ToolNumber) - # if not tool or tool.Diameter == 0: - # self.toolRadius = 5 - # else: - # self.toolRadius = tool.Diameter / 2 - self.pathData = pathData - return self.pathData - - def getHeight(self, obj): - return self.pathData.tagHeight() - - def getWidth(self, obj): - return self.pathData.tagWidth() - - def getAngle(self, obj): - return self.pathData.tagAngle() - - def getPathLength(self, obj): - return self.pathData.pathLength() - -class TaskPanel: - DataTag = QtCore.Qt.ItemDataRole.UserRole - DataValue = QtCore.Qt.ItemDataRole.DisplayRole - - def __init__(self, obj): - self.obj = obj - self.form = FreeCADGui.PySideUic.loadUi(":/panels/HoldingTagsEdit.ui") - FreeCAD.ActiveDocument.openTransaction(translate("PathDressup_HoldingTags", "Edit HoldingTags Dress-up")) - - def reject(self): - FreeCAD.ActiveDocument.abortTransaction() - FreeCADGui.Control.closeDialog() - FreeCAD.ActiveDocument.recompute() - FreeCADGui.Selection.removeObserver(self.s) - - def accept(self): - FreeCAD.ActiveDocument.commitTransaction() - FreeCADGui.ActiveDocument.resetEdit() - FreeCADGui.Control.closeDialog() - FreeCAD.ActiveDocument.recompute() - FreeCADGui.Selection.removeObserver(self.s) - FreeCAD.ActiveDocument.recompute() - - def open(self): - self.s = SelObserver() - # install the function mode resident - FreeCADGui.Selection.addObserver(self.s) - - def tableWidgetItem(self, tag, val): - item = QtGui.QTableWidgetItem() - item.setTextAlignment(QtCore.Qt.AlignRight) - item.setData(self.DataTag, tag) - item.setData(self.DataValue, val) - return item - - def getFields(self): - tags = [] - for row in range(0, self.form.twTags.rowCount()): - x = self.form.twTags.item(row, 0).data(self.DataValue) - y = self.form.twTags.item(row, 1).data(self.DataValue) - w = self.form.twTags.item(row, 2).data(self.DataValue) - h = self.form.twTags.item(row, 3).data(self.DataValue) - a = self.form.twTags.item(row, 4).data(self.DataValue) - tags.append(Tag(x, y, w, h, a, True)) - print("getFields: %d" % (len(tags))) - self.obj.Proxy.setTags(self.obj, tags) - - def updateTags(self): - self.tags = self.obj.Proxy.getTags(self.obj) - self.form.twTags.blockSignals(True) - self.form.twTags.setSortingEnabled(False) - self.form.twTags.clearSpans() - print("updateTags: %d" % (len(self.tags))) - self.form.twTags.setRowCount(len(self.tags)) - for row, tag in enumerate(self.tags): - self.form.twTags.setItem(row, 0, self.tableWidgetItem(tag, tag.x)) - self.form.twTags.setItem(row, 1, self.tableWidgetItem(tag, tag.y)) - self.form.twTags.setItem(row, 2, self.tableWidgetItem(tag, tag.width)) - self.form.twTags.setItem(row, 3, self.tableWidgetItem(tag, tag.height)) - self.form.twTags.setItem(row, 4, self.tableWidgetItem(tag, tag.angle)) - self.form.twTags.setSortingEnabled(True) - self.form.twTags.blockSignals(False) - - def cleanupUI(self): - print("cleanupUI") - if debugDressup: - for obj in FreeCAD.ActiveDocument.Objects: - if obj.Name.startswith('tag'): - FreeCAD.ActiveDocument.removeObject(obj.Name) - - def updateUI(self): - print("updateUI") - self.cleanupUI() - self.getFields() - if debugDressup: - FreeCAD.ActiveDocument.recompute() - - - def whenApplyClicked(self): - print("whenApplyClicked") - self.cleanupUI() - - count = self.form.sbCount.value() - spacing = self.form.dsbSpacing.value() - width = self.form.dsbWidth.value() - height = self.form.dsbHeight.value() - angle = self.form.dsbAngle.value() - - tags = self.obj.Proxy.generateTags(self.obj, count, width, height, angle, spacing * 0.99) - - self.obj.Proxy.setTags(self.obj, tags) - self.updateTags() - if debugDressup: - # this causes a big of an echo and a double click on the spin buttons, don't know why though - FreeCAD.ActiveDocument.recompute() - - def autoApply(self): - print("autoApply") - if self.form.cbAutoApply.checkState() == QtCore.Qt.CheckState.Checked: - self.whenApplyClicked() - - def updateTagSpacing(self, count): - print("updateTagSpacing") - if count == 0: - spacing = 0 - else: - spacing = self.pathLength / count - self.form.dsbSpacing.blockSignals(True) - self.form.dsbSpacing.setValue(spacing) - self.form.dsbSpacing.blockSignals(False) - - def whenCountChanged(self): - print("whenCountChanged") - self.updateTagSpacing(self.form.sbCount.value()) - self.autoApply() - - def whenSpacingChanged(self): - print("whenSpacingChanged") - if self.form.dsbSpacing.value() == 0: - count = 0 - else: - count = int(self.pathLength / self.form.dsbSpacing.value()) - self.form.sbCount.blockSignals(True) - self.form.sbCount.setValue(count) - self.form.sbCount.blockSignals(False) - self.autoApply() - - def whenOkClicked(self): - print("whenOkClicked") - self.whenApplyClicked() - self.form.toolBox.setCurrentWidget(self.form.tbpTags) - - def setupSpinBox(self, widget, val, decimals = 2): - widget.setMinimum(0) - if decimals: - widget.setDecimals(decimals) - widget.setValue(val) - - def setFields(self): - self.pathLength = self.obj.Proxy.getPathLength(self.obj) - vHeader = self.form.twTags.verticalHeader() - vHeader.setResizeMode(QtGui.QHeaderView.Fixed) - vHeader.setDefaultSectionSize(20) - self.updateTags() - self.setupSpinBox(self.form.sbCount, self.form.twTags.rowCount(), None) - self.setupSpinBox(self.form.dsbSpacing, 0) - self.setupSpinBox(self.form.dsbHeight, self.obj.Proxy.getHeight(self.obj)) - self.setupSpinBox(self.form.dsbWidth, self.obj.Proxy.getWidth(self.obj)) - self.setupSpinBox(self.form.dsbAngle, self.obj.Proxy.getAngle(self.obj)) - self.updateTagSpacing(self.form.twTags.rowCount()) - - def setupUi(self): - self.setFields() - self.form.sbCount.valueChanged.connect(self.whenCountChanged) - self.form.dsbSpacing.valueChanged.connect(self.whenSpacingChanged) - self.form.dsbHeight.valueChanged.connect(self.autoApply) - self.form.dsbWidth.valueChanged.connect(self.autoApply) - self.form.dsbAngle.valueChanged.connect(self.autoApply) - #self.form.pbAdd.clicked.connect(self.) - self.form.buttonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self.whenApplyClicked) - self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).clicked.connect(self.whenOkClicked) - self.form.twTags.itemChanged.connect(self.updateUI) - -class SelObserver: - def __init__(self): - import PathScripts.PathSelection as PST - PST.eselect() - - def __del__(self): - import PathScripts.PathSelection as PST - PST.clear() - - def addSelection(self, doc, obj, sub, pnt): - FreeCADGui.doCommand('Gui.Selection.addSelection(FreeCAD.ActiveDocument.' + obj + ')') - FreeCADGui.updateGui() - -class ViewProviderDressup: - - def __init__(self, vobj): - vobj.Proxy = self - - def attach(self, vobj): - self.Object = vobj.Object - return - - def claimChildren(self): - for i in self.Object.Base.InList: - if hasattr(i, "Group"): - group = i.Group - for g in group: - if g.Name == self.Object.Base.Name: - group.remove(g) - i.Group = group - print i.Group - #FreeCADGui.ActiveDocument.getObject(obj.Base.Name).Visibility = False - return [self.Object.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 - PathUtils.addToJob(arg1.Object.Base) - return True - -class CommandPathDressupHoldingTags: - - def GetResources(self): - return {'Pixmap': 'Path-Dressup', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTags", "HoldingTags Dress-up"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathDressup_HoldingTags", "Creates a HoldingTags 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: - FreeCAD.Console.PrintError(translate("PathDressup_HoldingTags", "Please select one path object\n")) - return - baseObject = selection[0] - if not baseObject.isDerivedFrom("Path::Feature"): - FreeCAD.Console.PrintError(translate("PathDressup_HoldingTags", "The selected object is not a path\n")) - return - if baseObject.isDerivedFrom("Path::FeatureCompoundPython"): - FreeCAD.Console.PrintError(translate("PathDressup_HoldingTags", "Please select a Profile object")) - return - - # everything ok! - FreeCAD.ActiveDocument.openTransaction(translate("PathDressup_HoldingTags", "Create HoldingTags Dress-up")) - FreeCADGui.addModule("PathScripts.PathDressupHoldingTags") - FreeCADGui.addModule("PathScripts.PathUtils") - FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "HoldingTagsDressup")') - FreeCADGui.doCommand('dbo = PathScripts.PathDressupHoldingTags.ObjectDressup(obj)') - FreeCADGui.doCommand('obj.Base = FreeCAD.ActiveDocument.' + selection[0].Name) - FreeCADGui.doCommand('PathScripts.PathDressupHoldingTags.ViewProviderDressup(obj.ViewObject)') - FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)') - FreeCADGui.doCommand('Gui.ActiveDocument.getObject(obj.Base.Name).Visibility = False') - FreeCADGui.doCommand('dbo.setup(obj)') - FreeCAD.ActiveDocument.commitTransaction() - FreeCAD.ActiveDocument.recompute() - -if FreeCAD.GuiUp: - # register the FreeCAD command - FreeCADGui.addCommand('PathDressup_HoldingTags', CommandPathDressupHoldingTags()) - -FreeCAD.Console.PrintLog("Loading PathDressupHoldingTags... done\n") diff --git a/src/Mod/Path/PathScripts/PathGeom.py b/src/Mod/Path/PathScripts/PathGeom.py index e4d176717..4ce9b08ab 100644 --- a/src/Mod/Path/PathScripts/PathGeom.py +++ b/src/Mod/Path/PathScripts/PathGeom.py @@ -65,11 +65,12 @@ class Side: class PathGeom: """Class to transform Path Commands into Edges and Wire and back again. The interface might eventuallly become part of Path itself.""" + CmdMoveFast = ['G0', 'G00'] CmdMoveStraight = ['G1', 'G01'] - CmdMoveCW = ['G2', 'G02'] - CmdMoveCCW = ['G3', 'G03'] - CmdMoveArc = CmdMoveCW + CmdMoveCCW - CmdMove = CmdMoveStraight + CmdMoveArc + CmdMoveCW = ['G2', 'G02'] + CmdMoveCCW = ['G3', 'G03'] + CmdMoveArc = CmdMoveCW + CmdMoveCCW + CmdMove = CmdMoveStraight + CmdMoveArc @classmethod def getAngle(cls, vertex): @@ -108,11 +109,11 @@ class PathGeom: return Vector(pt.x, pt.y, 0) @classmethod - def edgeForCmd(cls, cmd, startPoint): + def edgeForCmd(cls, cmd, startPoint, includeFastMoves = False): """Returns a Curve representing the givne command, assuming a given startinPoint.""" endPoint = cls.commandEndPoint(cmd, startPoint) - if cmd.Name in cls.CmdMoveStraight: + if (cmd.Name in cls.CmdMoveStraight) or (includeFastMoves and cmd.Name in cls.CmdMoveFast): return Part.Edge(Part.Line(startPoint, endPoint)) if cmd.Name in cls.CmdMoveArc: @@ -155,5 +156,35 @@ class PathGeom: e = helix.Edges[0] helix.translate(startPoint - e.valueAt(e.FirstParameter)) return helix.Edges[0] + return None + @classmethod + def wireForPath(cls, path, startPoint = FreeCAD.Vector(0, 0, 0)): + """Returns a wire representing all move commands found in the given path.""" + edges = [] + if hasattr(path, "Commands"): + for cmd in path.Commands: + edge = cls.edgeForCmd(cmd, startPoint, True) + if edge: + edges.append(edge) + startPoint = cls.commandEndPoint(cmd, startPoint) + return Part.Wire(edges) + + @classmethod + def wiresForPath(cls, path, startPoint = FreeCAD.Vector(0, 0, 0)): + """Returns a collection of wires, each representing a continuous cutting Path in path.""" + wires = [] + if hasattr(path, "Commands"): + edges = [] + for cmd in path.Commands: + if cmd.Name in cls.CmdMove: + edges.append(cls.edgeForCmd(cmd, startPoint, False)) + startPoint = cls.commandEndPoint(cmd, startPoint) + elif cmd.Name in cls.CmdMoveFast: + wires.append(Part.Wire(edges)) + edges = [] + startPoint = cls.commandEndPoint(cmd, startPoint) + if edges: + wires.append(Part.Wire(edges)) + return wires diff --git a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py deleted file mode 100644 index d6adb06d0..000000000 --- a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py +++ /dev/null @@ -1,639 +0,0 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2016 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 -import math -import unittest - -from FreeCAD import Vector -from PathScripts.PathDressupHoldingTags import * - -def pointsCoincide(pt1, pt2): - pt = pt1 - pt2 - if math.fabs(pt.x) > slack: - return False - if math.fabs(pt.y) > slack: - return False - if math.fabs(pt.z) > slack: - return False - return True - -class TagTestCaseBase(unittest.TestCase): - """Base class for all tag test cases providing additional assert functions.""" - - def assertCylinderAt(self, solid, pt, r, h): - """Verify that solid is a cylinder at the specified location.""" - self.assertEqual(len(solid.Edges), 3) - - lid = solid.Edges[0] - hull = solid.Edges[1] - base = solid.Edges[2] - - self.assertCircle(lid, Vector(pt.x, pt.y, pt.z+h), r) - self.assertLine(hull, Vector(pt.x+r, pt.y, pt.z), Vector(pt.x+r, pt.y, pt.z+h)) - self.assertCircle(base, Vector(pt.x, pt.y, pt.z), r) - - def assertConeAt(self, solid, pt, r1, r2, h): - """Verify that solid is a cone at the specified location.""" - self.assertEqual(len(solid.Edges), 3) - - lid = solid.Edges[0] - hull = solid.Edges[1] - base = solid.Edges[2] - - self.assertCircle(lid, Vector(pt.x, pt.y, pt.z+h), r2) - self.assertLine(hull, Vector(pt.x+r1, pt.y, pt.z), Vector(pt.x+r2, pt.y, pt.z+h)) - self.assertCircle(base, Vector(pt.x, pt.y, pt.z), r1) - - def assertCircle(self, edge, pt, r): - """Verivy that edge is a circle at given location.""" - curve = edge.Curve - self.assertIs(type(curve), Part.Circle) - self.assertCoincide(curve.Center, Vector(pt.x, pt.y, pt.z)) - self.assertAbout(curve.Radius, r) - - def assertLine(self, edge, pt1, pt2): - """Verify that edge is a line from pt1 to pt2.""" - curve = edge.Curve - self.assertIs(type(curve), Part.Line) - self.assertCoincide(curve.StartPoint, pt1) - self.assertCoincide(curve.EndPoint, pt2) - - def assertCoincide(self, pt1, pt2): - """Verify that 2 points coincide (with tolerance).""" - self.assertAbout(pt1.x, pt2.x) - self.assertAbout(pt1.y, pt2.y) - self.assertAbout(pt1.z, pt2.z) - - def assertAbout(self, v1, v2): - """Verify that 2 values are the same (accounting for float imprecision).""" - if math.fabs(v1 - v2) > slack: - self.fail("%f != %f" % (v1, v2)) - - def assertLines(self, edgs, tail, points): - """Check that there are 5 edges forming a trapezoid.""" - edges = list(edgs) - if tail: - edges.append(tail) - self.assertEqual(len(edges), len(points) - 1) - - for i in range(0, len(edges)): - self.assertLine(edges[i], points[i], points[i+1]) - -class TestTag00BasicHolding(TagTestCaseBase): - """Some basid test cases.""" - - def test00(self,x=1, y=1): - """Test getAngle.""" - self.assertAbout(getAngle(FreeCAD.Vector( 1*x, 0*y, 0)), 0) - self.assertAbout(getAngle(FreeCAD.Vector( 1*x, 1*y, 0)), math.pi/4) - self.assertAbout(getAngle(FreeCAD.Vector( 0*x, 1*y, 0)), math.pi/2) - self.assertAbout(getAngle(FreeCAD.Vector(-1*x, 1*y, 0)), 3*math.pi/4) - self.assertAbout(getAngle(FreeCAD.Vector(-1*x, 0*y, 0)), math.pi) - self.assertAbout(getAngle(FreeCAD.Vector(-1*x,-1*y, 0)), -3*math.pi/4) - self.assertAbout(getAngle(FreeCAD.Vector( 0*x,-1*y, 0)), -math.pi/2) - self.assertAbout(getAngle(FreeCAD.Vector( 1*x,-1*y, 0)), -math.pi/4) - - def test01(self): - """Test class Side.""" - self.assertEqual(Side.of(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 1, 0, 0)), Side.On) - self.assertEqual(Side.of(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector(-1, 0, 0)), Side.On) - self.assertEqual(Side.of(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 0, 1, 0)), Side.Left) - self.assertEqual(Side.of(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 0,-1, 0)), Side.Right) - - -class TestTag01BasicTag(TagTestCaseBase): # ============= - """Unit tests for the HoldingTags dressup.""" - - def test00(self): - """Check Tag origin, serialization and de-serialization.""" - tag = Tag(77, 13, 4, 5, 90, True) - self.assertCoincide(tag.originAt(3), Vector(77, 13, 3)) - s = tag.toString() - tagCopy = Tag.FromString(s) - self.assertEqual(tag.x, tagCopy.x) - self.assertEqual(tag.y, tagCopy.y) - self.assertEqual(tag.height, tagCopy.height) - self.assertEqual(tag.width, tagCopy.width) - self.assertEqual(tag.enabled, tagCopy.enabled) - - - def test01(self): - """Verify solid and core for a 90 degree tag are identical cylinders.""" - tag = Tag(100, 200, 4, 5, 90, True) - tag.createSolidsAt(17) - - self.assertIsNotNone(tag.solid) - self.assertCylinderAt(tag.solid, Vector(100, 200, 17), 2, 5) - - self.assertIsNotNone(tag.core) - self.assertCylinderAt(tag.core, Vector(100, 200, 17), 2, 5) - - def test02(self): - """Verify trapezoidal tag has a cone shape with a lid, and cylinder core.""" - tag = Tag(0, 0, 18, 5, 45, True) - tag.createSolidsAt(0) - - self.assertIsNotNone(tag.solid) - self.assertConeAt(tag.solid, Vector(0,0,0), 9, 4, 5) - - self.assertIsNotNone(tag.core) - self.assertCylinderAt(tag.core, Vector(0,0,0), 4, 5) - - def test03(self): - """Verify pointy cone shape of tag with pointy end if width, angle and height match up.""" - tag = Tag(0, 0, 10, 5, 45, True) - tag.createSolidsAt(0) - self.assertIsNotNone(tag.solid) - self.assertConeAt(tag.solid, Vector(0,0,0), 5, 0, 5) - - self.assertIsNone(tag.core) - - def test04(self): - """Verify height adjustment if tag isn't wide eough for angle.""" - tag = Tag(0, 0, 5, 17, 60, True) - tag.createSolidsAt(0) - self.assertIsNotNone(tag.solid) - self.assertConeAt(tag.solid, Vector(0,0,0), 2.5, 0, 2.5 * math.tan((60/180.0)*math.pi)) - - self.assertIsNone(tag.core) - -class TestTag02SquareTag(TagTestCaseBase): # ============= - """Unit tests for square tags.""" - - def test00(self): - """Verify no intersection.""" - tag = Tag( 0, 0, 4, 7, 90, True, 0) - pt1 = Vector(+5, 5, 0) - pt2 = Vector(-5, 5, 0) - edge = Part.Edge(Part.Line(pt1, pt2)) - - i = tag.intersect(edge) - self.assertIsNotNone(i) - self.assertTrue(i.isComplete()) - self.assertIsNotNone(i.edges) - self.assertFalse(i.edges) - self.assertLine(i.tail, pt1, pt2) - - def test01(self): - """Verify intersection of square tag with line ending at tag start.""" - tag = Tag( 0, 0, 8, 3, 90, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(4, 0, 0))) - - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P0) - self.assertEqual(len(i.edges), 1) - self.assertLine(i.edges[0], edge.Curve.StartPoint, edge.Curve.EndPoint) - self.assertIsNone(i.tail) - - def test02(self): - """Verify intersection of square tag with line ending between P1 and P2.""" - tag = Tag( 0, 0, 8, 3, 90, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(1, 0, 0))) - - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P1) - self.assertEqual(len(i.edges), 3) - p1 = Vector(4, 0, 0) - p2 = Vector(4, 0, 3) - p3 = Vector(1, 0, 3) - self.assertLine(i.edges[0], edge.Curve.StartPoint, p1) - self.assertLine(i.edges[1], p1, p2) - self.assertLine(i.edges[2], p2, p3) - self.assertIsNone(i.tail) - - # verify we stay in P1 if we add another segment - edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(0, 0, 0))) - i = i.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P1) - self.assertEqual(len(i.edges), 4) - p4 = Vector(0, 0, 3) - self.assertLine(i.edges[3], p3, p4) - self.assertIsNone(i.tail) - - def test03(self): - """Verify intesection of square tag with line ending on P2.""" - tag = Tag( 0, 0, 8, 3, 90, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-4, 0, 0))) - - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P2) - self.assertEqual(len(i.edges), 3) - p0 = edge.Curve.StartPoint - p1 = Vector( 4, 0, 0) - p2 = Vector( 4, 0, 3) - p3 = Vector(-4, 0, 3) - self.assertLine(i.edges[0], p0, p1) - self.assertLine(i.edges[1], p1, p2) - self.assertLine(i.edges[2], p2, p3) - self.assertIsNone(i.tail) - - # make sure it also works if we get there not directly - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(0, 0, 0))) - i = tag.intersect(edge) - edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(-4, 0, 0))) - i = i.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P2) - self.assertEqual(len(i.edges), 4) - p2a = Vector( 0, 0, 3) - self.assertLine(i.edges[0], p0, p1) - self.assertLine(i.edges[1], p1, p2) - self.assertLine(i.edges[2], p2, p2a) - self.assertLine(i.edges[3], p2a, p3) - self.assertIsNone(i.tail) - - def test04(self): - """Verify plunge down is inserted for square tag on exit.""" - tag = Tag( 0, 0, 8, 3, 90, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-5, 0, 0))) - - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P3) - self.assertTrue(i.isComplete()) - self.assertEqual(len(i.edges), 4) - p0 = edge.Curve.StartPoint - p1 = Vector( 4, 0, 0) - p2 = Vector( 4, 0, 3) - p3 = Vector(-4, 0, 3) - p4 = Vector(-4, 0, 0) - p5 = edge.Curve.EndPoint - self.assertLine(i.edges[0], p0, p1) - self.assertLine(i.edges[1], p1, p2) - self.assertLine(i.edges[2], p2, p3) - self.assertLine(i.edges[3], p3, p4) - self.assertIsNotNone(i.tail) - self.assertLine(i.tail, p4, p5) - - def test05(self): - """Verify all lines between P0 and P3 are added.""" - tag = Tag( 0, 0, 4, 7, 90, True, 0) - e0 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+2, 0, 0))) - e1 = Part.Edge(Part.Line(e0.Curve.EndPoint, Vector(+1, 0, 0))) - e2 = Part.Edge(Part.Line(e1.Curve.EndPoint, Vector(+0.5, 0, 0))) - e3 = Part.Edge(Part.Line(e2.Curve.EndPoint, Vector(-0.5, 0, 0))) - e4 = Part.Edge(Part.Line(e3.Curve.EndPoint, Vector(-1, 0, 0))) - e5 = Part.Edge(Part.Line(e4.Curve.EndPoint, Vector(-2, 0, 0))) - e6 = Part.Edge(Part.Line(e5.Curve.EndPoint, Vector(-5, 0, 0))) - - i = tag - for e in [e0, e1, e2, e3, e4, e5]: - i = i.intersect(e) - self.assertFalse(i.isComplete()) - i = i.intersect(e6) - self.assertTrue(i.isComplete()) - - pt0 = Vector(2, 0, 0) - pt1 = Vector(2, 0, 7) - pt2 = Vector(1, 0, 7) - pt3 = Vector(0.5, 0, 7) - pt4 = Vector(-0.5, 0, 7) - pt5 = Vector(-1, 0, 7) - pt6 = Vector(-2, 0, 7) - - self.assertEqual(len(i.edges), 8) - self.assertLines(i.edges, i.tail, [e0.Curve.StartPoint, pt0, pt1, pt2, pt3, pt4, pt5, pt6, e6.Curve.StartPoint, e6.Curve.EndPoint]) - self.assertIsNotNone(i.tail) - - def test06(self): - """Verify intersection of different z levels.""" - tag = Tag( 0, 0, 4, 7, 90, True, 0) - # for all lines below 7 we get the trapezoid - for i in range(0, 7): - p0 = Vector(5, 0, i) - p1 = Vector(2, 0, i) - p2 = Vector(2, 0, 7) - p3 = Vector(-2, 0, 7) - p4 = Vector(-2, 0, i) - p5 = Vector(-5, 0, i) - edge = Part.Edge(Part.Line(p0, p5)) - s = tag.intersect(edge) - self.assertTrue(s.isComplete()) - self.assertLines(s.edges, s.tail, [p0, p1, p2, p3, p4, p5]) - - # for all edges at height or above the original line is used - for i in range(7, 9): - edge = Part.Edge(Part.Line(Vector(5, 0, i), Vector(-5, 0, i))) - s = tag.intersect(edge) - self.assertTrue(s.isComplete()) - self.assertLine(s.tail, edge.Curve.StartPoint, edge.Curve.EndPoint) - -class TestTag03TrapezoidTag(TagTestCaseBase): # ============= - """Unit tests for trapezoid tags.""" - - def test00(self): - """Verify no intersection.""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - pt1 = Vector(+5, 5, 0) - pt2 = Vector(-5, 5, 0) - edge = Part.Edge(Part.Line(pt1, pt2)) - - i = tag.intersect(edge) - self.assertIsNotNone(i) - self.assertTrue(i.isComplete()) - self.assertIsNotNone(i.edges) - self.assertFalse(i.edges) - self.assertLine(i.tail, pt1, pt2) - - def test01(self): - """Veify intersection of trapezoid tag with line ending before P1.""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(4, 0, 0))) - - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P0) - self.assertEqual(len(i.edges), 1) - self.assertLine(i.edges[0], edge.Curve.StartPoint, edge.Curve.EndPoint) - self.assertIsNone(i.tail) - - # now add another segment that doesn't reach the top of the cone - edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(3, 0, 0))) - i = i.intersect(edge) - # still a P0 and edge fully consumed - p1 = Vector(edge.Curve.StartPoint) - p1.z = 0 - p2 = Vector(edge.Curve.EndPoint) - p2.z = 1 # height of cone @ (3,0) - self.assertEqual(i.state, Tag.Intersection.P0) - self.assertEqual(len(i.edges), 2) - self.assertLine(i.edges[1], p1, p2) - self.assertIsNone(i.tail) - - # add another segment to verify starting point offset - edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(2, 0, 0))) - i = i.intersect(edge) - # still a P0 and edge fully consumed - p3 = Vector(edge.Curve.EndPoint) - p3.z = 2 # height of cone @ (2,0) - self.assertEqual(i.state, Tag.Intersection.P0) - self.assertEqual(len(i.edges), 3) - self.assertLine(i.edges[2], p2, p3) - self.assertIsNone(i.tail) - - def test02(self): - """Verify intersection of trapezoid tag with line ending between P1 and P2""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(1, 0, 0))) - - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P1) - self.assertEqual(len(i.edges), 2) - p1 = Vector(4, 0, 0) - p2 = Vector(1, 0, 3) - self.assertLine(i.edges[0], edge.Curve.StartPoint, p1) - self.assertLine(i.edges[1], p1, p2) - self.assertIsNone(i.tail) - - # verify we stay in P1 if we add another segment - edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(0, 0, 0))) - i = i.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P1) - self.assertEqual(len(i.edges), 3) - p3 = Vector(0, 0, 3) - self.assertLine(i.edges[2], p2, p3) - self.assertIsNone(i.tail) - - def test03(self): - """Verify intersection of trapezoid tag with edge ending on P2.""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-1, 0, 0))) - - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P2) - p0 = Vector(edge.Curve.StartPoint) - p1 = Vector(4, 0, 0) - p2 = Vector(1, 0, 3) - p3 = Vector(-1, 0, 3) - self.assertLines(i.edges, i.tail, [p0, p1, p2, p3]) - self.assertIsNone(i.tail) - - # make sure we get the same result if there's another edge - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(1, 0, 0))) - i = tag.intersect(edge) - edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(-1, 0, 0))) - i = i.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P2) - self.assertLines(i.edges, i.tail, [p0, p1, p2, p3]) - self.assertIsNone(i.tail) - - # and also if the last segment doesn't cross the entire plateau - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(0.5, 0, 0))) - i = tag.intersect(edge) - edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(-1, 0, 0))) - i = i.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P2) - p2a = Vector(0.5, 0, 3) - self.assertLines(i.edges, i.tail, [p0, p1, p2, p2a, p3]) - self.assertIsNone(i.tail) - - def test04(self): - """Verify proper down plunge on trapezoid tag exit.""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-2, 0, 0))) - - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P2) - p0 = Vector(5, 0, 0) - p1 = Vector(4, 0, 0) - p2 = Vector(1, 0, 3) - p3 = Vector(-1, 0, 3) - p4 = Vector(-2, 0, 2) - self.assertLines(i.edges, i.tail, [p0, p1, p2, p3, p4]) - self.assertIsNone(i.tail) - - # make sure adding another segment doesn't change the state - edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(-3, 0, 0))) - i = i.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P2) - self.assertEqual(len(i.edges), 5) - p5 = Vector(-3, 0, 1) - self.assertLine(i.edges[4], p4, p5) - self.assertIsNone(i.tail) - - # now if we complete to P3 .... - edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(-4, 0, 0))) - i = i.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P3) - self.assertTrue(i.isComplete()) - self.assertEqual(len(i.edges), 6) - p6 = Vector(-4, 0, 0) - self.assertLine(i.edges[5], p5, p6) - self.assertIsNone(i.tail) - - # verify proper operation if there is a single edge going through all - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-4, 0, 0))) - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P3) - self.assertTrue(i.isComplete()) - self.assertLines(i.edges, i.tail, [p0, p1, p2, p3, p6]) - self.assertIsNone(i.tail) - - # verify tail is added as well - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-5, 0, 0))) - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P3) - self.assertTrue(i.isComplete()) - self.assertLines(i.edges, i.tail, [p0, p1, p2, p3, p6, edge.Curve.EndPoint]) - self.assertIsNotNone(i.tail) - - def test05(self): - """Verify all lines between P0 and P3 are added.""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - e0 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+4, 0, 0))) - e1 = Part.Edge(Part.Line(e0.Curve.EndPoint, Vector(+2, 0, 0))) - e2 = Part.Edge(Part.Line(e1.Curve.EndPoint, Vector(+0.5, 0, 0))) - e3 = Part.Edge(Part.Line(e2.Curve.EndPoint, Vector(-0.5, 0, 0))) - e4 = Part.Edge(Part.Line(e3.Curve.EndPoint, Vector(-1, 0, 0))) - e5 = Part.Edge(Part.Line(e4.Curve.EndPoint, Vector(-2, 0, 0))) - e6 = Part.Edge(Part.Line(e5.Curve.EndPoint, Vector(-5, 0, 0))) - - i = tag - for e in [e0, e1, e2, e3, e4, e5]: - i = i.intersect(e) - self.assertFalse(i.isComplete()) - i = i.intersect(e6) - self.assertTrue(i.isComplete()) - - p0 = Vector(4, 0, 0) - p1 = Vector(2, 0, 2) - p2 = Vector(1, 0, 3) - p3 = Vector(0.5, 0, 3) - p4 = Vector(-0.5, 0, 3) - p5 = Vector(-1, 0, 3) - p6 = Vector(-2, 0, 2) - p7 = Vector(-4, 0, 0) - - self.assertLines(i.edges, i.tail, [e0.Curve.StartPoint, p0, p1, p2, p3, p4, p5, p6, p7, e6.Curve.EndPoint]) - self.assertIsNotNone(i.tail) - - def test06(self): - """Verify intersection for different z levels.""" - tag = Tag( 0, 0, 8, 3, 45, True, 0) - # for all lines below 3 we get the trapezoid - for i in range(0, 3): - p0 = Vector(5, 0, i) - p1 = Vector(4-i, 0, i) - p2 = Vector(1, 0, 3) - p3 = Vector(-1, 0, 3) - p4 = Vector(-4+i, 0, i) - p5 = Vector(-5, 0, i) - edge = Part.Edge(Part.Line(p0, p5)) - s = tag.intersect(edge) - self.assertTrue(s.isComplete()) - self.assertLines(s.edges, s.tail, [p0, p1, p2, p3, p4, p5]) - - # for all edges at height or above the original line is used - for i in range(3, 5): - edge = Part.Edge(Part.Line(Vector(5, 0, i), Vector(-5, 0, i))) - s = tag.intersect(edge) - self.assertTrue(s.isComplete()) - self.assertLine(s.tail, edge.Curve.StartPoint, edge.Curve.EndPoint) - - -class TestTag04TriangularTag(TagTestCaseBase): # ======================== - """Unit tests for tags that take on a triangular shape.""" - - def test00(self): - """Verify intersection of triangular tag with line ending at tag start.""" - tag = Tag( 0, 0, 8, 7, 45, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(4, 0, 0))) - - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P0) - self.assertEqual(len(i.edges), 1) - self.assertLine(i.edges[0], edge.Curve.StartPoint, edge.Curve.EndPoint) - self.assertIsNone(i.tail) - - def test01(self): - """Verify intersection of triangular tag with line ending between P0 and P1.""" - tag = Tag( 0, 0, 8, 7, 45, True, 0) - edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(3, 0, 0))) - - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P0) - p1 = Vector(4, 0, 0) - p2 = Vector(3, 0, 1) - self.assertLines(i.edges, i.tail, [edge.Curve.StartPoint, p1, p2]) - self.assertIsNone(i.tail) - - # verify we stay in P1 if we add another segment - edge = Part.Edge(Part.Line(edge.Curve.EndPoint, Vector(1, 0, 0))) - i = i.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P0) - self.assertEqual(len(i.edges), 3) - p3 = Vector(1, 0, 3) - self.assertLine(i.edges[2], p2, p3) - self.assertIsNone(i.tail) - - def test02(self): - """Verify proper down plunge on exit of triangular tag.""" - tag = Tag( 0, 0, 8, 7, 45, True, 0) - - p0 = Vector(5, 0, 0) - p1 = Vector(4, 0, 0) - p2 = Vector(0, 0, 4) - edge = Part.Edge(Part.Line(p0, FreeCAD.Vector(0,0,0))) - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P2) - self.assertEqual(len(i.edges), 2) - self.assertLines(i.edges, i.tail, [p0, p1, p2]) - - # adding another segment doesn't make a difference - edge = Part.Edge(Part.Line(edge.Curve.EndPoint, FreeCAD.Vector(-3,0,0))) - i = i.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P2) - self.assertEqual(len(i.edges), 3) - p3 = Vector(-3, 0, 1) - self.assertLines(i.edges, i.tail, [p0, p1, p2, p3]) - - # same result if all is one line - edge = Part.Edge(Part.Line(p0, edge.Curve.EndPoint)) - i = tag.intersect(edge) - self.assertEqual(i.state, Tag.Intersection.P2) - self.assertLines(i.edges, i.tail, [p0, p1, p2, p3]) - - def test03(self): - """Verify triangular tag shap on intersection.""" - tag = Tag( 0, 0, 8, 7, 45, True, 0) - - p0 = Vector(5, 0, 0) - p1 = Vector(4, 0, 0) - p2 = Vector(0, 0, 4) - p3 = Vector(-4, 0, 0) - edge = Part.Edge(Part.Line(p0, p3)) - i = tag.intersect(edge) - self.assertTrue(i.isComplete()) - self.assertLines(i.edges, i.tail, [p0, p1, p2, p3]) - self.assertIsNone(i.tail) - - # this should also work if there is some excess, aka tail - p4 = Vector(-5, 0, 0) - edge = Part.Edge(Part.Line(p0, p4)) - i = tag.intersect(edge) - self.assertTrue(i.isComplete()) - self.assertLines(i.edges, i.tail, [p0, p1, p2, p3, p4]) - self.assertIsNotNone(i.tail) - diff --git a/src/Mod/Path/PathTests/TestPathGeom.py b/src/Mod/Path/PathTests/TestPathGeom.py index fbbc5def3..720857bc5 100644 --- a/src/Mod/Path/PathTests/TestPathGeom.py +++ b/src/Mod/Path/PathTests/TestPathGeom.py @@ -121,3 +121,27 @@ class TestPathGeom(PathTestBase): PathGeom.edgeForCmd( Path.Command('G3', {'X': p2.x, 'Y': p2.y, 'Z': p2.z, 'I': 1, 'J': 0, 'K': -1}), p1), p1, Vector(-1/math.sqrt(2), -1/math.sqrt(2), 1), p2) + + def test50(self): + """Verify proper wire(s) aggregation from a Path.""" + commands = [] + commands.append(Path.Command('G1', {'X': 1})) + commands.append(Path.Command('G1', {'Y': 1})) + commands.append(Path.Command('G0', {'X': 0})) + commands.append(Path.Command('G1', {'Y': 0})) + + wire = PathGeom.wireForPath(Path.Path(commands)) + self.assertEqual(len(wire.Edges), 4) + self.assertLine(wire.Edges[0], Vector(0,0,0), Vector(1,0,0)) + self.assertLine(wire.Edges[1], Vector(1,0,0), Vector(1,1,0)) + self.assertLine(wire.Edges[2], Vector(1,1,0), Vector(0,1,0)) + self.assertLine(wire.Edges[3], Vector(0,1,0), Vector(0,0,0)) + + wires = PathGeom.wiresForPath(Path.Path(commands)) + self.assertEqual(len(wires), 2) + self.assertEqual(len(wires[0].Edges), 2) + self.assertLine(wires[0].Edges[0], Vector(0,0,0), Vector(1,0,0)) + self.assertLine(wires[0].Edges[1], Vector(1,0,0), Vector(1,1,0)) + self.assertEqual(len(wires[1].Edges), 1) + self.assertLine(wires[1].Edges[0], Vector(0,1,0), Vector(0,0,0)) + diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 83e8a2bc0..5f246e8c7 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -26,10 +26,4 @@ import TestApp from PathTests.TestPathPost import PathPostTestCases -from PathTests.TestPathDressupHoldingTags import TestTag00BasicHolding -from PathTests.TestPathDressupHoldingTags import TestTag01BasicTag -from PathTests.TestPathDressupHoldingTags import TestTag02SquareTag -from PathTests.TestPathDressupHoldingTags import TestTag03TrapezoidTag -from PathTests.TestPathDressupHoldingTags import TestTag04TriangularTag - from PathTests.TestPathGeom import TestPathGeom From ad3e3166f5160acc38016e7f8bb893c4b235fdd2 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Tue, 29 Nov 2016 01:31:12 -0800 Subject: [PATCH 17/17] Some cleanup. --- src/Mod/Path/CMakeLists.txt | 1 + src/Mod/Path/PathScripts/PathGeom.py | 45 +++++++++++++++------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 8c52a7fc3..487f68d0f 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -72,6 +72,7 @@ SET(PathScripts_SRCS PathScripts/opensbp_pre.py PathScripts/rml_post.py PathScripts/slic3r_pre.py + PathTests/PathTestUtils.py PathTests/TestPathGeom.py PathTests/TestPathPost.py PathTests/__init__.py diff --git a/src/Mod/Path/PathScripts/PathGeom.py b/src/Mod/Path/PathScripts/PathGeom.py index 4ce9b08ab..450dba4cc 100644 --- a/src/Mod/Path/PathScripts/PathGeom.py +++ b/src/Mod/Path/PathScripts/PathGeom.py @@ -30,19 +30,16 @@ import Path from FreeCAD import Vector class Side: - """Class to determine and define the side a Path is on or Vectors are in relation to each other.""" + """Class to determine and define the side a Path is on, or Vectors are in relation to each other.""" Left = +1 - """Representing the left side.""" Right = -1 - """Representing the right side.""" Straight = 0 - """Used if two vectors form a line.""" On = 0 - """Synonym for Straight.""" @classmethod def toString(cls, side): - """Returns a string representation of the enum value.""" + """(side) + Returns a string representation of the enum value.""" if side == cls.Left: return 'Left' if side == cls.Right: @@ -51,7 +48,8 @@ class Side: @classmethod def of(cls, ptRef, pt): - """Determine the side of pt in relation to ptRef. + """(ptRef, pt) + Determine the side of pt in relation to ptRef. If both Points are viewed as vectors with their origin in (0,0,0) then the two vectors are either form a straigt line (On) or pt lies in the left or right hemishpere in regards to ptRef.""" @@ -74,7 +72,8 @@ class PathGeom: @classmethod def getAngle(cls, vertex): - """Returns the angle [-pi,pi] of a vertex using the X-axis as the reference. + """(vertex) + Returns the angle [-pi,pi] of a vertex using the X-axis as the reference. Positive angles for vertexes in the upper hemishpere (positive y values) and negative angles for the lower hemishpere.""" a = vertex.getAngle(FreeCAD.Vector(1,0,0)) @@ -84,7 +83,8 @@ class PathGeom: @classmethod def diffAngle(cls, a1, a2, direction = 'CW'): - """Returns the difference between two angles (a1 -> a2) into a given direction.""" + """(a1, a2, [direction='CW']) + Returns the difference between two angles (a1 -> a2) into a given direction.""" if direction == 'CW': while a1 < a2: a1 += 2*math.pi @@ -97,23 +97,26 @@ class PathGeom: @classmethod def commandEndPoint(cls, cmd, defaultPoint = Vector(), X='X', Y='Y', Z='Z'): - """Extracts the end point from a Path Command.""" + """(cmd, [defaultPoint=Vector()], [X='X'], [Y='Y'], [Z='Z']) + Extracts the end point from a Path Command.""" x = cmd.Parameters.get(X, defaultPoint.x) y = cmd.Parameters.get(Y, defaultPoint.y) z = cmd.Parameters.get(Z, defaultPoint.z) return FreeCAD.Vector(x, y, z) @classmethod - def xy(cls, pt): - """Conveninience function to return the projection of the Vector in the XY-plane.""" - return Vector(pt.x, pt.y, 0) + def xy(cls, point): + """(point) + Convenience function to return the projection of the Vector in the XY-plane.""" + return Vector(point.x, point.y, 0) @classmethod - def edgeForCmd(cls, cmd, startPoint, includeFastMoves = False): - """Returns a Curve representing the givne command, assuming a given startinPoint.""" + def edgeForCmd(cls, cmd, startPoint): + """(cmd, startPoint). + Returns an Edge representing the given command, assuming a given startPoint.""" endPoint = cls.commandEndPoint(cmd, startPoint) - if (cmd.Name in cls.CmdMoveStraight) or (includeFastMoves and cmd.Name in cls.CmdMoveFast): + if (cmd.Name in cls.CmdMoveStraight) or (cmd.Name in cls.CmdMoveFast): return Part.Edge(Part.Line(startPoint, endPoint)) if cmd.Name in cls.CmdMoveArc: @@ -160,11 +163,12 @@ class PathGeom: @classmethod def wireForPath(cls, path, startPoint = FreeCAD.Vector(0, 0, 0)): - """Returns a wire representing all move commands found in the given path.""" + """(path, [startPoint=Vector(0,0,0)]) + Returns a wire representing all move commands found in the given path.""" edges = [] if hasattr(path, "Commands"): for cmd in path.Commands: - edge = cls.edgeForCmd(cmd, startPoint, True) + edge = cls.edgeForCmd(cmd, startPoint) if edge: edges.append(edge) startPoint = cls.commandEndPoint(cmd, startPoint) @@ -172,13 +176,14 @@ class PathGeom: @classmethod def wiresForPath(cls, path, startPoint = FreeCAD.Vector(0, 0, 0)): - """Returns a collection of wires, each representing a continuous cutting Path in path.""" + """(path, [startPoint=Vector(0,0,0)]) + Returns a collection of wires, each representing a continuous cutting Path in path.""" wires = [] if hasattr(path, "Commands"): edges = [] for cmd in path.Commands: if cmd.Name in cls.CmdMove: - edges.append(cls.edgeForCmd(cmd, startPoint, False)) + edges.append(cls.edgeForCmd(cmd, startPoint)) startPoint = cls.commandEndPoint(cmd, startPoint) elif cmd.Name in cls.CmdMoveFast: wires.append(Part.Wire(edges))