diff --git a/src/Mod/Path/PathScripts/PathGeomOp.py b/src/Mod/Path/PathScripts/PathGeomOp.py index c334ead63e..f5d6f44a35 100644 --- a/src/Mod/Path/PathScripts/PathGeomOp.py +++ b/src/Mod/Path/PathScripts/PathGeomOp.py @@ -103,30 +103,41 @@ def offsetWire(wire, base, offset, forward): # offsetting a single edge doesn't work because there is an infinite # possible planes into which the edge could be offset # luckily, the plane here must be the XY-plane ... - n = (edge.Vertexes[1].Point - edge.Vertexes[0].Point).cross(FreeCAD.Vector(0, 0, 1)) + p0 = edge.Vertexes[0].Point + v0 = edge.Vertexes[1].Point - p0 + n = v0.cross(FreeCAD.Vector(0, 0, 1)) o = n.normalize() * offset edge.translate(o) - if base.isInside(edge.valueAt((edge.FirstParameter + edge.LastParameter)/2), offset/2, True): + + # offset edde the other way if the result is inside + if base.isInside(edge.valueAt((edge.FirstParameter + edge.LastParameter) / 2), offset / 2, True): edge.translate(-2 * o) - w = Part.Wire([edge]) - return orientWire(w, forward) + + # flip the edge if it's not on the right side of the original edge + if forward is not None: + v1 = edge.Vertexes[1].Point - p0 + left = PathGeom.Side.Left == PathGeom.Side.of(v0, v1) + if left != forward: + edge = PathGeom.flipEdge(edge) + return Part.Wire([edge]) + # if we get to this point the assumption is that makeOffset2D can deal with the edge pass - w = wire.makeOffset2D(offset) + offsetWire = wire.makeOffset2D(offset) if wire.isClosed(): - if not base.isInside(w.Edges[0].Vertexes[0].Point, offset/2, True): + if not base.isInside(offsetWire.Edges[0].Vertexes[0].Point, offset/2, True): PathLog.track('closed - outside') - return orientWire(w, forward) + return orientWire(offsetWire, forward) PathLog.track('closed - inside') try: - w = wire.makeOffset2D(-offset) + offsetWire = wire.makeOffset2D(-offset) except: # most likely offsetting didn't work because the wire is a hole # and the offset is too big - making the hole vanish return None - return orientWire(w, forward) + return orientWire(offsetWire, forward) # An edge is considered to be inside of shape if the mid point is inside # Of the remaining edges we take the longest wire to be the engraving side @@ -137,12 +148,15 @@ def offsetWire(wire, base, offset, forward): # if they need to be discarded, split, that should happen in a post process # 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) + # find edges that are not inside the shape 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 return False - outside = [e for e in w.Edges if not isInside(e)] + outside = [e for e in offsetWire.Edges if not isInside(e)] # discard all edges that are not part of the longest wire longestWire = None for w in [Part.Wire(el) for el in Part.sortEdges(outside)]: @@ -155,17 +169,22 @@ def offsetWire(wire, base, offset, forward): def isCircleAt(edge, center): '''isCircleAt(edge, center) ... helper function returns True if edge is a circle at the given center.''' - if Part.Circel == type(edge.Curve) or Part.ArcOfCircle == type(edge.Curve): + if Part.Circle == type(edge.Curve) or Part.ArcOfCircle == type(edge.Curve): return PathGeom.pointsCoincide(edge.Curve.Center, center) return False + # split offset wire into edges to the left side and edges to the right side collectLeft = False collectRight = False leftSideEdges = [] rightSideEdges = [] - for e in (w.Edges + w.Edges): + # traverse through all edges in order and start collecting them when we encounter + # an end point (circle centered at one of the end points of the original wire). + # should we come to an end point and determine that we've already collected the + # next side, we're done + for e in (offsetWire.Edges + offsetWire.Edges): if isCircleAt(e, start): if PathGeom.pointsCoincide(e.Curve.Axis, FreeCAD.Vector(0, 0, 1)): if not collectLeft and leftSideEdges: @@ -193,15 +212,22 @@ def offsetWire(wire, base, offset, forward): elif collectRight: rightSideEdges.append(e) + # 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 for e in longestWire.Edges: for e0 in rightSideEdges: if PathGeom.edgesMatch(e, e0): - if forward: - edges = [PathGeom.flipEdge(edge) for edge in rightSideEdges] - return Part.Wire(edges) + edges = rightSideEdges + if not forward: + 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. if not forward: - edges = [PathGeom.flipEdge(edge) for edge in rightSideEdges] - return Part.Wire(edges) + edges.reverse() + + return orientWire(Part.Wire(edges), None) diff --git a/src/Mod/Path/PathTests/TestPathGeomOp.py b/src/Mod/Path/PathTests/TestPathGeomOp.py index 5432b9a5d9..f2348a6840 100644 --- a/src/Mod/Path/PathTests/TestPathGeomOp.py +++ b/src/Mod/Path/PathTests/TestPathGeomOp.py @@ -467,4 +467,284 @@ class TestPathGeomOp(PathTestUtils.PathTestBase): self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) + def test30(self): + '''Check offsetting a single outside edge forward.''' + obj = doc.getObjectsByLabel('offset-edge')[0] + + w = getWireOutside(obj) + length = 40 * math.cos(math.pi/6) + for e in w.Edges: + self.assertRoughly(length, e.Length) + + # let's offset the horizontal edge for starters + hEdges = [e for e in w.Edges if PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y)] + + x = length / 2 + y = -10 + self.assertEqual(1, len(hEdges)) + edge = hEdges[0] + + self.assertCoincide(Vector(-x, y, 0), edge.Vertexes[0].Point) + self.assertCoincide(Vector(+x, y, 0), edge.Vertexes[1].Point) + + wire = PathGeomOp.offsetWire(Part.Wire([edge]), obj.Shape, 5, True) + self.assertEqual(1, len(wire.Edges)) + + y = y - 5 + self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[1].Point) + + # make sure we get the same result even if the edge is oriented the other way + edge = PathGeom.flipEdge(edge) + wire = PathGeomOp.offsetWire(Part.Wire([edge]), obj.Shape, 5, True) + self.assertEqual(1, len(wire.Edges)) + + self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[1].Point) + + def test31(self): + '''Check offsetting a single outside edge not forward.''' + obj = doc.getObjectsByLabel('offset-edge')[0] + + w = getWireOutside(obj) + length = 40 * math.cos(math.pi/6) + for e in w.Edges: + self.assertRoughly(length, e.Length) + + # let's offset the horizontal edge for starters + hEdges = [e for e in w.Edges if PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y)] + + x = length / 2 + y = -10 + self.assertEqual(1, len(hEdges)) + edge = hEdges[0] + self.assertCoincide(Vector(-x, y, 0), edge.Vertexes[0].Point) + self.assertCoincide(Vector(+x, y, 0), edge.Vertexes[1].Point) + + wire = PathGeomOp.offsetWire(Part.Wire([edge]), obj.Shape, 5, False) + self.assertEqual(1, len(wire.Edges)) + + y = y - 5 + self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[1].Point) + + # make sure we get the same result on a reversed edge + edge = PathGeom.flipEdge(edge) + wire = PathGeomOp.offsetWire(Part.Wire([edge]), obj.Shape, 5, False) + self.assertEqual(1, len(wire.Edges)) + + self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[1].Point) + + def test32(self): + '''Check offsetting multiple outside edges.''' + obj = doc.getObjectsByLabel('offset-edge')[0] + + w = getWireOutside(obj) + length = 40 * math.cos(math.pi/6) + + # let's offset the other two legs + lEdges = [e for e in w.Edges if not PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y)] + self.assertEqual(2, len(lEdges)) + + wire = PathGeomOp.offsetWire(Part.Wire(lEdges), obj.Shape, 2, True) + + x = length/2 + 2 * math.cos(math.pi/6) + y = -10 + 2 * math.sin(math.pi/6) + + self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(+x, y, 0), wire.Edges[-1].Vertexes[1].Point) + + rEdges = [e for e in wire.Edges if Part.Circle == type(e.Curve)] + + self.assertEqual(1, len(rEdges)) + self.assertCoincide(Vector(0, 20, 0), rEdges[0].Curve.Center) + self.assertCoincide(Vector(0, 0, -1), rEdges[0].Curve.Axis) + + #offset the other way + wire = PathGeomOp.offsetWire(Part.Wire(lEdges), obj.Shape, 2, False) + + self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(-x, y, 0), wire.Edges[-1].Vertexes[1].Point) + + rEdges = [e for e in wire.Edges if Part.Circle == type(e.Curve)] + + self.assertEqual(1, len(rEdges)) + self.assertCoincide(Vector(0, 20, 0), rEdges[0].Curve.Center) + self.assertCoincide(Vector(0, 0, +1), rEdges[0].Curve.Axis) + + def test33(self): + '''Check offsetting multiple backwards outside edges.''' + # This is exactly the same as test32, except that the wire is flipped to make + # sure the input orientation doesn't matter + obj = doc.getObjectsByLabel('offset-edge')[0] + + w = getWireOutside(obj) + length = 40 * math.cos(math.pi/6) + + # let's offset the other two legs + lEdges = [e for e in w.Edges if not PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y)] + self.assertEqual(2, len(lEdges)) + + w = PathGeom.flipWire(Part.Wire(lEdges)) + wire = PathGeomOp.offsetWire(w, obj.Shape, 2, True) + + x = length/2 + 2 * math.cos(math.pi/6) + y = -10 + 2 * math.sin(math.pi/6) + + self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(+x, y, 0), wire.Edges[-1].Vertexes[1].Point) + + rEdges = [e for e in wire.Edges if Part.Circle == type(e.Curve)] + + self.assertEqual(1, len(rEdges)) + self.assertCoincide(Vector(0, 20, 0), rEdges[0].Curve.Center) + self.assertCoincide(Vector(0, 0, -1), rEdges[0].Curve.Axis) + + #offset the other way + wire = PathGeomOp.offsetWire(Part.Wire(lEdges), obj.Shape, 2, False) + + self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(-x, y, 0), wire.Edges[-1].Vertexes[1].Point) + + rEdges = [e for e in wire.Edges if Part.Circle == type(e.Curve)] + + self.assertEqual(1, len(rEdges)) + self.assertCoincide(Vector(0, 20, 0), rEdges[0].Curve.Center) + self.assertCoincide(Vector(0, 0, +1), rEdges[0].Curve.Axis) + + def test34(self): + '''Check offsetting a single inside edge forward.''' + obj = doc.getObjectsByLabel('offset-edge')[0] + + w = getWireInside(obj) + length = 20 * math.cos(math.pi/6) + for e in w.Edges: + self.assertRoughly(length, e.Length) + + # let's offset the horizontal edge for starters + hEdges = [e for e in w.Edges if PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y)] + + x = length / 2 + y = -5 + self.assertEqual(1, len(hEdges)) + edge = hEdges[0] + + self.assertCoincide(Vector(-x, y, 0), edge.Vertexes[0].Point) + self.assertCoincide(Vector(+x, y, 0), edge.Vertexes[1].Point) + + wire = PathGeomOp.offsetWire(Part.Wire([edge]), obj.Shape, 2, True) + self.assertEqual(1, len(wire.Edges)) + + y = y + 2 + self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[1].Point) + + # make sure we get the same result even if the edge is oriented the other way + edge = PathGeom.flipEdge(edge) + wire = PathGeomOp.offsetWire(Part.Wire([edge]), obj.Shape, 2, True) + self.assertEqual(1, len(wire.Edges)) + + self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[1].Point) + + def test35(self): + '''Check offsetting a single inside edge not forward.''' + obj = doc.getObjectsByLabel('offset-edge')[0] + + w = getWireInside(obj) + length = 20 * math.cos(math.pi/6) + for e in w.Edges: + self.assertRoughly(length, e.Length) + + # let's offset the horizontal edge for starters + hEdges = [e for e in w.Edges if PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y)] + + x = length / 2 + y = -5 + self.assertEqual(1, len(hEdges)) + edge = hEdges[0] + + self.assertCoincide(Vector(-x, y, 0), edge.Vertexes[0].Point) + self.assertCoincide(Vector(+x, y, 0), edge.Vertexes[1].Point) + + wire = PathGeomOp.offsetWire(Part.Wire([edge]), obj.Shape, 2, False) + self.assertEqual(1, len(wire.Edges)) + + y = y + 2 + self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[1].Point) + + # make sure we get the same result even if the edge is oriented the other way + edge = PathGeom.flipEdge(edge) + wire = PathGeomOp.offsetWire(Part.Wire([edge]), obj.Shape, 2, False) + self.assertEqual(1, len(wire.Edges)) + + self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[1].Point) + + def test36(self): + '''Check offsetting multiple inside edges.''' + obj = doc.getObjectsByLabel('offset-edge')[0] + + w = getWireInside(obj) + length = 20 * math.cos(math.pi/6) + + # let's offset the other two legs + lEdges = [e for e in w.Edges if not PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y)] + self.assertEqual(2, len(lEdges)) + + wire = PathGeomOp.offsetWire(Part.Wire(lEdges), obj.Shape, 2, True) + + x = length/2 - 2 * math.cos(math.pi/6) + y = -5 - 2 * math.sin(math.pi/6) + + self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(-x, y, 0), wire.Edges[-1].Vertexes[1].Point) + + rEdges = [e for e in wire.Edges if Part.Circle == type(e.Curve)] + self.assertEqual(0, len(rEdges)) + + #offset the other way + wire = PathGeomOp.offsetWire(Part.Wire(lEdges), obj.Shape, 2, False) + + self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(+x, y, 0), wire.Edges[-1].Vertexes[1].Point) + + rEdges = [e for e in wire.Edges if Part.Circle == type(e.Curve)] + self.assertEqual(0, len(rEdges)) + + def test37(self): + '''Check offsetting multiple backwards inside edges.''' + # This is exactly the same as test36 except that the wire is flipped to make + # sure it's orientation doesn't matter + obj = doc.getObjectsByLabel('offset-edge')[0] + + w = getWireInside(obj) + length = 20 * math.cos(math.pi/6) + + # let's offset the other two legs + lEdges = [e for e in w.Edges if not PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y)] + self.assertEqual(2, len(lEdges)) + + w = PathGeom.flipWire(Part.Wire(lEdges)) + wire = PathGeomOp.offsetWire(w, obj.Shape, 2, True) + + x = length/2 - 2 * math.cos(math.pi/6) + y = -5 - 2 * math.sin(math.pi/6) + + self.assertCoincide(Vector(+x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(-x, y, 0), wire.Edges[-1].Vertexes[1].Point) + + rEdges = [e for e in wire.Edges if Part.Circle == type(e.Curve)] + self.assertEqual(0, len(rEdges)) + + #offset the other way + wire = PathGeomOp.offsetWire(Part.Wire(lEdges), obj.Shape, 2, False) + + self.assertCoincide(Vector(-x, y, 0), wire.Edges[0].Vertexes[0].Point) + self.assertCoincide(Vector(+x, y, 0), wire.Edges[-1].Vertexes[1].Point) + + rEdges = [e for e in wire.Edges if Part.Circle == type(e.Curve)] + self.assertEqual(0, len(rEdges))