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
+