diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 134d79ff45..f231f3fbc5 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -116,10 +116,8 @@ SET(PathScripts_post_SRCS SET(PathTests_SRCS PathTests/__init__.py - PathTests/boxtest.fcstd PathTests/PathTestUtils.py - PathTests/test_centroid_00.ngc - PathTests/test_linuxcnc_00.ngc + PathTests/TestPathChamfer.py PathTests/TestPathCore.py PathTests/TestPathDepthParams.py PathTests/TestPathDressupDogbone.py @@ -133,6 +131,10 @@ SET(PathTests_SRCS PathTests/TestPathToolController.py PathTests/TestPathTooltable.py PathTests/TestPathUtil.py + PathTests/boxtest.fcstd + PathTests/test_centroid_00.ngc + PathTests/test_chamfer.fcstd + PathTests/test_linuxcnc_00.ngc ) SET(PathImages_Ops diff --git a/src/Mod/Path/PathScripts/PathChamfer.py b/src/Mod/Path/PathScripts/PathChamfer.py index 59c5964cc5..4a83691585 100644 --- a/src/Mod/Path/PathScripts/PathChamfer.py +++ b/src/Mod/Path/PathScripts/PathChamfer.py @@ -26,11 +26,11 @@ import FreeCAD import Part import Path import PathScripts.PathEngraveBase as PathEngraveBase +import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import math -from PathScripts.PathGeom import PathGeom from PySide import QtCore if False: @@ -43,7 +43,32 @@ else: def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) -def removeInsideEdges(edges, shape, offset): +def orientWire(w, forward=True): + face = Part.Face(w) + cw = 0 < face.Surface.Axis.z + if forward != cw: + PathLog.track('orientWire - needs flipping') + return PathGeom.flipWire(w) + PathLog.track('orientWire - ok') + return w + +def isCircleAt(edge, 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): + PathLog.track('offsetWire') + + 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') + w = wire.makeOffset2D(-offset) + 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 @@ -52,48 +77,66 @@ def removeInsideEdges(edges, shape, offset): # 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 - remaining = [e for e in edges if not isInside(e)] - # of the ones remaining, the first and the last are the end offsets - allFirst = [e.firstVertex().Point for e in remaining] - allLast = [e.lastVertex().Point for e in remaining] - first = [f for f in allFirst if not f in allLast][0] - last = [l for l in allLast if not l in allFirst][0] - #return [e for e in remaining if not PathGeom.pointsCoincide(e.firstVertex().Point, first) and not PathGeom.pointsCoincide(e.lastVertex().Point, last)] - return remaining + 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 -def orientWireForClimbMilling(w): - face = Part.Face(w) - cw = 'Forward' == obj.ToolController.SpindleDir - wcw = 0 < face.Surface.Axis.z - if cw != wcw: - PathLog.track('flip wire') - # This works because Path creation will flip the edges accordingly - return Part.Wire([e for e in reversed(w.Edges)]) - PathLog.track('no flip', cw, wcw) - return w + # find the start and end point + start = wire.Vertexes[0].Point + end = wire.Vertexes[-1].Point -def offsetWire(obj, wire, base, offset): - PathLog.track(obj.Label) + collectLeft = False + collectRight = False + leftSideEdges = [] + rightSideEdges = [] - w = wire.makeOffset2D(offset) - if wire.isClosed(): - if not base.Shape.isInside(w.Edges[0].Vertexes[0].Point, offset/2, True): - return orientWireForClimbMilling(w) - w = wire.makeOffset2D(-offset) - return orientWireForClimbMilling(w) + 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 = removeInsideEdges(w.Edges, base, offset) - if not edges: - w = wire.makeOffset2D(-offset) - edges = removeInsideEdges(w.Edges, base, offset) - points = [] - for e in edges: - points - # determine the start point + 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) class ObjectChamfer(PathEngraveBase.ObjectOp): @@ -138,7 +181,7 @@ class ObjectChamfer(PathEngraveBase.ObjectOp): basewires.append(Part.Wire(edgelist)) for w in self.adjustWirePlacement(obj, base, basewires): - wires.append(offsetWire(obj, w, base, offset)) + wires.append(offsetWire(obj, w, base.Shape, offset)) self.wires = wires self.buildpathocc(obj, wires, [depth], True) diff --git a/src/Mod/Path/PathScripts/PathEngraveBase.py b/src/Mod/Path/PathScripts/PathEngraveBase.py index ef11b9ec6d..1678891f69 100644 --- a/src/Mod/Path/PathScripts/PathEngraveBase.py +++ b/src/Mod/Path/PathScripts/PathEngraveBase.py @@ -26,12 +26,12 @@ import DraftGeomUtils import FreeCAD import Part import Path +import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils import copy -from PathScripts.PathGeom import PathGeom from PySide import QtCore __doc__ = "Base class for all ops in the engrave family." diff --git a/src/Mod/Path/PathScripts/PathGeom.py b/src/Mod/Path/PathScripts/PathGeom.py index f0ce52fab2..1645931cd0 100644 --- a/src/Mod/Path/PathScripts/PathGeom.py +++ b/src/Mod/Path/PathScripts/PathGeom.py @@ -465,7 +465,17 @@ def flipEdge(edge): elif Part.Line == type(edge.Curve) or Part.LineSegment == type(edge.Curve): return Part.Edge(Part.LineSegment(edge.Vertexes[-1].Point, edge.Vertexes[0].Point)) elif Part.Circle == type(edge.Curve): - return Part.makeCircle(edge.Curve.Radius, edge.Curve.Center, -edge.Curve.Axis, -math.degrees(edge.LastParameter), -math.degrees(edge.FirstParameter)) + r = edge.Curve.Radius + c = edge.Curve.Center + d = edge.Curve.Axis + a = math.degrees(edge.Curve.AngleXU) + f = math.degrees(edge.FirstParameter) + l = math.degrees(edge.LastParameter) + if 0 > d.z: + a = a + 180 + PathLog.track(r, c, d, a, f, l) + arc = Part.makeCircle(r, c, -d, -l-a, -f-a) + return arc elif Part.BSplineCurve == type(edge.Curve): spline = edge.Curve @@ -491,3 +501,8 @@ def flipEdge(edge): return Part.Edge(flipped) +def flipWire(wire): + '''Flip the entire wire and all its edges so it is being processed the other way around.''' + edges = [flipEdge(e) for e in wire.Edges] + edges.reverse() + return Part.Wire(edges) diff --git a/src/Mod/Path/PathTests/TestPathChamfer.py b/src/Mod/Path/PathTests/TestPathChamfer.py new file mode 100644 index 0000000000..2e12d4339a --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathChamfer.py @@ -0,0 +1,113 @@ +# -*- 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.PathChamfer as PathChamfer +import PathScripts.PathGeom as PathGeom +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils + +from FreeCAD import Vector + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) + +def getWire(obj): + return obj.Tool.Tip.Profile[0].Shape.Wires[0] + +def getPositiveShape(obj): + return obj.Tool.Shape + +def getNegativeShape(obj): + return obj.Shape + +class TestPathChamfer(PathTestUtils.PathTestBase): + + def setUp(self): + self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_chamfer.fcstd') + self.circle = self.doc.getObjectsByLabel('circle-cut')[0] + self.square = self.doc.getObjectsByLabel('square-cut')[0] + self.triangle = self.doc.getObjectsByLabel('triangle-cut')[0] + self.shape = self.doc.getObjectsByLabel('shape-cut')[0] + + def tearDown(self): + FreeCAD.closeDocument("test_chamfer") + + def test01(self): + '''Check offsetting a cylinder.''' + obj = self.circle + + wire = PathChamfer.offsetWire(getWire(obj), 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), 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 test02(self): + '''Check offsetting a box.''' + obj = self.square + + wire = PathChamfer.offsetWire(getWire(obj), 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) + # As it turns out the arcs are oriented the wrong way + self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) + + + wire = PathChamfer.offsetWire(getWire(obj), 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) + # As it turns out the arcs are oriented the wrong way + self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) + diff --git a/src/Mod/Path/PathTests/TestPathGeom.py b/src/Mod/Path/PathTests/TestPathGeom.py index 9a2dd4e86f..d4d961c70c 100644 --- a/src/Mod/Path/PathTests/TestPathGeom.py +++ b/src/Mod/Path/PathTests/TestPathGeom.py @@ -427,6 +427,12 @@ class TestPathGeom(PathTestBase): edge = Part.makeCircle(3, Vector(1, 3, 2), Vector(0, 0, -1), 300, 340) self.assertEdgeShapesMatch(edge, PathGeom.flipEdge(edge)) + def test74(self): + '''Flip a rotated arc''' + # oh yes ... + edge = Part.makeCircle(3, Vector(1, 3, 2), Vector(0, 0, 1), 45, 90) + edge.rotate(edge.Curve.Center, Vector(0, 0, 1), -90) + self.assertEdgeShapesMatch(edge, PathGeom.flipEdge(edge)) def test75(self): '''Flip a b-spline''' @@ -440,3 +446,4 @@ class TestPathGeom(PathTestBase): + diff --git a/src/Mod/Path/PathTests/test_chamfer.fcstd b/src/Mod/Path/PathTests/test_chamfer.fcstd new file mode 100644 index 0000000000..2d09689fa8 Binary files /dev/null and b/src/Mod/Path/PathTests/test_chamfer.fcstd differ diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index ecf3a25d6e..6026e4a934 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -37,4 +37,5 @@ from PathTests.TestPathTool import TestPathTool from PathTests.TestPathTooltable import TestPathTooltable from PathTests.TestPathToolController import TestPathToolController from PathTests.TestPathSetupSheet import TestPathSetupSheet +from PathTests.TestPathChamfer import TestPathChamfer