Straight line intersection with square Tag.

This commit is contained in:
Markus Lampert 2016-11-20 14:53:03 -08:00
parent 74ac78276b
commit 8ce9c0c305
3 changed files with 426 additions and 78 deletions

View File

@ -114,12 +114,12 @@ def testSide():
testPrintSide(FreeCAD.Vector( 1, 0, 0), FreeCAD.Vector( 0,-1, 0))
def pathCommandForEdge(edge):
pt = edge.valueAt(edge.LastParameter)
pt = edge.Curve.EndPoint
params = {'X': pt.x, 'Y': pt.y, 'Z': pt.z}
if type(edge.Curve) == Part.Line:
return Part.Command('G1', params)
p1 = edge.valueAt(edge.FirstParameter)
p1 = edge.Curve.StartPoint
p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter)/2)
p3 = pt
if Side.Left == Side.of(p2 - p1, p3 - p2):
@ -132,13 +132,25 @@ def pathCommandForEdge(edge):
class Tag:
def __init__(self, x, y, width, height, angle, enabled=True):
@classmethod
def FromString(cls, string):
try:
t = eval(string)
return Tag(t[0], t[1], t[2], t[3], t[4], t[5])
except:
return None
def __init__(self, x, y, width, height, angle, enabled=True, z=None):
self.x = x
self.y = y
self.width = math.fabs(width)
self.height = math.fabs(height)
self.actualHeight = self.height
self.angle = math.fabs(angle)
self.enabled = enabled
if z is not None:
self.createSolidsAt(z)
def toString(self):
return str((self.x, self.y, self.width, self.height, self.angle, self.enabled))
@ -146,7 +158,14 @@ class Tag:
def originAt(self, z):
return FreeCAD.Vector(self.x, self.y, z)
def bottom(self):
return self.z
def top(self):
return self.z + self.actualHeight
def createSolidsAt(self, z):
self.z = z
r1 = self.width / 2
height = self.height
if self.angle == 90 and height > 0:
@ -162,6 +181,7 @@ class Tag:
r2 = 0
height = r1 * tangens
self.core = None
self.actualHeight = height
self.solid = Part.makeCone(r1, r2, height)
else:
# degenerated case - no tag
@ -171,14 +191,144 @@ class Tag:
if self.core:
self.core.translate(self.originAt(z))
@classmethod
def FromString(cls, string):
try:
t = eval(string)
return Tag(t[0], t[1], t[2], t[3], t[4], t[5])
except:
return None
class Intersection:
# An intersection with a tag has 4 markant points, where one might be optional.
# P1---P2 P2
# / \ /\
# / \ / \
# / \ / \
# ---P0 P3--- ---P0 P3---
# If no intersection occured the Intersection can be viewed as being
# at P3 with no additional edges.
P0 = 2
P1 = 3
P2 = 4
P3 = 5
def __init__(self, tag):
self.tag = tag
self.state = self.P3
self.edges = []
self.tail = None
def isComplete(self):
return self.state == self.P3
def moveEdgeToPlateau(self, edge):
if type(edge.Curve) is Part.Line:
pt1 = edge.Curve.StartPoint
pt2 = edge.Curve.EndPoint
pt1.z = self.tag.top()
pt2.z = self.tag.top()
#print("\nplateau= %s - %s" %(pt1, pt2))
return Part.Edge(Part.Line(pt1, pt2))
def intersect(self, edge):
#print("")
if self.state == self.P0:
#print("----- P0")
if self.tag.core:
self.state = self.P1
i = self.tag.nextIntersectionClosestTo(edge, self.tag.core, edge.Curve.StartPoint)
if i:
if i == edge.Curve.StartPoint:
self.edges.append(Part.Edge(Part.Line(i, FreeCAD.Vector(i.x, i.y, self.tag.top()))))
else:
e, tail = self.tag.splitEdgeAt(edge, i)
self.edges.append(self.mapEdgeTo(e, self.tag.solid))
edge = tail
else:
self.edges.append(self.mapEdgeTo(e, self.tag.solid))
# we're done with this edge
return self
else:
p = self.tag.originAt(self.tag.bottom() + self.tag.actualHeight)
if DraftGeomUtils.isPtOnEdge(p, edge):
e, tail = self.tag.splitEdgeAt(edge, p)
self.edges.append(self.mapEdgeTo(e, self.tag.solid))
edge = tail
self.state = self.P2
else:
self.edges.append(self.mapEdgeTo(e, self.tag.solid))
# we're done with this edge
return self
if self.state == self.P1:
#print("----- P1")
# must have core, find end of plateau
i = self.tag.nextIntersectionClosestTo(edge, self.tag.core, edge.Curve.EndPoint)
if i and i != edge.Curve.StartPoint:
self.state = self.P2
if i == edge.Curve.EndPoint:
self.edges.append(self.moveEdgeToPlateau(edge))
# edge fully consumed
return self
else:
e, tail = self.tag.splitEdgeAt(edge, i)
self.edges.append(self.moveEdgeToPlateau(e))
edge = tail
else:
self.edges.append(self.moveEdgeToPlateau(edge))
# edge fully consumed, we're still in P1
return self
if self.state == self.P2:
#print("----- P2")
i = self.tag.nextIntersectionClosestTo(edge, self.tag.solid, edge.Curve.EndPoint)
if i:
self.state = self.P3
#print("----- P3")
if i == edge.Curve.StartPoint:
self.edges.append(Part.Edge(Part.Line(FreeCAD.Vector(i.x, i.y, self.tag.top()), i)))
self.tail = edge
elif i == edge.Curve.EndPoint:
self.edges.append(self.mapEdgeTo(edge, self.tag.solid))
self.tail = None
else:
e, tail = self.tag.splitEdgeAt(edge, i)
self.edges.append(self.mapEdgeTo(e, self.tag.solid))
self.tail = tail
return self
def splitEdgeAt(self, edge, pt):
p = edge.Curve.parameter(pt)
wire = edge.split(p)
return wire.Edges
def nextIntersectionClosestTo(self, edge, solid, refPt):
pts = []
for face in solid.Faces:
i = edge.Curve.intersect(face.Surface)[0]
pts.extend([FreeCAD.Vector(p.X, p.Y, p.Z) for p in i])
if pts:
closest = sorted(pts, key=lambda pt: (pt - refPt).Length)[0]
return closest
return None
def intersect(self, edge):
inters = self.Intersection(self)
if edge.Curve.StartPoint.z < self.top() or edge.Curve.EndPoint.z < self.top():
i = self.nextIntersectionClosestTo(edge, self.solid, edge.Curve.StartPoint)
if i:
inters.state = self.Intersection.P0
if i == edge.Curve.EndPoint:
inters.edges.append(edge)
return inters
if i == edge.Curve.StartPoint:
tail = edge
else:
e,tail = self.splitEdgeAt(edge, i)
inters.edges.append(e)
return inters.intersect(tail)
# if we get here there is no intersection with the tag
inters.state = self.Intersection.P3
inters.tail = edge
return inters
class PathData:
def __init__(self, obj):
@ -235,16 +385,16 @@ class PathData:
def sortedBase(self, base):
# first find the exit point, where base wire is closed
edges = [e for e in self.edges if e.valueAt(e.FirstParameter).z == self.minZ and e.valueAt(e.LastParameter).z != self.maxZ]
exit = sorted(edges, key=lambda e: -e.valueAt(e.LastParameter).z)[0]
pt = exit.valueAt(exit.FirstParameter)
edges = [e for e in self.edges if e.Curve.StartPoint.z == self.minZ and e.Curve.EndPoint.z != self.maxZ]
exit = sorted(edges, key=lambda e: -e.Curve.EndPoint.z)[0]
pt = exit.Curve.StartPoint
# then find the first base edge, and sort them until done
ordered = []
while base:
edge = [e for e in base if e.valueAt(e.FirstParameter) == pt][0]
edge = [e for e in base if e.Curve.StartPoint == pt][0]
ordered.append(edge)
base.remove(edge)
pt = edge.valueAt(edge.LastParameter)
pt = edge.Curve.EndPoint
return ordered
@ -364,7 +514,7 @@ class PathData:
ordered = []
for edge in self.base.Edges:
ts = [t for t in tags if DraftGeomUtils.isPtOnEdge(t.originAt(self.minZ), edge)]
for t in sorted(ts, key=lambda t: (t.originAt(self.minZ) - edge.valueAt(edge.FirstParameter)).Length):
for t in sorted(ts, key=lambda t: (t.originAt(self.minZ) - edge.Curve.StartPoint).Length):
tags.remove(t)
ordered.append(t)
if tags:
@ -392,7 +542,7 @@ class ObjectDressup:
def tagIntersection(self, face, edge):
p1 = edge.valueAt(edge.FirstParameter)
p1 = edge.Curve.StartPoint
pts = edge.Curve.intersect(face.Surface)
if pts[0]:
closest = sorted(pts[0], key=lambda pt: (pt - p1).Length)[0]
@ -409,9 +559,9 @@ class ObjectDressup:
for face in solid.Faces:
pt = self.tagIntersection(face, edge)
if pt:
if pt == edge.valueAt(edge.FirstParameter):
if pt == edge.Curve.StartPoint:
pt
elif pt != edge.valueAt(edge.LastParameter):
elif pt != edge.Curve.EndPoint:
parameter = edge.Curve.parameter(pt)
wire = edge.split(parameter)
commands.append(pathCommandForEdge(wire.Edges[0]))

View File

@ -44,61 +44,8 @@ def pointsCoincide(pt1, pt2):
return False
return True
class PathDressupHoldingTagsTestCases(unittest.TestCase):
"""Unit tests for the HoldingTags dressup."""
def testTagBasics(self):
"""Check Tag origin, serialization and de-serialization."""
tag = Tag(77, 13, 4, 5, 90, True)
self.assertCoincide(tag.originAt(3), Vector(77, 13, 3))
s = tag.toString()
tagCopy = Tag.FromString(s)
self.assertEqual(tag.x, tagCopy.x)
self.assertEqual(tag.y, tagCopy.y)
self.assertEqual(tag.height, tagCopy.height)
self.assertEqual(tag.width, tagCopy.width)
self.assertEqual(tag.enabled, tagCopy.enabled)
def testTagSolidBasic(self):
"""For a 90 degree tag the core and solid are both defined and identical cylinders."""
tag = Tag(100, 200, 4, 5, 90, True)
tag.createSolidsAt(17)
self.assertIsNotNone(tag.solid)
self.assertCylinderAt(tag.solid, Vector(100, 200, 17), 2, 5)
self.assertIsNotNone(tag.core)
self.assertCylinderAt(tag.core, Vector(100, 200, 17), 2, 5)
def testTagSolidFlatCone(self):
"""Tests a Tag that has an angle leaving a flat face on top of the cone."""
tag = Tag(0, 0, 18, 5, 45, True)
tag.createSolidsAt(0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 9, 4, 5)
self.assertIsNotNone(tag.core)
self.assertCylinderAt(tag.core, Vector(0,0,0), 4, 5)
def testTagSolidCone(self):
"""Tests a Tag who's angled sides coincide at the tag's height."""
tag = Tag(0, 0, 10, 5, 45, True)
tag.createSolidsAt(0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 5, 0, 5)
self.assertIsNone(tag.core)
def testTagSolidShortCone(self):
"""Tests a Tag that's not wide enough to reach full height."""
tag = Tag(0, 0, 5, 17, 60, True)
tag.createSolidsAt(0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 2.5, 0, 2.5 * math.tan((60/180.0)*math.pi))
self.assertIsNone(tag.core)
class TagTestCaseBase(unittest.TestCase):
"""Base class for all tag test cases providing additional assert functions."""
def assertCylinderAt(self, solid, pt, r, h):
"""Verify that solid is a cylinder at the specified location."""
@ -127,14 +74,14 @@ class PathDressupHoldingTagsTestCases(unittest.TestCase):
def assertCircle(self, edge, pt, r):
"""Verivy that edge is a circle at given location."""
curve = edge.Curve
self.assertTrue(type(curve), Part.Circle)
self.assertIs(type(curve), Part.Circle)
self.assertCoincide(curve.Center, Vector(pt.x, pt.y, pt.z))
self.assertAbout(curve.Radius, r)
def assertLine(self, edge, pt1, pt2):
"""Verify that edge is a line from pt1 to pt2."""
curve = edge.Curve
self.assertTrue(type(curve), Part.Line)
self.assertIs(type(curve), Part.Line)
self.assertCoincide(curve.StartPoint, pt1)
self.assertCoincide(curve.EndPoint, pt2)
@ -147,4 +94,254 @@ class PathDressupHoldingTagsTestCases(unittest.TestCase):
def assertAbout(self, v1, v2):
"""Verify that 2 values are the same (accounting for float imprecision)."""
#print("assertAbout(%f, %f)" % (v1, v2))
self.assertTrue(math.fabs(v1 - v2) < slack)
if math.fabs(v1 - v2) > slack:
self.fail("%f != %f" % (v1, v2))
def assertTrapezoid(self, edgs, tail, spec):
"""Check that there are 5 edges forming a trapezoid."""
edges = list(edgs)
if tail:
edges.append(tail)
self.assertEqual(len(edges), 5)
p0 = spec[0]
p1 = Vector(spec[1], p0.y, p0.z)
p2 = Vector(p1.x, p1.y, spec[2])
p3 = Vector(-p2.x, p2.y, p2.z)
p4 = Vector(p3.x, p3.y, p0.z)
p5 = spec[3]
self.assertLine(edges[0], p0, p1)
self.assertLine(edges[1], p1, p2)
self.assertLine(edges[2], p2, p3)
self.assertLine(edges[3], p3, p4)
self.assertLine(edges[4], p4, p5)
class TagTestCases(TagTestCaseBase): # =============
"""Unit tests for the HoldingTags dressup."""
def testTagBasics(self):
#"""Check Tag origin, serialization and de-serialization."""
tag = Tag(77, 13, 4, 5, 90, True)
self.assertCoincide(tag.originAt(3), Vector(77, 13, 3))
s = tag.toString()
tagCopy = Tag.FromString(s)
self.assertEqual(tag.x, tagCopy.x)
self.assertEqual(tag.y, tagCopy.y)
self.assertEqual(tag.height, tagCopy.height)
self.assertEqual(tag.width, tagCopy.width)
self.assertEqual(tag.enabled, tagCopy.enabled)
def testTagSolidBasic(self):
#"""For a 90 degree tag the core and solid are both defined and identical cylinders."""
tag = Tag(100, 200, 4, 5, 90, True)
tag.createSolidsAt(17)
self.assertIsNotNone(tag.solid)
self.assertCylinderAt(tag.solid, Vector(100, 200, 17), 2, 5)
self.assertIsNotNone(tag.core)
self.assertCylinderAt(tag.core, Vector(100, 200, 17), 2, 5)
def testTagSolidFlatCone(self):
#"""Tests a Tag that has an angle leaving a flat face on top of the cone."""
tag = Tag(0, 0, 18, 5, 45, True)
tag.createSolidsAt(0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 9, 4, 5)
self.assertIsNotNone(tag.core)
self.assertCylinderAt(tag.core, Vector(0,0,0), 4, 5)
def testTagSolidCone(self):
#"""Tests a Tag who's angled sides coincide at the tag's height."""
tag = Tag(0, 0, 10, 5, 45, True)
tag.createSolidsAt(0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 5, 0, 5)
self.assertIsNone(tag.core)
def testTagSolidShortCone(self):
#"""Tests a Tag that's not wide enough to reach full height."""
tag = Tag(0, 0, 5, 17, 60, True)
tag.createSolidsAt(0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 2.5, 0, 2.5 * math.tan((60/180.0)*math.pi))
self.assertIsNone(tag.core)
class SquareTagTestCases(TagTestCaseBase): # =============
"""Unit tests for square tags."""
def testTagNoIntersect(self):
#"""Check that the returned tail if no intersection occurs matches the input."""
tag = Tag( 0, 0, 4, 7, 90, True, 0)
pt1 = Vector(+5, 3, 0)
pt2 = Vector(-5, 3, 0)
edge = Part.Edge(Part.Line(pt1, pt2))
i = tag.intersect(edge)
self.assertIsNotNone(i)
self.assertTrue(i.isComplete())
self.assertIsNotNone(i.edges)
self.assertFalse(i.edges)
self.assertLine(i.tail, pt1, pt2)
def testTagIntersectLine(self):
#"""Test that a straight line passing through a cylindrical tag is split up into 5 segments."""
tag = Tag( 0, 0, 4, 7, 90, True, 0)
pt1 = Vector(+5, 0, 0)
pt2 = Vector(-5, 0, 0)
edge = Part.Edge(Part.Line(pt1, pt2))
i = tag.intersect(edge)
self.assertIsNotNone(i)
self.assertTrue(i.isComplete())
pt0a = Vector(+2, 0, 0)
pt0b = Vector(+2, 0, 7)
pt0c = Vector(-2, 0, 7)
pt0d = Vector(-2, 0, 0)
self.assertEqual(len(i.edges), 4)
self.assertLine(i.edges[0], pt1, pt0a)
self.assertLine(i.edges[1], pt0a, pt0b)
self.assertLine(i.edges[2], pt0b, pt0c)
self.assertLine(i.edges[3], pt0c, pt0d)
self.assertLine(i.tail, pt0d, pt2)
def testTagIntersectPartialLineP0(self):
#"""Make sure line is accounted for if it reaches P0."""
tag = Tag( 0, 0, 4, 7, 90, True, 0)
edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(2, 0, 0)))
i = tag.intersect(edge)
self.assertFalse(i.isComplete())
self.assertEqual(len(i.edges), 1)
self.assertLine(i.edges[0], edge.Curve.StartPoint, edge.Curve.EndPoint)
self.assertIsNone(i.tail)
def testTagIntersectPartialLineP1(self):
#"""Make sure line is accounted for if it reaches beyond P1."""
tag = Tag( 0, 0, 4, 7, 90, True, 0)
edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(1, 0, 0)))
i = tag.intersect(edge)
self.assertFalse(i.isComplete())
pt0a = Vector(+2, 0, 0)
pt0b = Vector(+2, 0, 7)
pt1a = Vector(+1, 0, 7)
self.assertEqual(len(i.edges), 3)
self.assertLine(i.edges[0], edge.Curve.StartPoint, pt0a)
self.assertLine(i.edges[1], pt0a, pt0b)
self.assertLine(i.edges[2], pt0b, pt1a)
self.assertIsNone(i.tail)
def testTagIntersectPartialLineP2(self):
#"""Make sure line is accounted for if it reaches beyond P2."""
tag = Tag( 0, 0, 4, 7, 90, True, 0)
edge = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(-1, 0, 0)))
i = tag.intersect(edge)
self.assertFalse(i.isComplete())
pt0a = Vector(+2, 0, 0)
pt0b = Vector(+2, 0, 7)
pt1a = Vector(-1, 0, 7)
self.assertEqual(len(i.edges), 3)
self.assertLine(i.edges[0], edge.Curve.StartPoint, pt0a)
self.assertLine(i.edges[1], pt0a, pt0b)
self.assertLine(i.edges[2], pt0b, pt1a)
self.assertIsNone(i.tail)
def testTagIntersectPartialLineP11(self):
#"""Make sure a line is accounted for if it lies entirely between P1 and P2."""
tag = Tag( 0, 0, 4, 7, 90, True, 0)
e1 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+1, 0, 0)))
i = tag.intersect(e1)
self.assertFalse(i.isComplete())
e2 = Part.Edge(Part.Line(e1.Curve.EndPoint, Vector(0,0,0)))
i = i.intersect(e2)
pt0a = Vector(+2, 0, 0)
pt0b = Vector(+2, 0, 7)
pt1a = Vector(+1, 0, 7)
pt1b = Vector( 0, 0, 7)
self.assertEqual(len(i.edges), 4)
self.assertLine(i.edges[0], e1.Curve.StartPoint, pt0a)
self.assertLine(i.edges[1], pt0a, pt0b)
self.assertLine(i.edges[2], pt0b, pt1a)
self.assertLine(i.edges[3], pt1a, pt1b)
self.assertIsNone(i.tail)
def testTagIntersectPartialLinesP11223(self):
#"""Verify all lines between P0 and P3 are added."""
tag = Tag( 0, 0, 4, 7, 90, True, 0)
e0 = Part.Edge(Part.Line(Vector(5, 0, 0), Vector(+2, 0, 0)))
e1 = Part.Edge(Part.Line(e0.Curve.EndPoint, Vector(+1, 0, 0)))
e2 = Part.Edge(Part.Line(e1.Curve.EndPoint, Vector(+0.5, 0, 0)))
e3 = Part.Edge(Part.Line(e2.Curve.EndPoint, Vector(-0.5, 0, 0)))
e4 = Part.Edge(Part.Line(e3.Curve.EndPoint, Vector(-1, 0, 0)))
e5 = Part.Edge(Part.Line(e4.Curve.EndPoint, Vector(-2, 0, 0)))
e6 = Part.Edge(Part.Line(e5.Curve.EndPoint, Vector(-5, 0, 0)))
i = tag
for e in [e0, e1, e2, e3, e4, e5]:
i = i.intersect(e)
self.assertFalse(i.isComplete())
i = i.intersect(e6)
self.assertTrue(i.isComplete())
pt0 = Vector(2, 0, 0)
pt1 = Vector(2, 0, 7)
pt2 = Vector(1, 0, 7)
pt3 = Vector(0.5, 0, 7)
pt4 = Vector(-0.5, 0, 7)
pt5 = Vector(-1, 0, 7)
pt6 = Vector(-2, 0, 7)
self.assertEqual(len(i.edges), 8)
self.assertLine(i.edges[0], e0.Curve.StartPoint, pt0)
self.assertLine(i.edges[1], pt0, pt1)
self.assertLine(i.edges[2], pt1, pt2)
self.assertLine(i.edges[3], pt2, pt3)
self.assertLine(i.edges[4], pt3, pt4)
self.assertLine(i.edges[5], pt4, pt5)
self.assertLine(i.edges[6], pt5, pt6)
self.assertLine(i.edges[7], pt6, e5.Curve.EndPoint)
self.assertTrue(i.isComplete())
self.assertIsNotNone(i.tail)
self.assertLine(i.tail, e6.Curve.StartPoint, e6.Curve.EndPoint)
def testTagIntersectLineAt(self):
tag = Tag( 0, 0, 4, 7, 90, True, 0)
# for all lines below 7 we get the trapezoid
for i in range(0, 7):
edge = Part.Edge(Part.Line(Vector(5, 0, i), Vector(-5, 0, i)))
s = tag.intersect(edge)
self.assertTrue(s.isComplete())
self.assertTrapezoid(s.edges, s.tail, [edge.Curve.StartPoint, 2, 7, edge.Curve.EndPoint])
# for all edges at height or above the original line is used
for i in range(7, 9):
edge = Part.Edge(Part.Line(Vector(5, 0, i), Vector(-5, 0, i)))
s = tag.intersect(edge)
self.assertTrue(s.isComplete())
self.assertLine(s.tail, edge.Curve.StartPoint, edge.Curve.EndPoint)

View File

@ -25,4 +25,5 @@
import TestApp
from PathTests.TestPathPost import PathPostTestCases
from PathTests.TestPathDressupHoldingTags import PathDressupHoldingTagsTestCases
from PathTests.TestPathDressupHoldingTags import TagTestCases
from PathTests.TestPathDressupHoldingTags import SquareTagTestCases