Changed OpTools and unit tests to not rely on Part.Face creation to determine wire orientation.

This commit is contained in:
Markus Lampert
2018-07-04 22:01:11 -07:00
parent d2efdc4080
commit 41ca2b3b6c
2 changed files with 196 additions and 93 deletions

View File

@@ -27,6 +27,7 @@ import Part
import Path
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import math
from PySide import QtCore
@@ -40,37 +41,87 @@ else:
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
def debugEdge(label, e):
if Part.Line == type(e.Curve):
p0 = e.valueAt(e.FirstParameter)
p1 = e.valueAt(e.LastParameter)
print("%s Part.makeLine((%.2f, %.2f, %.2f), (%.2f, %.2f, %.2f))" % (label, p0.x, p0.y, p0.z, p1.x, p1.y, p1.z))
elif Part.Circle == type(e.Curve):
r = e.Curve.Radius
c = e.Curve.Center
a = e.Curve.Axis
xu = e.Curve.AngleXU
if a.z < 0:
first = math.degrees(xu - e.FirstParameter)
else:
first = math.degrees(xu + e.FirstParameter)
last = first + math.degrees(e.LastParameter - e.FirstParameter)
print("%s Part.makeCircle(%.2f, App.Vector(%.2f, %.2f, %.2f), App.Vector(%.2f, %.2f, %.2f), %.2f, %.2f)" % (label, r, c.x, c.y, c.z, a.x, a.y, a.z, first, last))
else:
print("%s %s (%.2f, %.2f, %.2f) -> (%.2f, %.2f, %.2f)" % (label, type(e.Curve).__name__, p0.x, p0.y, p0.z, p1.x, p1.y, p1.z))
def debugWire(label, w):
print("#%s wire >>>>>>>>>>>>>>>>>>>>>>>>" % label)
print("grp = FreeCAD.ActiveDocument.addObject('App::DocumentObjectGroup', '%s')" % label)
for i,e in enumerate(w.Edges):
edge = "%s_e%d" % (label, i)
debugEdge("%s = " % edge, e)
print("Part.show(%s, '%s')" % (edge, edge))
print("grp.addObject(FreeCAD.ActiveDocument.ActiveObject)")
print("#%s wire <<<<<<<<<<<<<<<<<<<<<<<<" % label)
def _orientEdges(inEdges):
PathLog.track()
# orient all edges of the wire so each edge's last value connects to the next edge's first value
e0 = inEdges[0]
# well, even the very first edge could be misoriented, so let's try and connect it to the second
if 1 < len(inEdges):
last = e0.valueAt(e0.LastParameter)
e1 = inEdges[1]
if not PathGeom.pointsCoincide(last, e1.valueAt(e1.FirstParameter)) and not PathGeom.pointsCoincide(last, e1.valueAt(e1.LastParameter)):
debugEdge('# _orientEdges - flip first', e0)
e0 = PathGeom.flipEdge(e0)
edges = [e0]
last = e0.valueAt(e0.LastParameter)
for e in inEdges[1:]:
edge = e if PathGeom.pointsCoincide(last, e.valueAt(e.FirstParameter)) else PathGeom.flipEdge(e)
edges.append(edge)
last = edge.valueAt(edge.LastParameter)
return edges
def _isWireClockwise(w):
# handle wires consisting of a single circle or 2 edges where one is an arc.
# in both cases, because the edges are expected to be oriented correctly, the orientation can be
# determined by looking at (one of) the circle curves.
if 2 >= len(w.Edges) and Part.Circle == type(w.Edges[0].Curve):
return 0 > w.Edges[0].Curve.Axis.z
if 2 == len(w.Edges) and Part.Circle == type(w.Edges[1].Curve):
return 0 > w.Edges[1].Curve.Axis.z
# for all other wires we presume they are polygonial and refer to Gauss
# https://en.wikipedia.org/wiki/Shoelace_formula
area = 0
for e in w.Edges:
v0 = e.valueAt(e.FirstParameter)
v1 = e.valueAt(e.LastParameter)
area = area + (v0.x * v1.y - v1.x * v0.y)
PathLog.track(area)
return area < 0
def isWireClockwise(w):
'''isWireClockwise(w) ... returns True if the wire winds clockwise. '''
return _isWireClockwise(Part.Wire(_orientEdges(w.Edges)))
def orientWire(w, forward=True):
'''orientWire(w, forward=True) ... orients given wire in a specific direction.
If forward = True (the default) the wire is oriented clockwise, looking down the negative Z axis.
If forward = False the wire is oriented counter clockwise.
If forward = None the orientation is determined by the order in which the edges appear in the wire.'''
# first, we must ensure all edges are oriented the same way
# one would thing this is the way it should be, but it turns out it isn't
# on top of that, when creating a face the axis of the face seems to depend
# the axis of any included arcs, and not in the order of the edges
e0 = w.Edges[0]
# well, even the very first edge could be misoriented, so let's try and connect it to the second
if 1 < len(w.Edges):
last = e0.valueAt(e0.LastParameter)
e1 = w.Edges[1]
if not PathGeom.pointsCoincide(last, e1.valueAt(e1.FirstParameter)) and not PathGeom.pointsCoincide(last, e1.valueAt(e1.LastParameter)):
e0 = PathGeom.flipEdge(e0)
edges = [e0]
last = e0.valueAt(e0.LastParameter)
for e in w.Edges[1:]:
edge = e if PathGeom.pointsCoincide(last, e.valueAt(e.FirstParameter)) else PathGeom.flipEdge(e)
edges.append(edge)
last = edge.valueAt(edge.LastParameter)
wire = Part.Wire(edges)
wire = Part.Wire(_orientEdges(w.Edges))
if forward is not None:
# now that we have a wire where all edges are oriented in the same way which
# also matches their order - we can create a face and get it's axis to determine
# the orientation of the wire - which is all we need here
face = Part.Face(wire)
cw = 0 < face.Surface.Axis.z
if forward != cw:
if forward != _isWireClockwise(wire):
PathLog.track('orientWire - needs flipping')
return PathGeom.flipWire(wire)
PathLog.track('orientWire - ok')
@@ -125,6 +176,7 @@ def offsetWire(wire, base, offset, forward):
pass
owire = wire.makeOffset2D(offset)
debugWire('makeOffset2D_%d' % len(wire.Edges), owire)
if wire.isClosed():
if not base.isInside(owire.Edges[0].Vertexes[0].Point, offset/2, True):
@@ -152,13 +204,29 @@ def offsetWire(wire, base, offset, forward):
# Depending on the Axis of the circle, and which side remains we know if the wire needs to be flipped
# first, let's make sure all edges are oriented the proper way
wire = orientWire(wire, None)
edges = _orientEdges(wire.Edges)
# determine the start and end point
start = edges[0].firstVertex().Point
end = edges[-1].lastVertex().Point
print("Part.show(Part.Vertex(%.2f, %.2f, %.2f), 'wire_start')" % (start.x, start.y, start.z))
print("Part.show(Part.Vertex(%.2f, %.2f, %.2f), 'wire_end')" % (end.x, end.y, end.z))
debugWire('wire', wire)
debugWire('wedges', Part.Wire(edges))
# find edges that are not inside the shape
common = base.common(owire)
insideEndpoints = [e.lastVertex().Point for e in common.Edges]
insideEndpoints.append(common.Edges[0].firstVertex().Point)
def isInside(edge):
if base.isInside(edge.Vertexes[0].Point, offset/2, True) and base.isInside(edge.Vertexes[-1].Point, offset/2, True):
return True
p0 = edge.firstVertex().Point
p1 = edge.lastVertex().Point
for p in insideEndpoints:
if PathGeom.pointsCoincide(p, p0, 0.01) or PathGeom.pointsCoincide(p, p1, 0.01):
return True
return False
outside = [e for e in owire.Edges if not isInside(e)]
# discard all edges that are not part of the longest wire
longestWire = None
@@ -166,9 +234,8 @@ def offsetWire(wire, base, offset, forward):
if not longestWire or longestWire.Length < w.Length:
longestWire = w
# find the start and end point
start = wire.Vertexes[0].Point
end = wire.Vertexes[-1].Point
debugWire('outside', Part.Wire(outside))
debugWire('longest', longestWire)
def isCircleAt(edge, center):
'''isCircleAt(edge, center) ... helper function returns True if edge is a circle at the given center.'''
@@ -215,6 +282,9 @@ def offsetWire(wire, base, offset, forward):
elif collectRight:
rightSideEdges.append(e)
debugWire('left', Part.Wire(leftSideEdges))
debugWire('right', Part.Wire(rightSideEdges))
# figure out if all the left sided edges or the right sided edges are the ones
# that are 'outside'. However, we return the full side.
edges = leftSideEdges
@@ -222,14 +292,18 @@ def offsetWire(wire, base, offset, forward):
for e0 in rightSideEdges:
if PathGeom.edgesMatch(e, e0):
edges = rightSideEdges
print("#use right side edges")
if not forward:
print("#reverse")
edges.reverse()
return orientWire(Part.Wire(edges), None)
# at this point we have the correct edges and they are in the order for forward
# traversal (climb milling). If that's not what we want just reverse the order,
# orientWire takes care of orienting the edges appropriately.
print("#use left side edges")
if not forward:
print("#reverse")
edges.reverse()
return orientWire(Part.Wire(edges), None)