diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index f231f3fbc5..e0b62a462f 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -45,6 +45,7 @@ SET(PathScripts_SRCS PathScripts/PathEngraveGui.py PathScripts/PathFixture.py PathScripts/PathGeom.py + PathScripts/PathGeomOp.py PathScripts/PathGetPoint.py PathScripts/PathGui.py PathScripts/PathHelix.py @@ -123,6 +124,7 @@ SET(PathTests_SRCS PathTests/TestPathDressupDogbone.py PathTests/TestPathDressupHoldingTags.py PathTests/TestPathGeom.py + PathTests/TestPathGeomOp.py PathTests/TestPathLog.py PathTests/TestPathPost.py PathTests/TestPathSetupSheet.py @@ -133,7 +135,7 @@ SET(PathTests_SRCS PathTests/TestPathUtil.py PathTests/boxtest.fcstd PathTests/test_centroid_00.ngc - PathTests/test_chamfer.fcstd + PathTests/test_geomop.fcstd PathTests/test_linuxcnc_00.ngc ) diff --git a/src/Mod/Path/PathScripts/PathChamfer.py b/src/Mod/Path/PathScripts/PathChamfer.py index 61c891b366..8344ccba27 100644 --- a/src/Mod/Path/PathScripts/PathChamfer.py +++ b/src/Mod/Path/PathScripts/PathChamfer.py @@ -26,7 +26,7 @@ import FreeCAD import Part import Path import PathScripts.PathEngraveBase as PathEngraveBase -import PathScripts.PathGeom as PathGeom +import PathScripts.PathGeomOp as PathGeomOp import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import math @@ -43,170 +43,6 @@ else: def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) -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) - 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: - PathLog.track('orientWire - needs flipping') - return PathGeom.flipWire(wire) - PathLog.track('orientWire - ok') - return wire - -def isCircleAt(edge, center): - '''isCircleAt(edge, center) ... helper function returns True if edge is a circle at the given center.''' - if Circel == type(edge.Curve) or ArcOfCircle == type(edge.Curve): - return PathGeom.pointsCoincide(edge.Curve.Center, center) - return False - -def offsetWire(wire, base, offset, forward): - '''offsetWire(wire, base, offset, forward) ... offsets the wire away from base and orients the wire accordingly. - The function tries to avoid most of the pitfalls of Part.makeOffset2D which is possible because all offsetting - happens in the XY plane. - ''' - PathLog.track('offsetWire') - - if 1 == len(wire.Edges): - edge = wire.Edges[0] - curve = edge.Curve - if Part.Circle == type(curve) and wire.isClosed(): - # it's a full circle and there are some problems with that, see - # http://www.freecadweb.org/wiki/Part%20Offset2D - # it's easy to construct them manually though - z = -1 if forward else 1 - edge = Part.makeCircle(curve.Radius + offset, curve.Center, FreeCAD.Vector(0, 0, z)) - if base.isInside(edge.Vertexes[0].Point, offset/2, True): - if offset > curve.Radius or PathGeom.isRoughly(offset, curve.Radius): - # offsetting a hole by its own radius (or more) makes the hole vanish - return None - edge = Part.makeCircle(curve.Radius - offset, curve.Center, FreeCAD.Vector(0, 0, -z)) - w = Part.Wire([edge]) - return w - if Part.Line == type(curve) or Part.LineSegment == type(curve): - # 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)) - o = n.normalize() * offset - edge.translate(o) - 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) - # if we get to this point the assumption is that makeOffset2D can deal with the edge - pass - - w = wire.makeOffset2D(offset) - - if wire.isClosed(): - if not base.isInside(w.Edges[0].Vertexes[0].Point, offset/2, True): - PathLog.track('closed - outside') - return orientWire(w, forward) - PathLog.track('closed - inside') - try: - w = 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) - - # 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 - # Looking for a circle with the start vertex as center marks and end - # starting from there follow the edges until a circle with the end vertex as center is found - # if the traversed edges include any oof the remainig from above, all those edges are remaining - # this is to also include edges which might partially be inside shape - # 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 - - # find edges that are not inside the shape - def isInside(edge): - if shape.Shape.isInside(edge.Vertexes[0].Point, offset/2, True) and shape.Shape.isInside(edge.Vertexes[-1].Point, offset/2, True): - return True - return False - outside = [e for e in 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)]: - 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 - - collectLeft = False - collectRight = False - leftSideEdges = [] - rightSideEdges = [] - - for e in (w.Edges + w.Edges): - if isCircleAt(e, start): - if PathGeom.pointsCoincide(e.Curve.Axis, FreeCAD.Vector(0, 0, 1)): - if not collectLeft and leftSideEdges: - break - collectLeft = True - collectRight = False - else: - if not collectRight and rightSideEdges: - break - collectLeft = False - collectRight = True - elif isCircleAt(e, end): - if PathGeom.pointsCoincide(e.Curve.Axis, FreeCAD.Vector(0, 0, 1)): - if not collectRight and rightSideEdges: - break - collectLeft = False - collectRight = True - else: - if not collectLeft and leftSideEdges: - break - collectLeft = True - collectRight = False - elif collectLeft: - leftSideEdges.append(e) - elif collectRight: - rightSideEdges.append(e) - - 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) - - if not forward: - edges = [PathGeom.flipEdge(edge) for edge in rightSideEdges] - return Part.Wire(edges) - def toolDepthAndOffset(width, extraDepth, tool): '''toolDepthAndOffset(width, extraDepth, tool) ... return tuple for given parameters.''' angle = tool.CuttingEdgeAngle @@ -260,7 +96,7 @@ class ObjectChamfer(PathEngraveBase.ObjectOp): for w in self.adjustWirePlacement(obj, base, basewires): self.adjusted_basewires.append(w) - wire = offsetWire(w, base.Shape, offset, True) + wire = PathGeomOp.offsetWire(w, base.Shape, offset, True) if wire: wires.append(wire) diff --git a/src/Mod/Path/PathScripts/PathGeomOp.py b/src/Mod/Path/PathScripts/PathGeomOp.py new file mode 100644 index 0000000000..c334ead63e --- /dev/null +++ b/src/Mod/Path/PathScripts/PathGeomOp.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2018 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.PathGeom as PathGeom +import PathScripts.PathLog as PathLog + +from PySide import QtCore + +if False: + PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) + PathLog.trackModule(PathLog.thisModule()) +else: + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + +# Qt tanslation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + +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) + 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: + PathLog.track('orientWire - needs flipping') + return PathGeom.flipWire(wire) + PathLog.track('orientWire - ok') + return wire + +def offsetWire(wire, base, offset, forward): + '''offsetWire(wire, base, offset, forward) ... offsets the wire away from base and orients the wire accordingly. + The function tries to avoid most of the pitfalls of Part.makeOffset2D which is possible because all offsetting + happens in the XY plane. + ''' + PathLog.track('offsetWire') + + if 1 == len(wire.Edges): + edge = wire.Edges[0] + curve = edge.Curve + if Part.Circle == type(curve) and wire.isClosed(): + # it's a full circle and there are some problems with that, see + # http://www.freecadweb.org/wiki/Part%20Offset2D + # it's easy to construct them manually though + z = -1 if forward else 1 + edge = Part.makeCircle(curve.Radius + offset, curve.Center, FreeCAD.Vector(0, 0, z)) + if base.isInside(edge.Vertexes[0].Point, offset/2, True): + if offset > curve.Radius or PathGeom.isRoughly(offset, curve.Radius): + # offsetting a hole by its own radius (or more) makes the hole vanish + return None + edge = Part.makeCircle(curve.Radius - offset, curve.Center, FreeCAD.Vector(0, 0, -z)) + w = Part.Wire([edge]) + return w + if Part.Line == type(curve) or Part.LineSegment == type(curve): + # 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)) + o = n.normalize() * offset + edge.translate(o) + 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) + # if we get to this point the assumption is that makeOffset2D can deal with the edge + pass + + w = wire.makeOffset2D(offset) + + if wire.isClosed(): + if not base.isInside(w.Edges[0].Vertexes[0].Point, offset/2, True): + PathLog.track('closed - outside') + return orientWire(w, forward) + PathLog.track('closed - inside') + try: + w = 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) + + # 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 + # Looking for a circle with the start vertex as center marks and end + # starting from there follow the edges until a circle with the end vertex as center is found + # if the traversed edges include any oof the remainig from above, all those edges are remaining + # this is to also include edges which might partially be inside shape + # 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 + + # 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)] + # 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)]: + 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 + + 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): + return PathGeom.pointsCoincide(edge.Curve.Center, center) + return False + + + collectLeft = False + collectRight = False + leftSideEdges = [] + rightSideEdges = [] + + for e in (w.Edges + w.Edges): + if isCircleAt(e, start): + if PathGeom.pointsCoincide(e.Curve.Axis, FreeCAD.Vector(0, 0, 1)): + if not collectLeft and leftSideEdges: + break + collectLeft = True + collectRight = False + else: + if not collectRight and rightSideEdges: + break + collectLeft = False + collectRight = True + elif isCircleAt(e, end): + if PathGeom.pointsCoincide(e.Curve.Axis, FreeCAD.Vector(0, 0, 1)): + if not collectRight and rightSideEdges: + break + collectLeft = False + collectRight = True + else: + if not collectLeft and leftSideEdges: + break + collectLeft = True + collectRight = False + elif collectLeft: + leftSideEdges.append(e) + elif collectRight: + rightSideEdges.append(e) + + 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) + + if not forward: + edges = [PathGeom.flipEdge(edge) for edge in rightSideEdges] + return Part.Wire(edges) + diff --git a/src/Mod/Path/PathTests/TestPathChamfer.py b/src/Mod/Path/PathTests/TestPathChamfer.py index f9721996c3..6cbc794471 100644 --- a/src/Mod/Path/PathTests/TestPathChamfer.py +++ b/src/Mod/Path/PathTests/TestPathChamfer.py @@ -22,452 +22,19 @@ # * * # *************************************************************************** -import FreeCAD -import Part import Path import PathScripts.PathChamfer as PathChamfer -import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathTests.PathTestUtils as PathTestUtils import math -from FreeCAD import Vector - PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) #PathLog.trackModule(PathLog.thisModule()) -def getWire(obj, nr=0): - return obj.Tip.Profile[0].Shape.Wires[nr] - -def getWireInside(obj): - w1 = getWire(obj, 0) - w2 = getWire(obj, 1) - if w2.BoundBox.isInside(w1.BoundBox): - return w1 - return w2 - -def getWireOutside(obj): - w1 = getWire(obj, 0) - w2 = getWire(obj, 1) - if w2.BoundBox.isInside(w1.BoundBox): - return w2 - return w1 - -def getPositiveShape(obj): - return obj.Tool.Shape - -def getNegativeShape(obj): - return obj.Shape - -doc = None -triangle = None -shape = None - -def makeWire(pts): - edges = [] - first = pts[0] - last = pts[0] - for p in pts[1:]: - edges.append(Part.Edge(Part.LineSegment(last, p))) - last = p - edges.append(Part.Edge(Part.LineSegment(last, first))) - return Part.Wire(edges) - class TestPathChamfer(PathTestUtils.PathTestBase): - @classmethod - def setUpClass(cls): - global doc - doc = FreeCAD.openDocument(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_chamfer.fcstd') - - @classmethod - def tearDownClass(cls): - FreeCAD.closeDocument("test_chamfer") - def test00(self): - '''Check that face orientation has anything to do with the wire orientation.''' - pa = Vector(1, 1, 0) - pb = Vector(1, 5, 0) - pc = Vector(5, 5, 0) - pd = Vector(5, 1, 0) - - w = makeWire([pa, pb, pc, pd]) - f = Part.Face(w) - self.assertCoincide(Vector(0, 0, -1), f.Surface.Axis) - - w = makeWire([pa, pd, pc, pb]) - f = Part.Face(w) - self.assertCoincide(Vector(0, 0, +1), f.Surface.Axis) - - def test01(self): - '''Check offsetting a circular hole.''' - obj = doc.getObjectsByLabel('offset-circle')[0] - - small = getWireInside(obj) - self.assertRoughly(10, small.Edges[0].Curve.Radius) - - wire = PathChamfer.offsetWire(small, obj.Shape, 3, True) - self.assertIsNotNone(wire) - self.assertEqual(1, len(wire.Edges)) - self.assertRoughly(7, wire.Edges[0].Curve.Radius) - self.assertCoincide(Vector(0, 0, 1), wire.Edges[0].Curve.Axis) - - wire = PathChamfer.offsetWire(small, obj.Shape, 9.9, True) - self.assertIsNotNone(wire) - self.assertEqual(1, len(wire.Edges)) - self.assertRoughly(0.1, wire.Edges[0].Curve.Radius) - self.assertCoincide(Vector(0, 0, 1), wire.Edges[0].Curve.Axis) - - def test02(self): - '''Check offsetting a circular hole by the radius or more makes the hole vanish.''' - obj = doc.getObjectsByLabel('offset-circle')[0] - - small = getWireInside(obj) - self.assertRoughly(10, small.Edges[0].Curve.Radius) - wire = PathChamfer.offsetWire(small, obj.Shape, 10, True) - self.assertIsNone(wire) - - wire = PathChamfer.offsetWire(small, obj.Shape, 15, True) - self.assertIsNone(wire) - - def test03(self): - '''Check offsetting a cylinder succeeds.''' - obj = doc.getObjectsByLabel('offset-circle')[0] - - big = getWireOutside(obj) - self.assertRoughly(20, big.Edges[0].Curve.Radius) - - wire = PathChamfer.offsetWire(big, obj.Shape, 10, True) - self.assertIsNotNone(wire) - self.assertEqual(1, len(wire.Edges)) - self.assertRoughly(30, wire.Edges[0].Curve.Radius) - self.assertCoincide(Vector(0, 0, -1), wire.Edges[0].Curve.Axis) - - wire = PathChamfer.offsetWire(big, obj.Shape, 20, True) - self.assertIsNotNone(wire) - self.assertEqual(1, len(wire.Edges)) - self.assertRoughly(40, wire.Edges[0].Curve.Radius) - self.assertCoincide(Vector(0, 0, -1), wire.Edges[0].Curve.Axis) - - def test04(self): - '''Check offsetting a hole with Placement.''' - obj = doc.getObjectsByLabel('offset-placement')[0] - - wires = [w for w in obj.Shape.Wires if 1 == len(w.Edges) and PathGeom.isRoughly(0, w.Edges[0].Vertexes[0].Point.z)] - self.assertEqual(2, len(wires)) - w = wires[1] if wires[0].BoundBox.isInside(wires[1].BoundBox) else wires[0] - - self.assertRoughly(10, w.Edges[0].Curve.Radius) - # make sure there is a placement and I didn't mess up the model - self.assertFalse(PathGeom.pointsCoincide(Vector(), w.Edges[0].Placement.Base)) - - wire = PathChamfer.offsetWire(w, obj.Shape, 2, True) - self.assertIsNotNone(wire) - self.assertEqual(1, len(wire.Edges)) - self.assertRoughly(8, wire.Edges[0].Curve.Radius) - self.assertCoincide(Vector(0, 0, 0), wire.Edges[0].Curve.Center) - self.assertCoincide(Vector(0, 0, 1), wire.Edges[0].Curve.Axis) - - def test05(self): - '''Check offsetting a cylinder with Placement.''' - obj = doc.getObjectsByLabel('offset-placement')[0] - - wires = [w for w in obj.Shape.Wires if 1 == len(w.Edges) and PathGeom.isRoughly(0, w.Edges[0].Vertexes[0].Point.z)] - self.assertEqual(2, len(wires)) - w = wires[0] if wires[0].BoundBox.isInside(wires[1].BoundBox) else wires[1] - - self.assertRoughly(20, w.Edges[0].Curve.Radius) - # make sure there is a placement and I didn't mess up the model - self.assertFalse(PathGeom.pointsCoincide(Vector(), w.Edges[0].Placement.Base)) - - wire = PathChamfer.offsetWire(w, obj.Shape, 2, True) - self.assertIsNotNone(wire) - self.assertEqual(1, len(wire.Edges)) - self.assertRoughly(22, wire.Edges[0].Curve.Radius) - self.assertCoincide(Vector(0, 0, 0), wire.Edges[0].Curve.Center) - self.assertCoincide(Vector(0, 0, -1), wire.Edges[0].Curve.Axis) - - def test10(self): - '''Check offsetting hole wire succeeds.''' - obj = doc.getObjectsByLabel('offset-edge')[0] - - small = getWireInside(obj) - # sanity check - y = 10 - x = 10 * math.cos(math.pi/6) - self.assertLines(small.Edges, False, [Vector(0, y, 0), Vector(-x, -y/2, 0), Vector(x, -y/2, 0), Vector(0, y, 0)]) - - wire = PathChamfer.offsetWire(small, obj.Shape, 3, True) - self.assertIsNotNone(wire) - self.assertEqual(3, len(wire.Edges)) - self.assertTrue(wire.isClosed()) - y = 4 # offset works in both directions - x = 4 * math.cos(math.pi/6) - self.assertLines(wire.Edges, False, [Vector(0, 4, 0), Vector(-x, -2, 0), Vector(x, -2, 0), Vector(0, 4, 0)]) - f = Part.Face(wire) - self.assertCoincide(Vector(0, 0, 1), f.Surface.Axis) - - def test11(self): - '''Check offsetting hole wire for more than it's size makes hole vanish.''' - obj = doc.getObjectsByLabel('offset-edge')[0] - - small = getWireInside(obj) - # sanity check - y = 10 - x = 10 * math.cos(math.pi/6) - self.assertLines(small.Edges, False, [Vector(0, y, 0), Vector(-x, -y/2, 0), Vector(x, -y/2, 0), Vector(0, y, 0)]) - wire = PathChamfer.offsetWire(small, obj.Shape, 5, True) - self.assertIsNone(wire) - - def test12(self): - '''Check offsetting a body wire succeeds.''' - obj = doc.getObjectsByLabel('offset-edge')[0] - - big = getWireOutside(obj) - # sanity check - y = 20 - x = 20 * math.cos(math.pi/6) - self.assertLines(big.Edges, False, [Vector(0, y, 0), Vector(-x, -y/2, 0), Vector(x, -y/2, 0), Vector(0, y, 0)]) - - wire = PathChamfer.offsetWire(big, obj.Shape, 5, True) - self.assertIsNotNone(wire) - self.assertEqual(6, len(wire.Edges)) - lastAngle = None - refAngle = math.pi / 3 - for e in wire.Edges: - if Part.Circle == type(e.Curve): - self.assertRoughly(5, e.Curve.Radius) - self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) - else: - self.assertRoughly(34.641, e.Length, 0.001) - begin = e.Vertexes[0].Point - end = e.Vertexes[1].Point - v = end - begin - angle = PathGeom.getAngle(v) - if PathGeom.isRoughly(0, angle) or PathGeom.isRoughly(math.pi, math.fabs(angle)): - if lastAngle: - self.assertRoughly(-refAngle, lastAngle) - elif PathGeom.isRoughly(+refAngle, angle): - if lastAngle: - self.assertRoughly(math.pi, math.fabs(lastAngle)) - elif PathGeom.isRoughly(-refAngle, angle): - if lastAngle: - self.assertRoughly(+refAngle, lastAngle) - else: - self.assertIsNone("%s: angle=%s" % (type(e.Curve), angle)) - lastAngle = angle - f = Part.Face(wire) - self.assertCoincide(Vector(0, 0, -1), f.Surface.Axis) - - def test21(self): - '''Check offsetting a cylinder.''' - obj = doc.getObjectsByLabel('circle-cut')[0] - - wire = PathChamfer.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True) - self.assertEqual(1, len(wire.Edges)) - edge = wire.Edges[0] - self.assertCoincide(Vector(), edge.Curve.Center) - self.assertCoincide(Vector(0, 0, -1), edge.Curve.Axis) - self.assertRoughly(33, edge.Curve.Radius) - - # the other way around everything's the same except the axis is negative - wire = PathChamfer.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, False) - self.assertEqual(1, len(wire.Edges)) - edge = wire.Edges[0] - self.assertCoincide(Vector(), edge.Curve.Center) - self.assertCoincide(Vector(0, 0, +1), edge.Curve.Axis) - self.assertRoughly(33, edge.Curve.Radius) - - - def test22(self): - '''Check offsetting a box.''' - obj = doc.getObjectsByLabel('square-cut')[0] - - wire = PathChamfer.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True) - self.assertEqual(8, len(wire.Edges)) - self.assertEqual(4, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) - self.assertEqual(4, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) - for e in wire.Edges: - if Part.Line == type(e.Curve): - if PathGeom.isRoughly(e.Vertexes[0].Point.x, e.Vertexes[1].Point.x): - self.assertEqual(40, e.Length) - if PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y): - self.assertEqual(60, e.Length) - if Part.Circle == type(e.Curve): - self.assertRoughly(3, e.Curve.Radius) - self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) - - # change offset orientation - wire = PathChamfer.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, False) - self.assertEqual(8, len(wire.Edges)) - self.assertEqual(4, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) - self.assertEqual(4, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) - for e in wire.Edges: - if Part.Line == type(e.Curve): - if PathGeom.isRoughly(e.Vertexes[0].Point.x, e.Vertexes[1].Point.x): - self.assertEqual(40, e.Length) - if PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y): - self.assertEqual(60, e.Length) - if Part.Circle == type(e.Curve): - self.assertRoughly(3, e.Curve.Radius) - self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) - - - def test23(self): - '''Check offsetting a triangle.''' - obj = doc.getObjectsByLabel('triangle-cut')[0] - - wire = PathChamfer.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True) - self.assertEqual(6, len(wire.Edges)) - self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) - self.assertEqual(3, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) - length = 60 * math.sin(math.radians(60)) - for e in wire.Edges: - if Part.Line == type(e.Curve): - self.assertRoughly(length, e.Length) - if Part.Circle == type(e.Curve): - self.assertRoughly(3, e.Curve.Radius) - self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) - - # change offset orientation - wire = PathChamfer.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, False) - self.assertEqual(6, len(wire.Edges)) - self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) - self.assertEqual(3, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) - for e in wire.Edges: - if Part.Line == type(e.Curve): - self.assertRoughly(length, e.Length) - if Part.Circle == type(e.Curve): - self.assertRoughly(3, e.Curve.Radius) - self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) - - def test24(self): - '''Check offsetting a shape.''' - obj = doc.getObjectsByLabel('shape-cut')[0] - - wire = PathChamfer.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True) - self.assertEqual(6, len(wire.Edges)) - self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) - self.assertEqual(3, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) - length = 40 - radius = 20 + 3 - for e in wire.Edges: - if Part.Line == type(e.Curve): - self.assertRoughly(length, e.Length) - if Part.Circle == type(e.Curve): - self.assertRoughly(radius, e.Curve.Radius) - self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) - - # change offset orientation - wire = PathChamfer.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, False) - self.assertEqual(6, len(wire.Edges)) - self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) - self.assertEqual(3, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) - for e in wire.Edges: - if Part.Line == type(e.Curve): - self.assertRoughly(length, e.Length) - if Part.Circle == type(e.Curve): - self.assertRoughly(radius, e.Curve.Radius) - self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) - - def test25(self): - '''Check offsetting a cylindrical hole.''' - obj = doc.getObjectsByLabel('circle-cut')[0] - - wire = PathChamfer.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True) - self.assertEqual(1, len(wire.Edges)) - edge = wire.Edges[0] - self.assertCoincide(Vector(), edge.Curve.Center) - self.assertCoincide(Vector(0, 0, +1), edge.Curve.Axis) - self.assertRoughly(27, edge.Curve.Radius) - - # the other way around everything's the same except the axis is negative - wire = PathChamfer.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, False) - self.assertEqual(1, len(wire.Edges)) - edge = wire.Edges[0] - self.assertCoincide(Vector(), edge.Curve.Center) - self.assertCoincide(Vector(0, 0, -1), edge.Curve.Axis) - self.assertRoughly(27, edge.Curve.Radius) - - - def test26(self): - '''Check offsetting a square hole.''' - obj = doc.getObjectsByLabel('square-cut')[0] - - wire = PathChamfer.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True) - self.assertEqual(4, len(wire.Edges)) - self.assertEqual(4, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) - for e in wire.Edges: - if PathGeom.isRoughly(e.Vertexes[0].Point.x, e.Vertexes[1].Point.x): - self.assertRoughly(34, e.Length) - if PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y): - self.assertRoughly(54, e.Length) - - # change offset orientation - wire = PathChamfer.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, False) - self.assertEqual(4, len(wire.Edges)) - self.assertEqual(4, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) - for e in wire.Edges: - if PathGeom.isRoughly(e.Vertexes[0].Point.x, e.Vertexes[1].Point.x): - self.assertRoughly(34, e.Length) - if PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y): - self.assertRoughly(54, e.Length) - - - def test27(self): - '''Check offsetting a triangular holee.''' - obj = doc.getObjectsByLabel('triangle-cut')[0] - - wire = PathChamfer.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True) - self.assertEqual(3, len(wire.Edges)) - self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) - length = 48 * math.sin(math.radians(60)) - for e in wire.Edges: - self.assertRoughly(length, e.Length) - f = Part.Face(wire) - self.assertCoincide(Vector(0, 0, +1), f.Surface.Axis) - - # change offset orientation - wire = PathChamfer.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, False) - self.assertEqual(3, len(wire.Edges)) - self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) - for e in wire.Edges: - self.assertRoughly(length, e.Length) - f = Part.Face(wire) - self.assertCoincide(Vector(0, 0, -1), f.Surface.Axis) - - def test28(self): - '''Check offsetting a shape hole.''' - obj = doc.getObjectsByLabel('shape-cut')[0] - - wire = PathChamfer.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True) - self.assertEqual(6, len(wire.Edges)) - self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) - self.assertEqual(3, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) - length = 40 - radius = 20 - 3 - for e in wire.Edges: - if Part.Line == type(e.Curve): - self.assertRoughly(length, e.Length) - if Part.Circle == type(e.Curve): - self.assertRoughly(radius, e.Curve.Radius) - self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) - - # change offset orientation - wire = PathChamfer.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, False) - self.assertEqual(6, len(wire.Edges)) - self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) - self.assertEqual(3, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) - for e in wire.Edges: - if Part.Line == type(e.Curve): - self.assertRoughly(length, e.Length) - if Part.Circle == type(e.Curve): - self.assertRoughly(radius, e.Curve.Radius) - self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) - - - def test50(self): '''Verify chamfer depth and offset for an end mill.''' tool = Path.Tool() tool.Diameter = 20 @@ -485,7 +52,7 @@ class TestPathChamfer(PathTestUtils.PathTestBase): self.assertRoughly(0.01, depth) self.assertRoughly(9, offset) - def test51(self): + def test01(self): '''Verify chamfer depth and offset for a 90° v-bit.''' tool = Path.Tool() tool.FlatRadius = 0 @@ -499,7 +66,7 @@ class TestPathChamfer(PathTestUtils.PathTestBase): self.assertRoughly(1.2, depth) self.assertRoughly(0.2, offset) - def test52(self): + def test02(self): '''Verify chamfer depth and offset for a 90° v-bit with non 0 flat radius.''' tool = Path.Tool() tool.FlatRadius = 0.3 @@ -513,7 +80,7 @@ class TestPathChamfer(PathTestUtils.PathTestBase): self.assertRoughly(2.2, depth) self.assertRoughly(0.5, offset) - def test53(self): + def test03(self): '''Verify chamfer depth and offset for a 60° v-bit with non 0 flat radius.''' tool = Path.Tool() tool.FlatRadius = 10 diff --git a/src/Mod/Path/PathTests/TestPathGeomOp.py b/src/Mod/Path/PathTests/TestPathGeomOp.py new file mode 100644 index 0000000000..5432b9a5d9 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathGeomOp.py @@ -0,0 +1,470 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2018 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.PathGeom as PathGeom +import PathScripts.PathGeomOp as PathGeomOp +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +import math + +from FreeCAD import Vector + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) + +def getWire(obj, nr=0): + return obj.Tip.Profile[0].Shape.Wires[nr] + +def getWireInside(obj): + w1 = getWire(obj, 0) + w2 = getWire(obj, 1) + if w2.BoundBox.isInside(w1.BoundBox): + return w1 + return w2 + +def getWireOutside(obj): + w1 = getWire(obj, 0) + w2 = getWire(obj, 1) + if w2.BoundBox.isInside(w1.BoundBox): + return w2 + return w1 + +def getPositiveShape(obj): + return obj.Tool.Shape + +def getNegativeShape(obj): + return obj.Shape + +doc = None +triangle = None +shape = None + +def makeWire(pts): + edges = [] + first = pts[0] + last = pts[0] + for p in pts[1:]: + edges.append(Part.Edge(Part.LineSegment(last, p))) + last = p + edges.append(Part.Edge(Part.LineSegment(last, first))) + return Part.Wire(edges) + + +class TestPathGeomOp(PathTestUtils.PathTestBase): + + @classmethod + def setUpClass(cls): + global doc + doc = FreeCAD.openDocument(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_geomop.fcstd') + + @classmethod + def tearDownClass(cls): + FreeCAD.closeDocument("test_geomop") + + def test00(self): + '''Check that face orientation has anything to do with the wire orientation.''' + pa = Vector(1, 1, 0) + pb = Vector(1, 5, 0) + pc = Vector(5, 5, 0) + pd = Vector(5, 1, 0) + + w = makeWire([pa, pb, pc, pd]) + f = Part.Face(w) + self.assertCoincide(Vector(0, 0, -1), f.Surface.Axis) + + w = makeWire([pa, pd, pc, pb]) + f = Part.Face(w) + self.assertCoincide(Vector(0, 0, +1), f.Surface.Axis) + + def test01(self): + '''Check offsetting a circular hole.''' + obj = doc.getObjectsByLabel('offset-circle')[0] + + small = getWireInside(obj) + self.assertRoughly(10, small.Edges[0].Curve.Radius) + + wire = PathGeomOp.offsetWire(small, obj.Shape, 3, True) + self.assertIsNotNone(wire) + self.assertEqual(1, len(wire.Edges)) + self.assertRoughly(7, wire.Edges[0].Curve.Radius) + self.assertCoincide(Vector(0, 0, 1), wire.Edges[0].Curve.Axis) + + wire = PathGeomOp.offsetWire(small, obj.Shape, 9.9, True) + self.assertIsNotNone(wire) + self.assertEqual(1, len(wire.Edges)) + self.assertRoughly(0.1, wire.Edges[0].Curve.Radius) + self.assertCoincide(Vector(0, 0, 1), wire.Edges[0].Curve.Axis) + + def test02(self): + '''Check offsetting a circular hole by the radius or more makes the hole vanish.''' + obj = doc.getObjectsByLabel('offset-circle')[0] + + small = getWireInside(obj) + self.assertRoughly(10, small.Edges[0].Curve.Radius) + wire = PathGeomOp.offsetWire(small, obj.Shape, 10, True) + self.assertIsNone(wire) + + wire = PathGeomOp.offsetWire(small, obj.Shape, 15, True) + self.assertIsNone(wire) + + def test03(self): + '''Check offsetting a cylinder succeeds.''' + obj = doc.getObjectsByLabel('offset-circle')[0] + + big = getWireOutside(obj) + self.assertRoughly(20, big.Edges[0].Curve.Radius) + + wire = PathGeomOp.offsetWire(big, obj.Shape, 10, True) + self.assertIsNotNone(wire) + self.assertEqual(1, len(wire.Edges)) + self.assertRoughly(30, wire.Edges[0].Curve.Radius) + self.assertCoincide(Vector(0, 0, -1), wire.Edges[0].Curve.Axis) + + wire = PathGeomOp.offsetWire(big, obj.Shape, 20, True) + self.assertIsNotNone(wire) + self.assertEqual(1, len(wire.Edges)) + self.assertRoughly(40, wire.Edges[0].Curve.Radius) + self.assertCoincide(Vector(0, 0, -1), wire.Edges[0].Curve.Axis) + + def test04(self): + '''Check offsetting a hole with Placement.''' + obj = doc.getObjectsByLabel('offset-placement')[0] + + wires = [w for w in obj.Shape.Wires if 1 == len(w.Edges) and PathGeom.isRoughly(0, w.Edges[0].Vertexes[0].Point.z)] + self.assertEqual(2, len(wires)) + w = wires[1] if wires[0].BoundBox.isInside(wires[1].BoundBox) else wires[0] + + self.assertRoughly(10, w.Edges[0].Curve.Radius) + # make sure there is a placement and I didn't mess up the model + self.assertFalse(PathGeom.pointsCoincide(Vector(), w.Edges[0].Placement.Base)) + + wire = PathGeomOp.offsetWire(w, obj.Shape, 2, True) + self.assertIsNotNone(wire) + self.assertEqual(1, len(wire.Edges)) + self.assertRoughly(8, wire.Edges[0].Curve.Radius) + self.assertCoincide(Vector(0, 0, 0), wire.Edges[0].Curve.Center) + self.assertCoincide(Vector(0, 0, 1), wire.Edges[0].Curve.Axis) + + def test05(self): + '''Check offsetting a cylinder with Placement.''' + obj = doc.getObjectsByLabel('offset-placement')[0] + + wires = [w for w in obj.Shape.Wires if 1 == len(w.Edges) and PathGeom.isRoughly(0, w.Edges[0].Vertexes[0].Point.z)] + self.assertEqual(2, len(wires)) + w = wires[0] if wires[0].BoundBox.isInside(wires[1].BoundBox) else wires[1] + + self.assertRoughly(20, w.Edges[0].Curve.Radius) + # make sure there is a placement and I didn't mess up the model + self.assertFalse(PathGeom.pointsCoincide(Vector(), w.Edges[0].Placement.Base)) + + wire = PathGeomOp.offsetWire(w, obj.Shape, 2, True) + self.assertIsNotNone(wire) + self.assertEqual(1, len(wire.Edges)) + self.assertRoughly(22, wire.Edges[0].Curve.Radius) + self.assertCoincide(Vector(0, 0, 0), wire.Edges[0].Curve.Center) + self.assertCoincide(Vector(0, 0, -1), wire.Edges[0].Curve.Axis) + + def test10(self): + '''Check offsetting hole wire succeeds.''' + obj = doc.getObjectsByLabel('offset-edge')[0] + + small = getWireInside(obj) + # sanity check + y = 10 + x = 10 * math.cos(math.pi/6) + self.assertLines(small.Edges, False, [Vector(0, y, 0), Vector(-x, -y/2, 0), Vector(x, -y/2, 0), Vector(0, y, 0)]) + + wire = PathGeomOp.offsetWire(small, obj.Shape, 3, True) + self.assertIsNotNone(wire) + self.assertEqual(3, len(wire.Edges)) + self.assertTrue(wire.isClosed()) + y = 4 # offset works in both directions + x = 4 * math.cos(math.pi/6) + self.assertLines(wire.Edges, False, [Vector(0, 4, 0), Vector(-x, -2, 0), Vector(x, -2, 0), Vector(0, 4, 0)]) + f = Part.Face(wire) + self.assertCoincide(Vector(0, 0, 1), f.Surface.Axis) + + def test11(self): + '''Check offsetting hole wire for more than it's size makes hole vanish.''' + obj = doc.getObjectsByLabel('offset-edge')[0] + + small = getWireInside(obj) + # sanity check + y = 10 + x = 10 * math.cos(math.pi/6) + self.assertLines(small.Edges, False, [Vector(0, y, 0), Vector(-x, -y/2, 0), Vector(x, -y/2, 0), Vector(0, y, 0)]) + wire = PathGeomOp.offsetWire(small, obj.Shape, 5, True) + self.assertIsNone(wire) + + def test12(self): + '''Check offsetting a body wire succeeds.''' + obj = doc.getObjectsByLabel('offset-edge')[0] + + big = getWireOutside(obj) + # sanity check + y = 20 + x = 20 * math.cos(math.pi/6) + self.assertLines(big.Edges, False, [Vector(0, y, 0), Vector(-x, -y/2, 0), Vector(x, -y/2, 0), Vector(0, y, 0)]) + + wire = PathGeomOp.offsetWire(big, obj.Shape, 5, True) + self.assertIsNotNone(wire) + self.assertEqual(6, len(wire.Edges)) + lastAngle = None + refAngle = math.pi / 3 + for e in wire.Edges: + if Part.Circle == type(e.Curve): + self.assertRoughly(5, e.Curve.Radius) + self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) + else: + self.assertRoughly(34.641, e.Length, 0.001) + begin = e.Vertexes[0].Point + end = e.Vertexes[1].Point + v = end - begin + angle = PathGeom.getAngle(v) + if PathGeom.isRoughly(0, angle) or PathGeom.isRoughly(math.pi, math.fabs(angle)): + if lastAngle: + self.assertRoughly(-refAngle, lastAngle) + elif PathGeom.isRoughly(+refAngle, angle): + if lastAngle: + self.assertRoughly(math.pi, math.fabs(lastAngle)) + elif PathGeom.isRoughly(-refAngle, angle): + if lastAngle: + self.assertRoughly(+refAngle, lastAngle) + else: + self.assertIsNone("%s: angle=%s" % (type(e.Curve), angle)) + lastAngle = angle + f = Part.Face(wire) + self.assertCoincide(Vector(0, 0, -1), f.Surface.Axis) + + def test21(self): + '''Check offsetting a cylinder.''' + obj = doc.getObjectsByLabel('circle-cut')[0] + + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True) + self.assertEqual(1, len(wire.Edges)) + edge = wire.Edges[0] + self.assertCoincide(Vector(), edge.Curve.Center) + self.assertCoincide(Vector(0, 0, -1), edge.Curve.Axis) + self.assertRoughly(33, edge.Curve.Radius) + + # the other way around everything's the same except the axis is negative + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, False) + self.assertEqual(1, len(wire.Edges)) + edge = wire.Edges[0] + self.assertCoincide(Vector(), edge.Curve.Center) + self.assertCoincide(Vector(0, 0, +1), edge.Curve.Axis) + self.assertRoughly(33, edge.Curve.Radius) + + + def test22(self): + '''Check offsetting a box.''' + obj = doc.getObjectsByLabel('square-cut')[0] + + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True) + self.assertEqual(8, len(wire.Edges)) + self.assertEqual(4, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) + self.assertEqual(4, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) + for e in wire.Edges: + if Part.Line == type(e.Curve): + if PathGeom.isRoughly(e.Vertexes[0].Point.x, e.Vertexes[1].Point.x): + self.assertEqual(40, e.Length) + if PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y): + self.assertEqual(60, e.Length) + if Part.Circle == type(e.Curve): + self.assertRoughly(3, e.Curve.Radius) + self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) + + # change offset orientation + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, False) + self.assertEqual(8, len(wire.Edges)) + self.assertEqual(4, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) + self.assertEqual(4, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) + for e in wire.Edges: + if Part.Line == type(e.Curve): + if PathGeom.isRoughly(e.Vertexes[0].Point.x, e.Vertexes[1].Point.x): + self.assertEqual(40, e.Length) + if PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y): + self.assertEqual(60, e.Length) + if Part.Circle == type(e.Curve): + self.assertRoughly(3, e.Curve.Radius) + self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) + + + def test23(self): + '''Check offsetting a triangle.''' + obj = doc.getObjectsByLabel('triangle-cut')[0] + + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True) + self.assertEqual(6, len(wire.Edges)) + self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) + self.assertEqual(3, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) + length = 60 * math.sin(math.radians(60)) + for e in wire.Edges: + if Part.Line == type(e.Curve): + self.assertRoughly(length, e.Length) + if Part.Circle == type(e.Curve): + self.assertRoughly(3, e.Curve.Radius) + self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) + + # change offset orientation + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, False) + self.assertEqual(6, len(wire.Edges)) + self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) + self.assertEqual(3, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) + for e in wire.Edges: + if Part.Line == type(e.Curve): + self.assertRoughly(length, e.Length) + if Part.Circle == type(e.Curve): + self.assertRoughly(3, e.Curve.Radius) + self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) + + def test24(self): + '''Check offsetting a shape.''' + obj = doc.getObjectsByLabel('shape-cut')[0] + + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, True) + self.assertEqual(6, len(wire.Edges)) + self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) + self.assertEqual(3, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) + length = 40 + radius = 20 + 3 + for e in wire.Edges: + if Part.Line == type(e.Curve): + self.assertRoughly(length, e.Length) + if Part.Circle == type(e.Curve): + self.assertRoughly(radius, e.Curve.Radius) + self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) + + # change offset orientation + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, False) + self.assertEqual(6, len(wire.Edges)) + self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) + self.assertEqual(3, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) + for e in wire.Edges: + if Part.Line == type(e.Curve): + self.assertRoughly(length, e.Length) + if Part.Circle == type(e.Curve): + self.assertRoughly(radius, e.Curve.Radius) + self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) + + def test25(self): + '''Check offsetting a cylindrical hole.''' + obj = doc.getObjectsByLabel('circle-cut')[0] + + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True) + self.assertEqual(1, len(wire.Edges)) + edge = wire.Edges[0] + self.assertCoincide(Vector(), edge.Curve.Center) + self.assertCoincide(Vector(0, 0, +1), edge.Curve.Axis) + self.assertRoughly(27, edge.Curve.Radius) + + # the other way around everything's the same except the axis is negative + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, False) + self.assertEqual(1, len(wire.Edges)) + edge = wire.Edges[0] + self.assertCoincide(Vector(), edge.Curve.Center) + self.assertCoincide(Vector(0, 0, -1), edge.Curve.Axis) + self.assertRoughly(27, edge.Curve.Radius) + + + def test26(self): + '''Check offsetting a square hole.''' + obj = doc.getObjectsByLabel('square-cut')[0] + + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True) + self.assertEqual(4, len(wire.Edges)) + self.assertEqual(4, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) + for e in wire.Edges: + if PathGeom.isRoughly(e.Vertexes[0].Point.x, e.Vertexes[1].Point.x): + self.assertRoughly(34, e.Length) + if PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y): + self.assertRoughly(54, e.Length) + + # change offset orientation + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, False) + self.assertEqual(4, len(wire.Edges)) + self.assertEqual(4, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) + for e in wire.Edges: + if PathGeom.isRoughly(e.Vertexes[0].Point.x, e.Vertexes[1].Point.x): + self.assertRoughly(34, e.Length) + if PathGeom.isRoughly(e.Vertexes[0].Point.y, e.Vertexes[1].Point.y): + self.assertRoughly(54, e.Length) + + + def test27(self): + '''Check offsetting a triangular holee.''' + obj = doc.getObjectsByLabel('triangle-cut')[0] + + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True) + self.assertEqual(3, len(wire.Edges)) + self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) + length = 48 * math.sin(math.radians(60)) + for e in wire.Edges: + self.assertRoughly(length, e.Length) + f = Part.Face(wire) + self.assertCoincide(Vector(0, 0, +1), f.Surface.Axis) + + # change offset orientation + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, False) + self.assertEqual(3, len(wire.Edges)) + self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) + for e in wire.Edges: + self.assertRoughly(length, e.Length) + f = Part.Face(wire) + self.assertCoincide(Vector(0, 0, -1), f.Surface.Axis) + + def test28(self): + '''Check offsetting a shape hole.''' + obj = doc.getObjectsByLabel('shape-cut')[0] + + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, True) + self.assertEqual(6, len(wire.Edges)) + self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) + self.assertEqual(3, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) + length = 40 + radius = 20 - 3 + for e in wire.Edges: + if Part.Line == type(e.Curve): + self.assertRoughly(length, e.Length) + if Part.Circle == type(e.Curve): + self.assertRoughly(radius, e.Curve.Radius) + self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) + + # change offset orientation + wire = PathGeomOp.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, False) + self.assertEqual(6, len(wire.Edges)) + self.assertEqual(3, len([e for e in wire.Edges if Part.Line == type(e.Curve)])) + self.assertEqual(3, len([e for e in wire.Edges if Part.Circle == type(e.Curve)])) + for e in wire.Edges: + if Part.Line == type(e.Curve): + self.assertRoughly(length, e.Length) + if Part.Circle == type(e.Curve): + self.assertRoughly(radius, e.Curve.Radius) + self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) + + + diff --git a/src/Mod/Path/PathTests/test_chamfer.fcstd b/src/Mod/Path/PathTests/test_geomop.fcstd similarity index 100% rename from src/Mod/Path/PathTests/test_chamfer.fcstd rename to src/Mod/Path/PathTests/test_geomop.fcstd diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 6026e4a934..29fd2ab73a 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -28,6 +28,7 @@ from PathTests.TestPathLog import TestPathLog from PathTests.TestPathCore import TestPathCore #from PathTests.TestPathPost import PathPostTestCases from PathTests.TestPathGeom import TestPathGeom +from PathTests.TestPathGeomOp import TestPathGeomOp from PathTests.TestPathUtil import TestPathUtil from PathTests.TestPathDepthParams import depthTestCases from PathTests.TestPathDressupHoldingTags import TestHoldingTags