diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 4a0d5edfc..701ba44b1 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -26,6 +26,7 @@ SET(PathScripts_SRCS PathScripts/PathDressup.py PathScripts/PathDressupDogbone.py PathScripts/PathDressupDragknife.py + PathScripts/PathDressupHoldingTags.py PathScripts/PathDrilling.py PathScripts/PathEngrave.py PathScripts/PathFacePocket.py @@ -73,6 +74,7 @@ SET(PathScripts_SRCS PathScripts/slic3r_pre.py PathTests/PathTestUtils.py PathTests/TestPathDepthParams.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 bb147e140..5be33f8ea 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -53,6 +53,7 @@ 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 new file mode 100644 index 000000000..465b0ad6e --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/HoldingTagsEdit.ui @@ -0,0 +1,240 @@ + + + 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 cc8405ef8..e046eb542 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -73,6 +73,7 @@ class PathWorkbench (Workbench): from PathScripts import PathProfileEdges from PathScripts import PathDressupDogbone from PathScripts import PathMillFace + from PathScripts import PathDressupHoldingTags import PathCommands # build commands list @@ -82,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 = ["PathDressup_Dogbone", "PathDressup_DragKnife"] + dressupcmdlist = ["PathDressup_Dogbone", "PathDressup_DragKnife", "PathDressup_HoldingTags"] extracmdlist = ["Path_SelectLoop"] #modcmdmore = ["Path_Hop",] #remotecmdlist = ["Path_Remote"] @@ -134,7 +135,7 @@ class PathWorkbench (Workbench): if len(FreeCADGui.Selection.getSelection()) == 1: if FreeCADGui.Selection.getSelection()[0].isDerivedFrom("Path::Feature"): self.appendContextMenu("", ["Path_Inspect"]) - if FreeCADGui.Selection.getSelection()[0].Name in ["Profile", "Contour"]: + 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 new file mode 100644 index 000000000..4c13da8ea --- /dev/null +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -0,0 +1,946 @@ +# -*- 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 DraftGeomUtils +import Path +import Part +import math + +from PathScripts import PathUtils +from PathScripts.PathGeom import * +from PySide import QtCore, QtGui + +"""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 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 PathGeom.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 PathGeom.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 PathGeom.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 PathGeom.pointsCoincide(i, edge.Curve.StartPoint): + self.edges.append(self.tag.mapEdgeToSolid(edge)) + return self + if PathGeom.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 PathGeom.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 PathGeom.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.valueAt(edge.FirstParameter).z < self.top() or edge.valueAt(edge.LastParameter).z < self.top(): + i = self.nextIntersectionClosestTo(edge, self.solid, edge.valueAt(edge.FirstParameter)) + if i: + inters.state = self.Intersection.P0 + inters.p0 = i + if PathGeom.pointsCoincide(i, edge.valueAt(edge.LastParameter)): + inters.edges.append(edge) + return inters + if PathGeom.pointsCoincide(i, edge.valueAt(edge.FirstParameter)): + 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.wire = PathGeom.wireForPath(obj.Base.Path) + self.edges = wire.Edges + self.base = self.findBottomWire(self.edges) + # determine overall length + self.length = self.base.Length + + 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/PathTests/PathTestUtils.py b/src/Mod/Path/PathTests/PathTestUtils.py index 30b56024e..029ca7914 100644 --- a/src/Mod/Path/PathTests/PathTestUtils.py +++ b/src/Mod/Path/PathTests/PathTestUtils.py @@ -27,6 +27,7 @@ import Part import math import unittest +from FreeCAD import Vector from PathScripts.PathGeom import Side class PathTestBase(unittest.TestCase): @@ -48,24 +49,35 @@ class PathTestBase(unittest.TestCase): self.assertCoincide(edge.Curve.StartPoint, pt1) self.assertCoincide(edge.Curve.EndPoint, pt2) + def assertLines(self, edgs, tail, points): + """Verify that the edges match the polygon resulting from points.""" + 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]) + 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 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.assertRoughly(curve.Radius, r) + def assertCurve(self, edge, p1, p2, p3): """Verify that the edge goes through the given 3 points, representing start, mid and end point respectively.""" @@ -73,3 +85,27 @@ class PathTestBase(unittest.TestCase): self.assertCoincide(edge.valueAt(edge.LastParameter), p3) self.assertCoincide(edge.valueAt((edge.FirstParameter + edge.LastParameter)/2), p2) + 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) + diff --git a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py new file mode 100644 index 000000000..f5db3e386 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py @@ -0,0 +1,546 @@ +# -*- 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 PathTests.PathTestUtils import PathTestBase + +class TestTag01BasicTag(PathTestBase): # ============= + """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(PathTestBase): # ============= + """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(PathTestBase): # ============= + """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(PathTestBase): # ======================== + """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 9886b77d5..872916353 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -29,3 +29,8 @@ from PathTests.TestPathPost import PathPostTestCases from PathTests.TestPathGeom import TestPathGeom from PathTests.TestPathDepthParams import depthTestCases +from PathTests.TestPathDressupHoldingTags import TestTag01BasicTag +from PathTests.TestPathDressupHoldingTags import TestTag02SquareTag +from PathTests.TestPathDressupHoldingTags import TestTag03TrapezoidTag +from PathTests.TestPathDressupHoldingTags import TestTag04TriangularTag +