From ebc1190d8b588ec4e8ca4d631ca93bd0be82cbae Mon Sep 17 00:00:00 2001 From: mlampert Date: Wed, 2 Nov 2022 13:25:09 -0700 Subject: [PATCH] PATH: Feature/dogbone ii (#7660) * Start of new dogbone dressup * Added Instruction and tangents support for G2/3 moves * Added Maneuver class to represent a set of moves and process them coherently * Created kinks and verify their creation. * Added dogbone detection and verification * Simplified gcode strings * Added horizontal t-bones generation * Added support for vertical t-bone * Consolidated t-bone creation * Added support for pathLength * Added support for tbone on short edge * Added support for long edges * Added support for dogbones * Fixed dogbone for non-horizontal lead-in * Horizontal bone adaptive length tests * Fixed dogbone angle and adaptive length * Some code cleanup * Added adaptive length tests for dogbones * Split base data classes into their own PathLanguage module. * Splitting dogboneII implementation into its constituents * Moved adaptive length into DogbonII module * Separate dogboneII generator test cases and changed interface to allow for dynamic length calculations * Unit tests for length calculation * Initial DogboneII unit test * Unit tests and fixes for plunge move handling * Unit tests for the remaining styles and incision strategies * Basic DogboneII gui * Added support for markers * Better color and selection scheme for markers * Cleaned up import statements * Added DogboneII to Path WB init * Support for dogbone on dogbone and fixed t-bone generation * Fixed t-bone on short leg bones * Fixed tbone on short edge when short edge is m1 * Fixed t-bone on long edge for m0/m1 and CW/CCW * Removed redundant code * Removed redundant 'Dress-up' from menu entries * black code formatting * added generator to cmake * Fixed typos --- src/Mod/Path/CMakeLists.txt | 13 +- src/Mod/Path/Path/Base/Generator/dogboneII.py | 220 ++++++ src/Mod/Path/Path/Base/Generator/drill.py | 4 +- src/Mod/Path/Path/Base/Generator/rotation.py | 4 +- .../Path/Path/Base/Gui/PreferencesAdvanced.py | 4 +- src/Mod/Path/Path/Base/Gui/Util.py | 4 +- src/Mod/Path/Path/Base/Language.py | 269 ++++++++ .../Path/Path/Base/SetupSheetOpPrototype.py | 16 +- src/Mod/Path/Path/Dressup/DogboneII.py | 368 ++++++++++ src/Mod/Path/Path/Dressup/Gui/AxisMap.py | 2 +- src/Mod/Path/Path/Dressup/Gui/Boundary.py | 7 +- src/Mod/Path/Path/Dressup/Gui/Dogbone.py | 19 +- src/Mod/Path/Path/Dressup/Gui/DogboneII.py | 378 +++++++++++ src/Mod/Path/Path/Dressup/Gui/Dragknife.py | 4 +- src/Mod/Path/Path/Dressup/Gui/LeadInOut.py | 6 +- src/Mod/Path/Path/Dressup/Gui/RampEntry.py | 8 +- src/Mod/Path/Path/Dressup/Gui/Tags.py | 9 +- src/Mod/Path/Path/Dressup/Gui/ZCorrect.py | 9 +- src/Mod/Path/Path/Geom.py | 9 + src/Mod/Path/Path/GuiInit.py | 1 + src/Mod/Path/Path/Main/Gui/Inspect.py | 13 +- src/Mod/Path/Path/Main/Gui/PreferencesJob.py | 4 +- src/Mod/Path/Path/Main/Stock.py | 1 + src/Mod/Path/Path/Op/Area.py | 2 +- src/Mod/Path/Path/Op/Drilling.py | 4 +- src/Mod/Path/Path/Op/EngraveBase.py | 4 +- src/Mod/Path/Path/Op/FeatureExtension.py | 4 +- src/Mod/Path/Path/Op/Gui/Base.py | 8 +- src/Mod/Path/Path/Op/Gui/FeatureExtension.py | 4 +- src/Mod/Path/Path/Op/Gui/SimpleCopy.py | 4 +- src/Mod/Path/Path/Op/Gui/ThreadMilling.py | 18 +- src/Mod/Path/Path/Op/Profile.py | 8 +- src/Mod/Path/Path/Op/SurfaceSupport.py | 4 +- src/Mod/Path/Path/Op/ThreadMilling.py | 10 +- src/Mod/Path/Path/Post/Utils.py | 7 +- src/Mod/Path/Path/Post/UtilsArguments.py | 9 +- src/Mod/Path/Path/Post/UtilsParse.py | 50 +- src/Mod/Path/Path/Post/scripts/dxf_post.py | 1 - .../Post/scripts/refactored_centroid_post.py | 4 +- .../Path/Post/scripts/refactored_grbl_post.py | 4 +- .../Post/scripts/refactored_linuxcnc_post.py | 4 +- .../scripts/refactored_mach3_mach4_post.py | 4 +- .../Path/Post/scripts/refactored_test_post.py | 4 +- src/Mod/Path/Path/Preferences.py | 1 - src/Mod/Path/Path/Tool/Controller.py | 14 +- src/Mod/Path/Path/Tool/Gui/BitCmd.py | 8 +- src/Mod/Path/Path/Tool/Gui/BitLibrary.py | 2 +- src/Mod/Path/Path/Tool/Gui/Controller.py | 8 +- src/Mod/Path/PathCommands.py | 6 +- src/Mod/Path/PathScripts/PathUtils.py | 1 + src/Mod/Path/PathScripts/PathUtilsGui.py | 4 +- src/Mod/Path/PathTests/TestPathDeburr.py | 1 + .../PathTests/TestPathDressupDogboneII.py | 640 ++++++++++++++++++ src/Mod/Path/PathTests/TestPathDrillable.py | 68 +- .../PathTests/TestPathGeneratorDogboneII.py | 354 ++++++++++ src/Mod/Path/PathTests/TestPathGeom.py | 22 +- src/Mod/Path/PathTests/TestPathHelpers.py | 5 +- src/Mod/Path/PathTests/TestPathLanguage.py | 124 ++++ src/Mod/Path/PathTests/TestPathOpUtil.py | 32 +- src/Mod/Path/PathTests/TestPathPreferences.py | 8 +- src/Mod/Path/PathTests/TestPathSetupSheet.py | 28 +- .../Path/PathTests/TestPathThreadMilling.py | 43 +- .../Path/PathTests/TestRefactoredTestPost.py | 56 +- src/Mod/Path/TestPathApp.py | 6 + 64 files changed, 2708 insertions(+), 252 deletions(-) create mode 100644 src/Mod/Path/Path/Base/Generator/dogboneII.py create mode 100644 src/Mod/Path/Path/Base/Language.py create mode 100644 src/Mod/Path/Path/Dressup/DogboneII.py create mode 100644 src/Mod/Path/Path/Dressup/Gui/DogboneII.py create mode 100644 src/Mod/Path/PathTests/TestPathDressupDogboneII.py create mode 100644 src/Mod/Path/PathTests/TestPathGeneratorDogboneII.py create mode 100644 src/Mod/Path/PathTests/TestPathLanguage.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 6383353c35..94d250bdf7 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -33,9 +33,10 @@ SET(PathPython_SRCS SET(PathPythonBase_SRCS Path/Base/__init__.py - Path/Base//Drillable.py - Path/Base/MachineState.py + Path/Base/Drillable.py Path/Base/FeedRate.py + Path/Base/Language.py + Path/Base/MachineState.py Path/Base/Property.py Path/Base/PropertyBag.py Path/Base/SetupSheet.py @@ -59,6 +60,7 @@ SET(PathPythonDressup_SRCS Path/Dressup/__init__.py Path/Dressup/Utils.py Path/Dressup/Boundary.py + Path/Dressup/DogboneII.py Path/Dressup/Tags.py ) @@ -66,6 +68,7 @@ SET(PathPythonDressupGui_SRCS Path/Dressup/Gui/__init__.py Path/Dressup/Gui/AxisMap.py Path/Dressup/Gui/Dogbone.py + Path/Dressup/Gui/DogboneII.py Path/Dressup/Gui/Dragknife.py Path/Dressup/Gui/LeadInOut.py Path/Dressup/Gui/Boundary.py @@ -222,6 +225,7 @@ SET(PathScripts_SRCS ) SET(PathPythonBaseGenerator_SRCS + Path/Base/Generator/dogboneII.py Path/Base/Generator/drill.py Path/Base/Generator/helix.py Path/Base/Generator/rotation.py @@ -287,19 +291,22 @@ SET(PathTests_SRCS PathTests/TestPathDeburr.py PathTests/TestPathDepthParams.py PathTests/TestPathDressupDogbone.py + PathTests/TestPathDressupDogboneII.py PathTests/TestPathDressupHoldingTags.py PathTests/TestPathDrillGenerator.py PathTests/TestPathDrillable.py - PathTests/TestPathRotationGenerator.py + PathTests/TestPathGeneratorDogboneII.py PathTests/TestPathGeom.py PathTests/TestPathHelix.py PathTests/TestPathHelpers.py PathTests/TestPathHelixGenerator.py + PathTests/TestPathLanguage.py PathTests/TestPathLog.py PathTests/TestPathOpUtil.py PathTests/TestPathPost.py PathTests/TestPathPreferences.py PathTests/TestPathPropertyBag.py + PathTests/TestPathRotationGenerator.py PathTests/TestPathSetupSheet.py PathTests/TestPathStock.py PathTests/TestPathToolChangeGenerator.py diff --git a/src/Mod/Path/Path/Base/Generator/dogboneII.py b/src/Mod/Path/Path/Base/Generator/dogboneII.py new file mode 100644 index 0000000000..ca21480755 --- /dev/null +++ b/src/Mod/Path/Path/Base/Generator/dogboneII.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 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 Path +import Path.Base.Language as PathLanguage +import math + +# Path.Log.trackModule(Path.Log.thisModule()) + +PI = math.pi + + +class Kink(object): + """A Kink represents the angle at which two moves connect. + A positive kink angle represents a move to the left, and a negative angle represents a move to the right.""" + + def __init__(self, m0, m1): + if m1 is None: + m1 = m0[1] + m0 = m0[0] + self.m0 = m0 + self.m1 = m1 + self.t0 = m0.anglesOfTangents()[1] + self.t1 = m1.anglesOfTangents()[0] + if Path.Geom.isRoughly(self.t0, self.t1): + self.defl = 0 + else: + self.defl = Path.Geom.normalizeAngle(self.t1 - self.t0) + + def isKink(self): + return self.defl != 0 + + def goesLeft(self): + return self.defl > 0 + + def goesRight(self): + return self.defl < 0 + + def deflection(self): + """deflection() ... returns the tangential difference of the two edges at their intersection""" + return self.defl + + def normAngle(self): + """normAngle() ... returns the angle opposite between the two tangents""" + + # The normal angle is perpendicular to the "average tangent" of the kink. The question + # is into which direction to turn. One lies in the center between the two edges and the + # other is opposite to that. As it turns out, the magnitude of the tangents tell it all. + if self.t0 > self.t1: + return Path.Geom.normalizeAngle((self.t0 + self.t1 + math.pi) / 2) + return Path.Geom.normalizeAngle((self.t0 + self.t1 - math.pi) / 2) + + def position(self): + """position() ... position of the edge's intersection""" + return self.m0.positionEnd() + + def x(self): + return self.position().x + + def y(self): + return self.position().y + + def __repr__(self): + return f"({self.x():.4f}, {self.y():.4f})[t0={180*self.t0/math.pi:.2f}, t1={180*self.t1/math.pi:.2f}, deflection={180*self.defl/math.pi:.2f}, normAngle={180*self.normAngle()/math.pi:.2f}]" + + +class Bone(object): + """A Bone holds all the information of a bone and the kink it is attached to""" + + def __init__(self, kink, angle, length, instr=None): + self.kink = kink + self.angle = angle + self.length = length + self.instr = [] if instr is None else instr + + def addInstruction(self, instr): + self.instr.append(instr) + + def position(self): + """pos() ... return the position of the bone""" + return self.kink.position() + + def tip(self): + """tip() ... return the tip of the bone.""" + dx = abs(self.length) * math.cos(self.angle) + dy = abs(self.length) * math.sin(self.angle) + return self.position() + FreeCAD.Vector(dx, dy, 0) + + +def kink_to_path(kink, g0=False): + return Path.Path( + [PathLanguage.instruction_to_command(instr) for instr in [kink.m0, kink.m1]] + ) + + +def bone_to_path(bone, g0=False): + kink = bone.kink + cmds = [] + if g0 and not Path.Geom.pointsCoincide( + kink.m0.positionBegin(), FreeCAD.Vector(0, 0, 0) + ): + pos = kink.m0.positionBegin() + param = {} + if not Path.Geom.isRoughly(pos.x, 0): + param["X"] = pos.x + if not Path.Geom.isRoughly(pos.y, 0): + param["Y"] = pos.y + cmds.append(Path.Command("G0", param)) + for instr in [kink.m0, bone.instr[0], bone.instr[1], kink.m1]: + cmds.append(PathLanguage.instruction_to_command(instr)) + return Path.Path(cmds) + + +def generate_bone(kink, length, angle): + dx = length * math.cos(angle) + dy = length * math.sin(angle) + p0 = kink.position() + + if Path.Geom.isRoughly(0, dx): + # vertical bone + moveIn = PathLanguage.MoveStraight(kink.position(), "G1", {"Y": p0.y + dy}) + moveOut = PathLanguage.MoveStraight(moveIn.positionEnd(), "G1", {"Y": p0.y}) + elif Path.Geom.isRoughly(0, dy): + # horizontal bone + moveIn = PathLanguage.MoveStraight(kink.position(), "G1", {"X": p0.x + dx}) + moveOut = PathLanguage.MoveStraight(moveIn.positionEnd(), "G1", {"X": p0.x}) + else: + moveIn = PathLanguage.MoveStraight( + kink.position(), "G1", {"X": p0.x + dx, "Y": p0.y + dy} + ) + moveOut = PathLanguage.MoveStraight( + moveIn.positionEnd(), "G1", {"X": p0.x, "Y": p0.y} + ) + + return Bone(kink, angle, length, [moveIn, moveOut]) + + +class Generator(object): + def __init__(self, calc_length, nominal_length, custom_length): + self.calc_length = calc_length + self.nominal_length = nominal_length + self.custom_length = custom_length + + def length(self, kink, angle): + return self.calc_length(kink, angle, self.nominal_length, self.custom_length) + + def generate_func(self): + return generate_bone + + def generate(self, kink): + angle = self.angle(kink) + return self.generate_func()(kink, self.length(kink, angle), angle) + + +class GeneratorTBoneHorizontal(Generator): + def angle(self, kink): + if abs(kink.normAngle()) > (PI / 2): + return -PI + else: + return 0 + + +class GeneratorTBoneVertical(Generator): + def angle(self, kink): + if kink.normAngle() > 0: + return PI / 2 + else: + return -PI / 2 + + +class GeneratorTBoneOnShort(Generator): + def angle(self, kink): + rot = PI / 2 if kink.goesRight() else -PI / 2 + + if kink.m0.pathLength() < kink.m1.pathLength(): + return Path.Geom.normalizeAngle(kink.t0 + rot) + else: + return Path.Geom.normalizeAngle(kink.t1 + rot) + + +class GeneratorTBoneOnLong(Generator): + def angle(self, kink): + rot = PI / 2 if kink.goesRight() else -PI / 2 + + if kink.m0.pathLength() > kink.m1.pathLength(): + return Path.Geom.normalizeAngle(kink.t0 + rot) + else: + return Path.Geom.normalizeAngle(kink.t1 + rot) + + +class GeneratorDogbone(Generator): + def angle(self, kink): + return kink.normAngle() + + +def generate(kink, generator, calc_length, nominal_length, custom_length=None): + if custom_length is None: + custom_length = nominal_length + gen = generator(calc_length, nominal_length, custom_length) + return gen.generate(kink) diff --git a/src/Mod/Path/Path/Base/Generator/drill.py b/src/Mod/Path/Path/Base/Generator/drill.py index 72ebda1b3c..926f413f8f 100644 --- a/src/Mod/Path/Path/Base/Generator/drill.py +++ b/src/Mod/Path/Path/Base/Generator/drill.py @@ -37,7 +37,9 @@ else: Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) -def generate(edge, dwelltime=0.0, peckdepth=0.0, repeat=1, retractheight=None, chipBreak=False): +def generate( + edge, dwelltime=0.0, peckdepth=0.0, repeat=1, retractheight=None, chipBreak=False +): """ Generates Gcode for drilling a single hole. diff --git a/src/Mod/Path/Path/Base/Generator/rotation.py b/src/Mod/Path/Path/Base/Generator/rotation.py index 27518a7f87..6df8640eae 100644 --- a/src/Mod/Path/Path/Base/Generator/rotation.py +++ b/src/Mod/Path/Path/Base/Generator/rotation.py @@ -82,7 +82,9 @@ def __getCRotation(normalVector, cMin=-360, cMax=360): with either the +y or -y axis. multiple poses may be possible. Returns a list of all valid poses """ - Path.Log.debug("normalVector: {} cMin: {} cMax: {}".format(normalVector, cMin, cMax)) + Path.Log.debug( + "normalVector: {} cMin: {} cMax: {}".format(normalVector, cMin, cMax) + ) angle = relAngle(normalVector, refAxis.y) diff --git a/src/Mod/Path/Path/Base/Gui/PreferencesAdvanced.py b/src/Mod/Path/Path/Base/Gui/PreferencesAdvanced.py index bb1725f483..92d85a6386 100644 --- a/src/Mod/Path/Path/Base/Gui/PreferencesAdvanced.py +++ b/src/Mod/Path/Path/Base/Gui/PreferencesAdvanced.py @@ -63,7 +63,9 @@ class AdvancedPreferencesPage: self.form.WarningSuppressOpenCamLib.setChecked( Path.Preferences.suppressOpenCamLibWarning() ) - self.form.WarningSuppressVelocity.setChecked(Path.Preferences.suppressVelocity()) + self.form.WarningSuppressVelocity.setChecked( + Path.Preferences.suppressVelocity() + ) self.updateSelection() def updateSelection(self, state=None): diff --git a/src/Mod/Path/Path/Base/Gui/Util.py b/src/Mod/Path/Path/Base/Gui/Util.py index 320ac6092f..5516a20b51 100644 --- a/src/Mod/Path/Path/Base/Gui/Util.py +++ b/src/Mod/Path/Path/Base/Gui/Util.py @@ -160,7 +160,9 @@ class QuantitySpinBox(QtCore.QObject): self.widget.setProperty("binding", "%s.%s" % (obj.Name, prop)) self.valid = True else: - Path.Log.warning("Cannot find property {} of {}".format(prop, obj.Label)) + Path.Log.warning( + "Cannot find property {} of {}".format(prop, obj.Label) + ) self.valid = False else: self.valid = False diff --git a/src/Mod/Path/Path/Base/Language.py b/src/Mod/Path/Path/Base/Language.py new file mode 100644 index 0000000000..11bdc59099 --- /dev/null +++ b/src/Mod/Path/Path/Base/Language.py @@ -0,0 +1,269 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 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 Path +import math + +__title__ = "PathLanguage - classes for an internal language/representation for Path" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Functions to extract and convert between Path.Command and Part.Edge and utility functions to reason about them." + +CmdMoveStraight = Path.Geom.CmdMoveStraight + Path.Geom.CmdMoveRapid + + +class Instruction(object): + """An Instruction is a pure python replacement of Path.Command which also tracks its begin position.""" + + def __init__(self, begin, cmd, param=None): + self.begin = begin + if type(cmd) == Path.Command: + self.cmd = Path.Name + self.param = Path.Parameters + else: + self.cmd = cmd + if param is None: + self.param = {} + else: + self.param = param + + def anglesOfTangents(self): + return (0, 0) + + def setPositionBegin(self, begin): + self.begin = begin + + def positionBegin(self): + """positionBegin() ... returns a Vector of the begin position""" + return self.begin + + def positionEnd(self): + """positionEnd() ... returns a Vector of the end position""" + return FreeCAD.Vector( + self.x(self.begin.x), self.y(self.begin.y), self.z(self.begin.z) + ) + + def pathLength(self): + """pathLength() ... returns the length in mm""" + return 0 + + def isMove(self): + return False + + def isPlunge(self): + """isPlunge() ... return true if this moves up or down""" + return self.isMove() and not Path.Geom.isRoughly( + self.begin.z, self.z(self.begin.z) + ) + + def leadsInto(self, instr): + """leadsInto(instr) ... return true if instr is a continuation of self""" + return Path.Geom.pointsCoincide(self.positionEnd(), instr.positionBegin()) + + def x(self, default=0): + return self.param.get("X", default) + + def y(self, default=0): + return self.param.get("Y", default) + + def z(self, default=0): + return self.param.get("Z", default) + + def i(self, default=0): + return self.param.get("I", default) + + def j(self, default=0): + return self.param.get("J", default) + + def k(self, default=0): + return self.param.get("K", default) + + def xyBegin(self): + """xyBegin() ... internal convenience function""" + return FreeCAD.Vector(self.begin.x, self.begin.y, 0) + + def xyEnd(self): + """xyEnd() ... internal convenience function""" + return FreeCAD.Vector(self.x(self.begin.x), self.y(self.begin.y), 0) + + def __repr__(self): + return f"{self.cmd}{self.param}" + + def str(self, digits=2): + if digits == 0: + s = [f"{k}: {int(v)}" for k, v in self.param.items()] + else: + fmt = f"{{}}: {{:.{digits}}}" + s = [fmt.format(k, v) for k, v in self.param.items()] + return f"{self.cmd}{{{', '.join(s)}}}" + + +class MoveStraight(Instruction): + def anglesOfTangents(self): + """anglesOfTangents() ... return a tuple with the tangent angles at begin and end position""" + begin = self.xyBegin() + end = self.xyEnd() + if end == begin: + return (0, 0) + a = Path.Geom.getAngle(end - begin) + return (a, a) + + def isMove(self): + return True + + def pathLength(self): + return (self.positionEnd() - self.positionBegin()).Length + + +class MoveArc(Instruction): + def anglesOfTangents(self): + """anglesOfTangents() ... return a tuple with the tangent angles at begin and end position""" + begin = self.xyBegin() + end = self.xyEnd() + center = self.xyCenter() + # calculate angle of the hypotenuse at begin and end + s0 = Path.Geom.getAngle(begin - center) + s1 = Path.Geom.getAngle(end - center) + # the tangents are perpendicular to the hypotenuse with the sign determined by the + # direction of the arc + return ( + Path.Geom.normalizeAngle(s0 + self.arcDirection()), + Path.Geom.normalizeAngle(s1 + self.arcDirection()), + ) + + def isMove(self): + return True + + def isArc(self): + return True + + def isCW(self): + return self.arcDirection() < 0 + + def isCCW(self): + return self.arcDirection() > 0 + + def arcAngle(self): + """arcAngle() ... return the angle of the arc opening""" + begin = self.xyBegin() + end = self.xyEnd() + center = self.xyCenter() + s0 = Path.Geom.getAngle(begin - center) + s1 = Path.Geom.getAngle(end - center) + + if self.isCW(): + while s0 < s1: + s0 = s0 + 2 * math.pi + return s0 - s1 + + # CCW + while s1 < s0: + s1 = s1 + 2 * math.pi + return s1 - s0 + + def arcRadius(self): + """arcRadius() ... return the radius""" + return (self.xyBegin() - self.xyCenter()).Length + + def pathLength(self): + return self.arcAngle() * self.arcRadius() + + def xyCenter(self): + return FreeCAD.Vector(self.begin.x + self.i(), self.begin.y + self.j(), 0) + + +class MoveArcCW(MoveArc): + def arcDirection(self): + return -math.pi / 2 + + +class MoveArcCCW(MoveArc): + def arcDirection(self): + return math.pi / 2 + + +class Maneuver(object): + """A series of instructions and moves""" + + def __init__(self, begin=None, instr=None): + self.instr = instr if instr else [] + self.setPositionBegin(begin if begin else FreeCAD.Vector(0, 0, 0)) + + def setPositionBegin(self, begin): + self.begin = begin + for i in self.instr: + i.setPositionBegin(begin) + begin = i.positionEnd() + + def positionBegin(self): + return self.begin + + def getMoves(self): + return [instr for instr in self.instr if instr.isMove()] + + def addInstruction(self, instr): + self.instr.append(instr) + + def addInstructions(self, coll): + self.instr.extend(coll) + + def toPath(self): + return Path.Path([instruction_to_command(instr) for instr in self.instr]) + + def __repr__(self): + if self.instr: + return "\n".join([str(i) for i in self.instr]) + return "" + + @classmethod + def InstructionFromCommand(cls, cmd, begin=None): + if not begin: + begin = FreeCAD.Vector(0, 0, 0) + + if cmd.Name in CmdMoveStraight: + return MoveStraight(begin, cmd.Name, cmd.Parameters) + if cmd.Name in Path.Geom.CmdMoveCW: + return MoveArcCW(begin, cmd.Name, cmd.Parameters) + if cmd.Name in Path.Geom.CmdMoveCCW: + return MoveArcCCW(begin, cmd.Name, cmd.Parameters) + return Instruction(begin, cmd.Name, cmd.Parameters) + + @classmethod + def FromPath(cls, path, begin=None): + maneuver = Maneuver(begin) + instr = [] + begin = maneuver.positionBegin() + for cmd in path.Commands: + i = cls.InstructionFromCommand(cmd, begin) + instr.append(i) + begin = i.positionEnd() + maneuver.instr = instr + return maneuver + + @classmethod + def FromGCode(cls, gcode, begin=None): + return cls.FromPath(Path.Path(gcode), begin) + + +def instruction_to_command(instr): + return Path.Command(instr.cmd, instr.param) diff --git a/src/Mod/Path/Path/Base/SetupSheetOpPrototype.py b/src/Mod/Path/Path/Base/SetupSheetOpPrototype.py index 1634d5a1f6..4bb8060c77 100644 --- a/src/Mod/Path/Path/Base/SetupSheetOpPrototype.py +++ b/src/Mod/Path/Path/Base/SetupSheetOpPrototype.py @@ -65,11 +65,11 @@ class Property(object): def setupProperty(self, obj, name, category, value): created = False if not hasattr(obj, name): - Path.Log.track('add', obj.Name, name, self.propType) + Path.Log.track("add", obj.Name, name, self.propType) obj.addProperty(self.propType, name, category, self.info) self.initProperty(obj, name) created = True - Path.Log.track('set', obj.Name, name, value, type(value)) + Path.Log.track("set", obj.Name, name, value, type(value)) setattr(obj, name, value) return created @@ -130,7 +130,9 @@ class PropertyFloat(Property): try: return float(string) except ValueError: - Path.Log.error(f"{self.category}.{self.name} [{self.propType}] : '{string}'") + Path.Log.error( + f"{self.category}.{self.name} [{self.propType}] : '{string}'" + ) raise @@ -142,7 +144,9 @@ class PropertyInteger(Property): try: return int(string) except ValueError: - Path.Log.error(f"{self.category}.{self.name} [{self.propType}] : '{string}'") + Path.Log.error( + f"{self.category}.{self.name} [{self.propType}] : '{string}'" + ) raise @@ -159,7 +163,9 @@ class PropertyBool(Property): try: return bool(string) except ValueError: - Path.Log.error(f"{self.category}.{self.name} [{self.propType}] : '{string}'") + Path.Log.error( + f"{self.category}.{self.name} [{self.propType}] : '{string}'" + ) raise diff --git a/src/Mod/Path/Path/Dressup/DogboneII.py b/src/Mod/Path/Path/Dressup/DogboneII.py new file mode 100644 index 0000000000..f4fa0243f6 --- /dev/null +++ b/src/Mod/Path/Path/Dressup/DogboneII.py @@ -0,0 +1,368 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 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 * +# * * +# *************************************************************************** + +from PySide.QtCore import QT_TRANSLATE_NOOP +import FreeCAD +import Path +import Path.Base.Generator.dogboneII as dogboneII +import Path.Base.Language as PathLanguage +import math + +if False: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) + +PI = math.pi + + +def calc_length_adaptive(kink, angle, nominal_length, custom_length): + Path.Log.track(kink, angle, nominal_length, custom_length) + + if Path.Geom.isRoughly(abs(kink.deflection()), 0): + return 0 + + # If the kink poses a 180deg turn the adaptive length is undefined. Mathematically + # it's infinite but that is not practical. + # We define the adaptive length to be the nominal length for this case. + if Path.Geom.isRoughly(abs(kink.deflection()), PI): + return nominal_length + + # The distance of the (estimated) corner from the kink position depends only on the + # deflection of the kink. + # Some sample values to build up intuition: + # deflection : dog bone : norm distance : calc + # ----------------:-------------:---------------:-------------- + # 0 : -PI/2 : 1 + # PI/6 : -5*PI/12 : 1.03528 : 1/cos( (pi/6) / 2) + # PI/4 : -3*PI/8 : 1.08239 : 1/cos( (pi/4) / 2) + # PI/3 : -PI/3 : 1.1547 : 1/cos( (pi/3) / 2) + # PI/2 : -PI/4 : 1.41421 : 1/cos( (pi/2) / 2) + # 2*PI/3 : -PI/6 : 2 : 1/cos((2*pi/3) / 2) + # 3*PI/4 : -PI/8 : 2.61313 : 1/cos((3*pi/4) / 2) + # 5*PI/6 : -PI/12 : 3.8637 : 1/cos((5*pi/6) / 2) + # PI : 0 : nan <-- see above + # The last column can be geometrically derived or found by experimentation. + dist = nominal_length / math.cos(kink.deflection() / 2) + + # The depth of the bone depends on the direction of the bone in relation to the + # direction of the corner. If the direction is identical then the depth is the same + # as the distance of the corner minus the nominal_length (which corresponds to the + # radius of the tool). + # If the corner's direction is PI/4 off the bone angle the intersecion of the tool + # with the corner is the projection of the corner onto the bone. + # If the corner's direction is perpendicular to the bone's angle there is, strictly + # speaking no intersection and the bone is ineffective. However, giving it our + # best shot we should probably move the entire depth. + + da = Path.Geom.normalizeAngle(kink.normAngle() - angle) + depth = dist * math.cos(da) + if depth < 0: + Path.Log.debug( + f"depth={depth:4f}: kink={kink}, angle={180*angle/PI}, dist={dist:.4f}, da={180*da/PI} -> depth=0.0" + ) + depth = 0 + else: + height = dist * abs(math.sin(da)) + if height < nominal_length: + depth = depth - math.sqrt(nominal_length * nominal_length - height * height) + Path.Log.debug( + f"{kink}: angle={180*angle/PI}, dist={dist:.4f}, da={180*da/PI}, depth={depth:.4f}" + ) + + return depth + + +def calc_length_nominal(kink, angle, nominal_length, custom_length): + return nominal_length + + +def calc_length_custom(kink, angle, nominal_length, custom_length): + return custom_length + + +class Style(object): + """Style - enumeration class for the supported bone styles""" + + Dogbone = "Dogbone" + Tbone_H = "T-bone horizontal" + Tbone_V = "T-bone vertical" + Tbone_L = "T-bone long edge" + Tbone_S = "T-bone short edge" + All = [Dogbone, Tbone_H, Tbone_V, Tbone_L, Tbone_S] + + Generator = { + Dogbone: dogboneII.GeneratorDogbone, + Tbone_H: dogboneII.GeneratorTBoneHorizontal, + Tbone_V: dogboneII.GeneratorTBoneVertical, + Tbone_S: dogboneII.GeneratorTBoneOnShort, + Tbone_L: dogboneII.GeneratorTBoneOnLong, + } + + +class Side(object): + """Side - enumeration class for the side of the path to attach bones""" + + Left = "Left" + Right = "Right" + All = [Left, Right] + + @classmethod + def oppositeOf(cls, side): + if side == cls.Left: + return cls.Right + if side == cls.Right: + return cls.Left + return None + + +class Incision(object): + """Incision - enumeration class for the different depths of bone incision""" + + Fixed = "fixed" + Adaptive = "adaptive" + Custom = "custom" + All = [Adaptive, Fixed, Custom] + + Calc = { + Fixed: calc_length_nominal, + Adaptive: calc_length_adaptive, + Custom: calc_length_custom, + } + + +def insertBone(obj, kink): + """insertBone(kink, side) - return True if a bone should be inserted into the kink""" + if not kink.isKink(): + Path.Log.debug(f"not a kink") + return False + + if obj.Side == Side.Right and kink.goesRight(): + return False + if obj.Side == Side.Left and kink.goesLeft(): + return False + return True + + +class BoneState(object): + def __init__(self, bone, nr, enabled=True): + self.bone = bone + self.bones = {nr: bone} + self.enabled = enabled + pos = bone.position() + self.pos = FreeCAD.Vector(pos.x, pos.y, 0) + + def isEnabled(self): + return self.enabled + + def addBone(self, bone, nr): + self.bones[nr] = bone + + def position(self): + return self.pos + + def boneTip(self): + return self.bone.tip() + + def boneIDs(self): + return list(sorted(self.bones.keys())) + + def zLevels(self): + return list(sorted([bone.position().z for bone in self.bones.values()])) + + def length(self): + return self.bone.length + + +class Proxy(object): + def __init__(self, obj, base): + obj.addProperty( + "App::PropertyLink", + "Base", + "Base", + QT_TRANSLATE_NOOP("App::Property", "The base path to dress up"), + ) + obj.Base = base + + obj.addProperty( + "App::PropertyEnumeration", + "Side", + "Dressup", + QT_TRANSLATE_NOOP("App::Property", "The side of path to insert bones"), + ) + obj.Side = Side.All + if hasattr(base, "BoneBlacklist"): + obj.Side = base.Side + else: + side = Side.Right + if hasattr(obj.Base, "Side") and obj.Base.Side == "Inside": + side = Side.Left + if hasattr(obj.Base, "Direction") and obj.Base.Direction == "CCW": + side = Side.oppositeOf(side) + obj.Side = side + + obj.addProperty( + "App::PropertyEnumeration", + "Style", + "Dressup", + QT_TRANSLATE_NOOP("App::Property", "The style of bones"), + ) + obj.Style = Style.All + obj.Style = Style.Dogbone + + obj.addProperty( + "App::PropertyEnumeration", + "Incision", + "Dressup", + QT_TRANSLATE_NOOP( + "App::Property", "The algorithm to determine the bone length" + ), + ) + obj.Incision = Incision.All + obj.Incision = Incision.Adaptive + + obj.addProperty( + "App::PropertyLength", + "Custom", + "Dressup", + QT_TRANSLATE_NOOP( + "App::Property", "Dressup length if Incision == Incision.Custom" + ), + ) + obj.Custom = 0.0 + + obj.addProperty( + "App::PropertyIntegerList", + "BoneBlacklist", + "Dressup", + QT_TRANSLATE_NOOP("App::Property", "Bones that aren't dressed up"), + ) + obj.BoneBlacklist = [] + + self.onDocumentRestored(obj) + + def onDocumentRestored(self, obj): + self.obj = obj + obj.setEditorMode("BoneBlacklist", 2) # hide + + def __getstate__(self): + return None + + def __setstate__(self, state): + return None + + def toolRadius(self, obj): + if not hasattr(obj.Base, "ToolController"): + return self.toolRadius(obj.Base) + return obj.Base.ToolController.Tool.Diameter.Value / 2 + + def createBone(self, obj, move0, move1): + kink = dogboneII.Kink(move0, move1) + Path.Log.debug(f"{obj.Label}.createBone({kink})") + if insertBone(obj, kink): + generator = Style.Generator[obj.Style] + calc_length = Incision.Calc[obj.Incision] + nominal = self.toolRadius(obj) + custom = obj.Custom.Value + return dogboneII.generate(kink, generator, calc_length, nominal, custom) + return None + + def execute(self, obj): + Path.Log.track(obj.Label) + maneuver = PathLanguage.Maneuver() + bones = [] + lastMove = None + moveAfterPlunge = None + dressingUpDogbone = hasattr(obj.Base, "BoneBlacklist") + if obj.Base and obj.Base.Path and obj.Base.Path.Commands: + for i, instr in enumerate( + PathLanguage.Maneuver.FromPath(obj.Base.Path).instr + ): + # Path.Log.debug(f"instr: {instr}") + if instr.isMove(): + thisMove = instr + bone = None + if thisMove.isPlunge(): + if ( + lastMove + and moveAfterPlunge + and lastMove.leadsInto(moveAfterPlunge) + ): + bone = self.createBone(obj, lastMove, moveAfterPlunge) + lastMove = None + moveAfterPlunge = None + else: + if moveAfterPlunge is None: + moveAfterPlunge = thisMove + if lastMove: + bone = self.createBone(obj, lastMove, thisMove) + lastMove = thisMove + if bone: + enabled = not len(bones) in obj.BoneBlacklist + if enabled and not ( + dressingUpDogbone + and obj.Base.Proxy.includesBoneAt(bone.position()) + ): + maneuver.addInstructions(bone.instr) + else: + Path.Log.debug(f"{bone.kink} disabled {enabled}") + bones.append(bone) + maneuver.addInstruction(thisMove) + else: + # non-move instructions get added verbatim + maneuver.addInstruction(instr) + + else: + Path.Log.info(f"No Path found to dress up in op {obj.Base}") + self.maneuver = maneuver + self.bones = bones + self.boneTips = None + obj.Path = maneuver.toPath() + + def boneStates(self, obj): + state = {} + if hasattr(self, "bones"): + for nr, bone in enumerate(self.bones): + pos = bone.position() + loc = f"({pos.x:.4f}, {pos.y:.4f})" + if state.get(loc, None): + state[loc].addBone(bone, nr) + else: + state[loc] = BoneState(bone, nr) + if nr in obj.BoneBlacklist: + state[loc].enabled = False + return state.values() + + def includesBoneAt(self, pos): + if hasattr(self, "bones"): + for nr, bone in enumerate(self.bones): + if Path.Geom.pointsCoincide(bone.position(), pos): + return not (nr in self.obj.BoneBlacklist) + return False + + +def Create(base, name="DressupDogbone"): + obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) + pxy = Proxy(obj, base) + + obj.Proxy = pxy + + return obj diff --git a/src/Mod/Path/Path/Dressup/Gui/AxisMap.py b/src/Mod/Path/Path/Dressup/Gui/AxisMap.py index df3271bea5..2cd1497a06 100644 --- a/src/Mod/Path/Path/Dressup/Gui/AxisMap.py +++ b/src/Mod/Path/Path/Dressup/Gui/AxisMap.py @@ -260,7 +260,7 @@ class CommandPathDressup: def GetResources(self): return { "Pixmap": "Path_Dressup", - "MenuText": QT_TRANSLATE_NOOP("Path_DressupAxisMap", "Axis Map Dress-up"), + "MenuText": QT_TRANSLATE_NOOP("Path_DressupAxisMap", "Axis Map"), "Accel": "", "ToolTip": QT_TRANSLATE_NOOP( "Path_DressupAxisMap", "Remap one axis to another." diff --git a/src/Mod/Path/Path/Dressup/Gui/Boundary.py b/src/Mod/Path/Path/Dressup/Gui/Boundary.py index 0947f271b9..6e67e82b0d 100644 --- a/src/Mod/Path/Path/Dressup/Gui/Boundary.py +++ b/src/Mod/Path/Path/Dressup/Gui/Boundary.py @@ -260,9 +260,7 @@ class CommandPathDressupPathBoundary: def GetResources(self): return { "Pixmap": "Path_Dressup", - "MenuText": QT_TRANSLATE_NOOP( - "Path_DressupPathBoundary", "Boundary Dress-up" - ), + "MenuText": QT_TRANSLATE_NOOP("Path_DressupPathBoundary", "Boundary"), "ToolTip": QT_TRANSLATE_NOOP( "Path_DressupPathBoundary", "Creates a Path Boundary Dress-up object from a selected path", @@ -291,8 +289,7 @@ class CommandPathDressupPathBoundary: FreeCAD.ActiveDocument.openTransaction("Create Path Boundary Dress-up") FreeCADGui.addModule("Path.Dressup.Gui.Boundary") FreeCADGui.doCommand( - "Path.Dressup.Gui.Boundary.Create(App.ActiveDocument.%s)" - % baseObject.Name + "Path.Dressup.Gui.Boundary.Create(App.ActiveDocument.%s)" % baseObject.Name ) # FreeCAD.ActiveDocument.commitTransaction() # Final `commitTransaction()` called via TaskPanel.accept() FreeCAD.ActiveDocument.recompute() diff --git a/src/Mod/Path/Path/Dressup/Gui/Dogbone.py b/src/Mod/Path/Path/Dressup/Gui/Dogbone.py index 1ffd6436fd..8dbc31bded 100644 --- a/src/Mod/Path/Path/Dressup/Gui/Dogbone.py +++ b/src/Mod/Path/Path/Dressup/Gui/Dogbone.py @@ -853,7 +853,9 @@ class ObjectDressup(object): # debugCircle(e2.Curve.Center, e2.Curve.Radius, "bone.%d-2" % (self.boneId), (0.,1.,0.)) if Path.Geom.pointsCoincide( pt, e1.valueAt(e1.LastParameter) - ) or Path.Geom.pointsCoincide(pt, e2.valueAt(e2.FirstParameter)): + ) or Path.Geom.pointsCoincide( + pt, e2.valueAt(e2.FirstParameter) + ): continue # debugMarker(pt, "it", (0.0, 1.0, 1.0)) # 1. remove all redundant commands @@ -1359,7 +1361,7 @@ class CommandDressupDogbone(object): def GetResources(self): return { "Pixmap": "Path_Dressup", - "MenuText": QT_TRANSLATE_NOOP("Path_DressupDogbone", "Dogbone Dress-up"), + "MenuText": QT_TRANSLATE_NOOP("Path_DressupDogbone", "Dogbone"), "ToolTip": QT_TRANSLATE_NOOP( "Path_DressupDogbone", "Creates a Dogbone Dress-up object from a selected path", @@ -1401,11 +1403,12 @@ class CommandDressupDogbone(object): FreeCAD.ActiveDocument.recompute() -if FreeCAD.GuiUp: - import FreeCADGui - from PySide import QtGui - from pivy import coin - - FreeCADGui.addCommand("Path_DressupDogbone", CommandDressupDogbone()) +# obsolete, replaced by DogboneII +# if FreeCAD.GuiUp: +# import FreeCADGui +# from PySide import QtGui +# from pivy import coin +# +# FreeCADGui.addCommand("Path_DressupDogbone", CommandDressupDogbone()) FreeCAD.Console.PrintLog("Loading DressupDogbone... done\n") diff --git a/src/Mod/Path/Path/Dressup/Gui/DogboneII.py b/src/Mod/Path/Path/Dressup/Gui/DogboneII.py new file mode 100644 index 0000000000..bc184b3001 --- /dev/null +++ b/src/Mod/Path/Path/Dressup/Gui/DogboneII.py @@ -0,0 +1,378 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 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 * +# * * +# *************************************************************************** + +from PySide import QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP +import FreeCAD +import FreeCADGui +import Path +import Path.Dressup.DogboneII as DogboneII +import PathScripts.PathUtils as PathUtils + +if False: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) +else: + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) + +translate = FreeCAD.Qt.translate + + +class Marker(object): + def __init__(self, pt, r, h, ena): + if Path.Geom.isRoughly(h, 0): + h = 0.1 + self.pt = pt + self.r = r + self.h = h + self.ena = ena + self.sep = coin.SoSeparator() + self.pos = coin.SoTranslation() + self.pos.translation = (pt.x, pt.y, pt.z + h / 2) + self.rot = coin.SoRotationXYZ() + self.rot.axis = self.rot.X + self.rot.angle = DogboneII.PI / 2 + self.cyl = coin.SoCylinder() + self.cyl.radius = r + self.cyl.height = h + # self.cyl.removePart(self.cyl.TOP) + # self.cyl.removePart(self.cyl.BOTTOM) + self.material = coin.SoMaterial() + self.sep.addChild(self.pos) + self.sep.addChild(self.rot) + self.sep.addChild(self.material) + self.sep.addChild(self.cyl) + self.lowlight() + + def setSelected(self, selected): + if selected: + self.highlight() + else: + self.lowlight() + + def highlight(self): + self.material.diffuseColor = self.color(1) + self.material.transparency = 0.75 + + def lowlight(self): + self.material.diffuseColor = self.color(0) + self.material.transparency = 0.90 + + def _colorEnabled(self, id): + if id == 1: + return coin.SbColor(0.0, 0.9, 0.0) + return coin.SbColor(0.0, 0.9, 0.0) + + def _colorDisabled(self, id): + if id == 1: + return coin.SbColor(0.9, 0.0, 0.0) + return coin.SbColor(0.9, 0.0, 0.0) + + def color(self, id): + if self.ena: + return self._colorEnabled(id) + return self._colorDisabled(id) + + +class TaskPanel(object): + DataIds = QtCore.Qt.ItemDataRole.UserRole + DataState = QtCore.Qt.ItemDataRole.UserRole + 1 + + def __init__(self, viewProvider, obj): + self.viewProvider = viewProvider + self.obj = obj + self.form = FreeCADGui.PySideUic.loadUi(":/panels/DogboneEdit.ui") + self.s = None + FreeCAD.ActiveDocument.openTransaction("Edit Dogbone Dress-up") + # self.height = 10 ??? + self.markers = [] + + def reject(self): + FreeCAD.ActiveDocument.abortTransaction() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + FreeCADGui.Selection.removeObserver(self.s) + self.cleanup() + + def accept(self): + self.getFields() + FreeCAD.ActiveDocument.commitTransaction() + FreeCADGui.ActiveDocument.resetEdit() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + FreeCADGui.Selection.removeObserver(self.s) + FreeCAD.ActiveDocument.recompute() + self.cleanup() + + def cleanup(self): + self.viewProvider.showMarkers(False) + for m in self.markers: + self.viewProvider.switch.removeChild(m.sep) + self.markers = [] + + def getFields(self): + self.obj.Style = str(self.form.styleCombo.currentText()) + self.obj.Side = str(self.form.sideCombo.currentText()) + self.obj.Incision = str(self.form.incisionCombo.currentText()) + self.obj.Custom = self.form.custom.value() + blacklist = [] + for i in range(0, self.form.bones.count()): + item = self.form.bones.item(i) + if item.checkState() == QtCore.Qt.CheckState.Unchecked: + blacklist.extend(item.data(self.DataIds)) + self.obj.BoneBlacklist = sorted(blacklist) + self.obj.Proxy.execute(self.obj) + + def updateBoneList(self): + itemList = [] + for state in self.obj.Proxy.boneStates(self.obj): + pos = state.position() + ids = ",".join([str(nr) for nr in state.boneIDs()]) + lbl = f"({pos.x:.2f}, {pos.y:.2f}): {ids}" + item = QtGui.QListWidgetItem(lbl) + if state.isEnabled(): + item.setCheckState(QtCore.Qt.CheckState.Checked) + else: + item.setCheckState(QtCore.Qt.CheckState.Unchecked) + flags = QtCore.Qt.ItemFlag.ItemIsSelectable + flags |= QtCore.Qt.ItemFlag.ItemIsEnabled + flags |= QtCore.Qt.ItemFlag.ItemIsUserCheckable + item.setFlags(flags) + item.setData(self.DataIds, state.boneIDs()) + item.setData(self.DataState, state) + itemList.append(item) + + markers = [] + self.form.bones.clear() + for item in sorted( + itemList, key=lambda item: item.data(self.DataState).boneIDs()[0] + ): + self.form.bones.addItem(item) + state = item.data(self.DataState) + loc = state.boneTip() + r = self.obj.Proxy.toolRadius(self.obj) + zs = state.zLevels() + markers.append( + Marker( + FreeCAD.Vector(loc.x, loc.y, min(zs)), + r, + max(1, max(zs) - min(zs)), + state.isEnabled(), + ) + ) + for m in self.markers: + self.viewProvider.switch.removeChild(m.sep) + for m in markers: + self.viewProvider.switch.addChild(m.sep) + self.markers = markers + + def updateUI(self): + customSelected = self.obj.Incision == DogboneII.Incision.Custom + self.form.custom.setEnabled(customSelected) + self.form.customLabel.setEnabled(customSelected) + self.updateBoneList() + + def updateModel(self): + self.getFields() + self.updateUI() + FreeCAD.ActiveDocument.recompute() + + def setupCombo(self, combo, text, items): + if items and len(items) > 0: + for i in range(combo.count(), -1, -1): + combo.removeItem(i) + combo.addItems(items) + index = combo.findText(text, QtCore.Qt.MatchFixedString) + if index >= 0: + combo.setCurrentIndex(index) + + def setFields(self): + self.setupCombo(self.form.styleCombo, self.obj.Style, DogboneII.Style.All) + self.setupCombo(self.form.sideCombo, self.obj.Side, DogboneII.Side.All) + self.setupCombo( + self.form.incisionCombo, self.obj.Incision, DogboneII.Incision.All + ) + self.form.custom.setMinimum(0.0) + self.form.custom.setDecimals(3) + self.form.custom.setValue(self.obj.Custom) + self.updateUI() + + def open(self): + self.s = SelObserver() + # install the function mode resident + FreeCADGui.Selection.addObserver(self.s) + + def setupUi(self): + self.setFields() + # now that the form is filled, setup the signal handlers + self.form.styleCombo.currentIndexChanged.connect(self.updateModel) + self.form.sideCombo.currentIndexChanged.connect(self.updateModel) + self.form.incisionCombo.currentIndexChanged.connect(self.updateModel) + self.form.custom.valueChanged.connect(self.updateModel) + self.form.bones.itemChanged.connect(self.updateModel) + self.form.bones.itemSelectionChanged.connect(self.updateMarkers) + + self.viewProvider.showMarkers(True) + + def updateMarkers(self): + index = self.form.bones.currentRow() + for i, m in enumerate(self.markers): + m.setSelected(i == index) + + +class SelObserver(object): + def __init__(self): + import Path.Op.Gui.Selection as PST + + PST.eselect() + + def __del__(self): + import Path.Op.Gui.Selection as PST + + PST.clear() + + def addSelection(self, doc, obj, sub, pnt): + FreeCADGui.doCommand( + "Gui.Selection.addSelection(FreeCAD.ActiveDocument." + obj + ")" + ) + FreeCADGui.updateGui() + + +class ViewProviderDressup(object): + def __init__(self, vobj): + self.vobj = vobj + self.obj = None + + def attach(self, vobj): + self.obj = vobj.Object + if self.obj and self.obj.Base: + for i in self.obj.Base.InList: + if hasattr(i, "Group"): + group = i.Group + for g in group: + if g.Name == self.obj.Base.Name: + group.remove(g) + i.Group = group + # FreeCADGui.ActiveDocument.getObject(obj.Base.Name).Visibility = False + self.switch = coin.SoSwitch() + vobj.RootNode.addChild(self.switch) + + def showMarkers(self, on): + sw = coin.SO_SWITCH_ALL if on else coin.SO_SWITCH_NONE + self.switch.whichChild = sw + + def claimChildren(self): + return [self.obj.Base] + + def setEdit(self, vobj, mode=0): + FreeCADGui.Control.closeDialog() + panel = TaskPanel(self, vobj.Object) + FreeCADGui.Control.showDialog(panel) + panel.setupUi() + return True + + def __getstate__(self): + return None + + def __setstate__(self, state): + return None + + def onDelete(self, arg1=None, arg2=None): + """this makes sure that the base operation is added back to the project and visible""" + if arg1.Object and arg1.Object.Base: + FreeCADGui.ActiveDocument.getObject(arg1.Object.Base.Name).Visibility = True + job = PathUtils.findParentJob(arg1.Object) + if job: + job.Proxy.addOperation(arg1.Object.Base, arg1.Object) + arg1.Object.Base = None + return True + + +def Create(base, name="DressupDogbone"): + """ + Create(obj, name='DressupDogbone') ... dresses the given Path.Op.Profile object with dogbones. + """ + obj = DogboneII.Create(base, name) + job = PathUtils.findParentJob(base) + job.Proxy.addOperation(obj, base) + + if FreeCAD.GuiUp: + obj.ViewObject.Proxy = ViewProviderDressup(obj.ViewObject) + obj.Base.ViewObject.Visibility = False + + return obj + + +class CommandDressupDogboneII(object): + def GetResources(self): + return { + "Pixmap": "Path_Dressup", + "MenuText": QT_TRANSLATE_NOOP("Path_DressupDogbone", "Dogbone"), + "ToolTip": QT_TRANSLATE_NOOP( + "Path_DressupDogbone", + "Creates a Dogbone Dress-up object from a selected path", + ), + } + + def IsActive(self): + if FreeCAD.ActiveDocument is not None: + for o in FreeCAD.ActiveDocument.Objects: + if o.Name[:3] == "Job": + return True + return False + + def Activated(self): + + # check that the selection contains exactly what we want + selection = FreeCADGui.Selection.getSelection() + if len(selection) != 1: + FreeCAD.Console.PrintError( + translate("Path_DressupDogbone", "Please select one path object") + "\n" + ) + return + baseObject = selection[0] + if not baseObject.isDerivedFrom("Path::Feature"): + FreeCAD.Console.PrintError( + translate("Path_DressupDogbone", "The selected object is not a path") + + "\n" + ) + return + + # everything ok! + FreeCAD.ActiveDocument.openTransaction("Create Dogbone Dress-up") + FreeCADGui.addModule("Path.Dressup.Gui.DogboneII") + FreeCADGui.doCommand( + "Path.Dressup.Gui.DogboneII.Create(FreeCAD.ActiveDocument.%s)" + % baseObject.Name + ) + # FreeCAD.ActiveDocument.commitTransaction() # Final `commitTransaction()` called via TaskPanel.accept() + FreeCAD.ActiveDocument.recompute() + + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtGui + from pivy import coin + + FreeCADGui.addCommand("Path_DressupDogbone", CommandDressupDogboneII()) + +FreeCAD.Console.PrintLog("Loading DressupDogboneII ... done\n") diff --git a/src/Mod/Path/Path/Dressup/Gui/Dragknife.py b/src/Mod/Path/Path/Dressup/Gui/Dragknife.py index d1f6699cd5..56f36b8790 100644 --- a/src/Mod/Path/Path/Dressup/Gui/Dragknife.py +++ b/src/Mod/Path/Path/Dressup/Gui/Dragknife.py @@ -599,9 +599,7 @@ class CommandDressupDragknife: def GetResources(self): return { "Pixmap": "Path_Dressup", - "MenuText": QT_TRANSLATE_NOOP( - "Path_DressupDragKnife", "DragKnife Dress-up" - ), + "MenuText": QT_TRANSLATE_NOOP("Path_DressupDragKnife", "DragKnife"), "ToolTip": QT_TRANSLATE_NOOP( "Path_DressupDragKnife", "Modifies a path to add dragknife corner actions", diff --git a/src/Mod/Path/Path/Dressup/Gui/LeadInOut.py b/src/Mod/Path/Path/Dressup/Gui/LeadInOut.py index bdb4f2b16b..9ce7e97fde 100644 --- a/src/Mod/Path/Path/Dressup/Gui/LeadInOut.py +++ b/src/Mod/Path/Path/Dressup/Gui/LeadInOut.py @@ -711,7 +711,7 @@ class CommandPathDressupLeadInOut: def GetResources(self): return { "Pixmap": "Path_Dressup", - "MenuText": QT_TRANSLATE_NOOP("Path_DressupLeadInOut", "LeadInOut Dressup"), + "MenuText": QT_TRANSLATE_NOOP("Path_DressupLeadInOut", "LeadInOut"), "ToolTip": QT_TRANSLATE_NOOP( "Path_DressupLeadInOut", "Creates a Cutter Radius Compensation G41/G42 Entry Dressup object from a selected path", @@ -753,9 +753,7 @@ class CommandPathDressupLeadInOut: FreeCADGui.doCommand( 'obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "LeadInOutDressup")' ) - FreeCADGui.doCommand( - "dbo = Path.Dressup.Gui.LeadInOut.ObjectDressup(obj)" - ) + FreeCADGui.doCommand("dbo = Path.Dressup.Gui.LeadInOut.ObjectDressup(obj)") FreeCADGui.doCommand("base = FreeCAD.ActiveDocument." + selection[0].Name) FreeCADGui.doCommand("job = PathScripts.PathUtils.findParentJob(base)") FreeCADGui.doCommand("obj.Base = base") diff --git a/src/Mod/Path/Path/Dressup/Gui/RampEntry.py b/src/Mod/Path/Path/Dressup/Gui/RampEntry.py index 5b740bef3f..820f9516eb 100644 --- a/src/Mod/Path/Path/Dressup/Gui/RampEntry.py +++ b/src/Mod/Path/Path/Dressup/Gui/RampEntry.py @@ -895,9 +895,7 @@ class CommandPathDressupRampEntry: def GetResources(self): return { "Pixmap": "Path_Dressup", - "MenuText": QT_TRANSLATE_NOOP( - "Path_DressupRampEntry", "RampEntry Dress-up" - ), + "MenuText": QT_TRANSLATE_NOOP("Path_DressupRampEntry", "RampEntry"), "ToolTip": QT_TRANSLATE_NOOP( "Path_DressupRampEntry", "Creates a Ramp Entry Dress-up object from a selected path", @@ -940,9 +938,7 @@ class CommandPathDressupRampEntry: FreeCADGui.doCommand( 'obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "RampEntryDressup")' ) - FreeCADGui.doCommand( - "dbo = Path.Dressup.Gui.RampEntry.ObjectDressup(obj)" - ) + FreeCADGui.doCommand("dbo = Path.Dressup.Gui.RampEntry.ObjectDressup(obj)") FreeCADGui.doCommand("base = FreeCAD.ActiveDocument." + selection[0].Name) FreeCADGui.doCommand("job = PathScripts.PathUtils.findParentJob(base)") FreeCADGui.doCommand("obj.Base = base") diff --git a/src/Mod/Path/Path/Dressup/Gui/Tags.py b/src/Mod/Path/Path/Dressup/Gui/Tags.py index 0ec1c9d5ad..cfe0490e6e 100644 --- a/src/Mod/Path/Path/Dressup/Gui/Tags.py +++ b/src/Mod/Path/Path/Dressup/Gui/Tags.py @@ -244,7 +244,9 @@ class PathDressupTagTaskPanel: self.Positions.append(FreeCAD.Vector(point.x, point.y, 0)) self.updateTagsView() else: - Path.Log.notice("ignore new tag at %s (obj=%s, on-path=%d" % (point, obj, 0)) + Path.Log.notice( + "ignore new tag at %s (obj=%s, on-path=%d" % (point, obj, 0) + ) def addNewTag(self): self.tags = self.getTags(True) @@ -561,7 +563,7 @@ class CommandPathDressupTag: def GetResources(self): return { "Pixmap": "Path_Dressup", - "MenuText": QT_TRANSLATE_NOOP("Path_DressupTag", "Tag Dress-up"), + "MenuText": QT_TRANSLATE_NOOP("Path_DressupTag", "Tag"), "ToolTip": QT_TRANSLATE_NOOP( "Path_DressupTag", "Creates a Tag Dress-up object from a selected path" ), @@ -588,8 +590,7 @@ class CommandPathDressupTag: FreeCAD.ActiveDocument.openTransaction("Create Tag Dress-up") FreeCADGui.addModule("Path.Dressup.Gui.Tags") FreeCADGui.doCommand( - "Path.Dressup.Gui.Tags.Create(App.ActiveDocument.%s)" - % baseObject.Name + "Path.Dressup.Gui.Tags.Create(App.ActiveDocument.%s)" % baseObject.Name ) # FreeCAD.ActiveDocument.commitTransaction() # Final `commitTransaction()` called via TaskPanel.accept() FreeCAD.ActiveDocument.recompute() diff --git a/src/Mod/Path/Path/Dressup/Gui/ZCorrect.py b/src/Mod/Path/Path/Dressup/Gui/ZCorrect.py index 15f884ecc7..c591a02867 100644 --- a/src/Mod/Path/Path/Dressup/Gui/ZCorrect.py +++ b/src/Mod/Path/Path/Dressup/Gui/ZCorrect.py @@ -174,7 +174,10 @@ class ObjectDressup: Path.Log.debug(" curLoc:{}".format(currLocation)) newparams = dict(c.Parameters) zval = newparams.get("Z", currLocation["Z"]) - if c.Name in Path.Geom.CmdMoveStraight + Path.Geom.CmdMoveArc: + if ( + c.Name + in Path.Geom.CmdMoveStraight + Path.Geom.CmdMoveArc + ): curVec = FreeCAD.Vector( currLocation["X"], currLocation["Y"], @@ -340,9 +343,7 @@ class CommandPathDressup: def GetResources(self): return { "Pixmap": "Path_Dressup", - "MenuText": QT_TRANSLATE_NOOP( - "Path_DressupZCorrect", "Z Depth Correction Dress-up" - ), + "MenuText": QT_TRANSLATE_NOOP("Path_DressupZCorrect", "Z Depth Correction"), "Accel": "", "ToolTip": QT_TRANSLATE_NOOP( "Path_DressupZCorrect", "Use Probe Map to correct Z depth" diff --git a/src/Mod/Path/Path/Geom.py b/src/Mod/Path/Path/Geom.py index 29955f6f25..b1163ccbd7 100644 --- a/src/Mod/Path/Path/Geom.py +++ b/src/Mod/Path/Path/Geom.py @@ -127,6 +127,15 @@ def edgeConnectsTo(edge, vector, error=Tolerance): ) or pointsCoincide(edge.valueAt(edge.LastParameter), vector, error) +def normalizeAngle(a): + """normalizeAngle(a) ... return angle shifted into interval -pi <= a <= pi""" + while a > math.pi: + a = a - 2 * math.pi + while a < -math.pi: + a = a + 2 * math.pi + return a + + def getAngle(vector): """getAngle(vector) Returns the angle [-pi,pi] of a vector using the X-axis as the reference. diff --git a/src/Mod/Path/Path/GuiInit.py b/src/Mod/Path/Path/GuiInit.py index b57207240b..63b5a7607b 100644 --- a/src/Mod/Path/Path/GuiInit.py +++ b/src/Mod/Path/Path/GuiInit.py @@ -42,6 +42,7 @@ def Startup(): from Path.Base.Gui import SetupSheet from Path.Dressup.Gui import AxisMap from Path.Dressup.Gui import Dogbone + from Path.Dressup.Gui import DogboneII from Path.Dressup.Gui import Dragknife from Path.Dressup.Gui import LeadInOut from Path.Dressup.Gui import Boundary diff --git a/src/Mod/Path/Path/Main/Gui/Inspect.py b/src/Mod/Path/Path/Main/Gui/Inspect.py index 63d4e6f7d6..e24e537817 100644 --- a/src/Mod/Path/Path/Main/Gui/Inspect.py +++ b/src/Mod/Path/Path/Main/Gui/Inspect.py @@ -61,18 +61,25 @@ class GCodeHighlighter(QtGui.QSyntaxHighlighter): self.highlightingRules = [] numberFormat = QtGui.QTextCharFormat() numberFormat.setForeground(colors[0]) - self.highlightingRules.append((QtCore.QRegularExpression("[\\-0-9\\.]"), numberFormat)) + self.highlightingRules.append( + (QtCore.QRegularExpression("[\\-0-9\\.]"), numberFormat) + ) keywordFormat = QtGui.QTextCharFormat() keywordFormat.setForeground(colors[1]) keywordFormat.setFontWeight(QtGui.QFont.Bold) keywordPatterns = ["\\bG[0-9]+\\b", "\\bM[0-9]+\\b"] self.highlightingRules.extend( - [(QtCore.QRegularExpression(pattern), keywordFormat) for pattern in keywordPatterns] + [ + (QtCore.QRegularExpression(pattern), keywordFormat) + for pattern in keywordPatterns + ] ) speedFormat = QtGui.QTextCharFormat() speedFormat.setFontWeight(QtGui.QFont.Bold) speedFormat.setForeground(colors[2]) - self.highlightingRules.append((QtCore.QRegularExpression("\\bF[0-9\\.]+\\b"), speedFormat)) + self.highlightingRules.append( + (QtCore.QRegularExpression("\\bF[0-9\\.]+\\b"), speedFormat) + ) def highlightBlock(self, text): diff --git a/src/Mod/Path/Path/Main/Gui/PreferencesJob.py b/src/Mod/Path/Path/Main/Gui/PreferencesJob.py index 7d3d3c0b8a..3ca02ae0dd 100644 --- a/src/Mod/Path/Path/Main/Gui/PreferencesJob.py +++ b/src/Mod/Path/Path/Main/Gui/PreferencesJob.py @@ -145,9 +145,7 @@ class JobPreferencesPage: Path.Preferences.setDefaultStockTemplate("") def saveToolsSettings(self): - Path.Preferences.setToolsSettings( - self.form.toolsAbsolutePaths.isChecked() - ) + Path.Preferences.setToolsSettings(self.form.toolsAbsolutePaths.isChecked()) def selectComboEntry(self, widget, text): index = widget.findText(text, QtCore.Qt.MatchFixedString) diff --git a/src/Mod/Path/Path/Main/Stock.py b/src/Mod/Path/Path/Main/Stock.py index f467da5011..23d970d59b 100644 --- a/src/Mod/Path/Path/Main/Stock.py +++ b/src/Mod/Path/Path/Main/Stock.py @@ -325,6 +325,7 @@ class StockCreateCylinder(Stock): if prop in ["Radius", "Height"] and not "Restore" in obj.State: self.execute(obj) + def SetupStockObject(obj, stockType): Path.Log.track(obj.Label, stockType) if FreeCAD.GuiUp and obj.ViewObject: diff --git a/src/Mod/Path/Path/Op/Area.py b/src/Mod/Path/Path/Op/Area.py index 094627c58e..5309f10b47 100644 --- a/src/Mod/Path/Path/Op/Area.py +++ b/src/Mod/Path/Path/Op/Area.py @@ -227,7 +227,7 @@ class ObjectOp(PathOp.ObjectOp): area.add(baseobject) areaParams = self.areaOpAreaParams(obj, isHole) - areaParams['SectionTolerance'] = 1e-07 + areaParams["SectionTolerance"] = 1e-07 heights = [i for i in self.depthparams] Path.Log.debug("depths: {}".format(heights)) diff --git a/src/Mod/Path/Path/Op/Drilling.py b/src/Mod/Path/Path/Op/Drilling.py index b21a03dfb4..3142efb7c3 100644 --- a/src/Mod/Path/Path/Op/Drilling.py +++ b/src/Mod/Path/Path/Op/Drilling.py @@ -245,7 +245,7 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp): dwelltime = obj.DwellTime if obj.DwellEnabled else 0.0 peckdepth = obj.PeckDepth.Value if obj.PeckEnabled else 0.0 repeat = 1 # technical debt: Add a repeat property for user control - chipBreak = (obj.chipBreakEnabled and obj.PeckEnabled) + chipBreak = obj.chipBreakEnabled and obj.PeckEnabled try: drillcommands = drill.generate( @@ -254,7 +254,7 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp): peckdepth, repeat, obj.RetractHeight.Value, - chipBreak=chipBreak + chipBreak=chipBreak, ) except ValueError as e: # any targets that fail the generator are ignored diff --git a/src/Mod/Path/Path/Op/EngraveBase.py b/src/Mod/Path/Path/Op/EngraveBase.py index 0a235bb356..2f1c9b2131 100644 --- a/src/Mod/Path/Path/Op/EngraveBase.py +++ b/src/Mod/Path/Path/Op/EngraveBase.py @@ -137,7 +137,9 @@ class ObjectOp(PathOp.ObjectOp): ) first = False - if Path.Geom.pointsCoincide(last, edge.valueAt(edge.FirstParameter)): + if Path.Geom.pointsCoincide( + last, edge.valueAt(edge.FirstParameter) + ): # if Path.Geom.pointsCoincide(last, edge.Vertexes[0].Point): for cmd in Path.Geom.cmdsForEdge(edge): self.appendCommand(cmd, z, relZ, self.horizFeed) diff --git a/src/Mod/Path/Path/Op/FeatureExtension.py b/src/Mod/Path/Path/Op/FeatureExtension.py index fd14420453..7d16d0b952 100644 --- a/src/Mod/Path/Path/Op/FeatureExtension.py +++ b/src/Mod/Path/Path/Op/FeatureExtension.py @@ -411,7 +411,9 @@ class Extension(object): else: Path.Log.debug("else is NOT Part.Circle") - Path.Log.track(self.feature, self.sub, type(edge.Curve), endPoints(edge)) + Path.Log.track( + self.feature, self.sub, type(edge.Curve), endPoints(edge) + ) direction = self._getDirection(sub) if direction is None: return None diff --git a/src/Mod/Path/Path/Op/Gui/Base.py b/src/Mod/Path/Path/Op/Gui/Base.py index 9f61f5d5cf..bb956edeba 100644 --- a/src/Mod/Path/Path/Op/Gui/Base.py +++ b/src/Mod/Path/Path/Op/Gui/Base.py @@ -917,7 +917,9 @@ class TaskPanelDepthsPage(TaskPanelPage): self.form.finalDepthSet.hide() if self.haveStepDown(): - self.stepDown = PathGuiUtil.QuantitySpinBox(self.form.stepDown, obj, "StepDown") + self.stepDown = PathGuiUtil.QuantitySpinBox( + self.form.stepDown, obj, "StepDown" + ) else: self.form.stepDown.hide() self.form.stepDownLabel.hide() @@ -1389,7 +1391,9 @@ def Create(res): diag.setWindowModality(QtCore.Qt.ApplicationModal) diag.exec_() except PathOp.PathNoTCException: - Path.Log.warning(translate("PathOp", "No tool controller, aborting op creation")) + Path.Log.warning( + translate("PathOp", "No tool controller, aborting op creation") + ) FreeCAD.ActiveDocument.abortTransaction() FreeCAD.ActiveDocument.recompute() diff --git a/src/Mod/Path/Path/Op/Gui/FeatureExtension.py b/src/Mod/Path/Path/Op/Gui/FeatureExtension.py index 8ce59b0260..3721fa2ac7 100644 --- a/src/Mod/Path/Path/Op/Gui/FeatureExtension.py +++ b/src/Mod/Path/Path/Op/Gui/FeatureExtension.py @@ -369,7 +369,9 @@ class TaskPanelExtensionPage(PathOpGui.TaskPanelPage): return Path.Geom.edgesMatch(e0, e1) self.extensionEdges = extensionEdges - Path.Log.debug("extensionEdges.values(): {}".format(extensionEdges.values())) + Path.Log.debug( + "extensionEdges.values(): {}".format(extensionEdges.values()) + ) for edgeList in Part.sortEdges( list(extensionEdges.keys()) ): # Identify connected edges that form wires diff --git a/src/Mod/Path/Path/Op/Gui/SimpleCopy.py b/src/Mod/Path/Path/Op/Gui/SimpleCopy.py index b8a21c3443..64f2879108 100644 --- a/src/Mod/Path/Path/Op/Gui/SimpleCopy.py +++ b/src/Mod/Path/Path/Op/Gui/SimpleCopy.py @@ -74,9 +74,7 @@ class CommandPathSimpleCopy: FreeCADGui.addModule("PathScripts.PathUtils") FreeCADGui.addModule("Path.Op.Custom") FreeCADGui.doCommand( - 'obj = Path.Op.Custom.Create("' - + selection[0].Name - + '_SimpleCopy")' + 'obj = Path.Op.Custom.Create("' + selection[0].Name + '_SimpleCopy")' ) FreeCADGui.doCommand("obj.ViewObject.Proxy = 0") FreeCADGui.doCommand("obj.Gcode = [c.toGCode() for c in srcpath.Commands]") diff --git a/src/Mod/Path/Path/Op/Gui/ThreadMilling.py b/src/Mod/Path/Path/Op/Gui/ThreadMilling.py index 464f78150e..adcbc428bd 100644 --- a/src/Mod/Path/Path/Op/Gui/ThreadMilling.py +++ b/src/Mod/Path/Path/Op/Gui/ThreadMilling.py @@ -146,26 +146,20 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): def _isThreadImperial(self): return ( - self.form.threadType.currentData() - in PathThreadMilling.ThreadTypesImperial + self.form.threadType.currentData() in PathThreadMilling.ThreadTypesImperial ) def _isThreadMetric(self): - return ( - self.form.threadType.currentData() - in PathThreadMilling.ThreadTypesMetric - ) + return self.form.threadType.currentData() in PathThreadMilling.ThreadTypesMetric def _isThreadInternal(self): return ( - self.form.threadType.currentData() - in PathThreadMilling.ThreadTypesInternal + self.form.threadType.currentData() in PathThreadMilling.ThreadTypesInternal ) def _isThreadExternal(self): return ( - self.form.threadType.currentData() - in PathThreadMilling.ThreadTypesExternal + self.form.threadType.currentData() in PathThreadMilling.ThreadTypesExternal ) def _updateFromThreadType(self): @@ -195,9 +189,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): self.pitch.updateSpinBox(0) fillThreads( self.form, - PathThreadMilling.ThreadTypeData[ - self.form.threadType.currentData() - ], + PathThreadMilling.ThreadTypeData[self.form.threadType.currentData()], self.obj.ThreadName, ) self._updateFromThreadName() diff --git a/src/Mod/Path/Path/Op/Profile.py b/src/Mod/Path/Path/Op/Profile.py index 4879f0a36f..05ee46943b 100644 --- a/src/Mod/Path/Path/Op/Profile.py +++ b/src/Mod/Path/Path/Op/Profile.py @@ -849,7 +849,9 @@ class ObjectProfile(PathAreaOp.ObjectOp): # verify that wire chosen is not inside the physical model if wi > 0: # and isInterior is False: - Path.Log.debug("Multiple wires in cut area. First choice is not 0. Testing.") + Path.Log.debug( + "Multiple wires in cut area. First choice is not 0. Testing." + ) testArea = fcShp.cut(base.Shape) isReady = self._checkTagIntersection(iTAG, eTAG, self.cutSide, testArea) @@ -863,7 +865,9 @@ class ObjectProfile(PathAreaOp.ObjectOp): workShp = pfcShp.cut(fcShp) if testArea.Area < minArea: - Path.Log.debug("offset area is less than minArea of {}.".format(minArea)) + Path.Log.debug( + "offset area is less than minArea of {}.".format(minArea) + ) Path.Log.debug("Using wire index {}.".format(wi - 1)) pWire = Part.Wire(Part.__sortEdges__(workShp.Wires[wi - 1].Edges)) pfcShp = Part.Face(pWire) diff --git a/src/Mod/Path/Path/Op/SurfaceSupport.py b/src/Mod/Path/Path/Op/SurfaceSupport.py index 88656ef260..79e468df61 100644 --- a/src/Mod/Path/Path/Op/SurfaceSupport.py +++ b/src/Mod/Path/Path/Op/SurfaceSupport.py @@ -626,7 +626,9 @@ class ProcessSelectedFaces: if fcShp: Path.Log.debug("vShapes[{}]: {}".format(m, vShapes[m])) if vShapes[m]: - Path.Log.debug(" -Cutting void from base profile shape.") + Path.Log.debug( + " -Cutting void from base profile shape." + ) adjPS = prflShp.cut(vShapes[m][0]) self.profileShapes[m] = [adjPS] else: diff --git a/src/Mod/Path/Path/Op/ThreadMilling.py b/src/Mod/Path/Path/Op/ThreadMilling.py index 144098273a..41babae7c5 100644 --- a/src/Mod/Path/Path/Op/ThreadMilling.py +++ b/src/Mod/Path/Path/Op/ThreadMilling.py @@ -100,9 +100,11 @@ ThreadTypesMetric = [ ThreadTypes = ThreadTypesInternal + ThreadTypesExternal Directions = [DirectionClimb, DirectionConventional] + def _isThreadInternal(obj): return obj.ThreadType in ThreadTypesInternal + def threadSetupInternal(obj, zTop, zBottom): Path.Log.track() if obj.ThreadOrientation == RightHand: @@ -118,6 +120,7 @@ def threadSetupInternal(obj, zTop, zBottom): # for conventional milling, cut bottom up with G2 return ("G2", zBottom, zTop) + def threadSetupExternal(obj, zTop, zBottom): Path.Log.track() if obj.ThreadOrientation == RightHand: @@ -132,6 +135,7 @@ def threadSetupExternal(obj, zTop, zBottom): # for climb milling need to go bottom up and the other way return ("G2", zBottom, zTop) + def threadSetup(obj): """Return (cmd, zbegin, zend) of thread milling operation""" Path.Log.track() @@ -442,11 +446,7 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp): float(self.tool.Diameter), float(self.tool.Crest), ): - if ( - not start is None - and not _isThreadInternal(obj) - and not obj.LeadInOut - ): + if not start is None and not _isThreadInternal(obj) and not obj.LeadInOut: # external thread without lead in/out have to go up and over # in other words we need a move to clearance and not take any # shortcuts when moving to the elevator position diff --git a/src/Mod/Path/Path/Post/Utils.py b/src/Mod/Path/Path/Post/Utils.py index b4090afde3..388392a962 100644 --- a/src/Mod/Path/Path/Post/Utils.py +++ b/src/Mod/Path/Path/Post/Utils.py @@ -52,13 +52,16 @@ class GCodeHighlighter(QtGui.QSyntaxHighlighter): keywordPatterns = ["\\bG[0-9]+\\b", "\\bM[0-9]+\\b"] self.highlightingRules = [ - (QtCore.QRegularExpression(pattern), keywordFormat) for pattern in keywordPatterns + (QtCore.QRegularExpression(pattern), keywordFormat) + for pattern in keywordPatterns ] speedFormat = QtGui.QTextCharFormat() speedFormat.setFontWeight(QtGui.QFont.Bold) speedFormat.setForeground(QtCore.Qt.green) - self.highlightingRules.append((QtCore.QRegularExpression("\\bF[0-9\\.]+\\b"), speedFormat)) + self.highlightingRules.append( + (QtCore.QRegularExpression("\\bF[0-9\\.]+\\b"), speedFormat) + ) def highlightBlock(self, text): for pattern, hlFormat in self.highlightingRules: diff --git a/src/Mod/Path/Path/Post/UtilsArguments.py b/src/Mod/Path/Path/Post/UtilsArguments.py index d1a6fb22e2..8fafa7b206 100644 --- a/src/Mod/Path/Path/Post/UtilsArguments.py +++ b/src/Mod/Path/Path/Post/UtilsArguments.py @@ -602,14 +602,18 @@ def process_shared_arguments(values, parser, argstring, all_visible, filename): if args.output_all_arguments: argument_text = all_visible.format_help() if not filename == "-": - gfile = pythonopen(filename, "w", newline=values["END_OF_LINE_CHARACTERS"]) + gfile = pythonopen( + filename, "w", newline=values["END_OF_LINE_CHARACTERS"] + ) gfile.write(argument_text) gfile.close() return (False, argument_text) if args.output_visible_arguments: argument_text = parser.format_help() if not filename == "-": - gfile = pythonopen(filename, "w", newline=values["END_OF_LINE_CHARACTERS"]) + gfile = pythonopen( + filename, "w", newline=values["END_OF_LINE_CHARACTERS"] + ) gfile.write(argument_text) gfile.close() return (False, argument_text) @@ -708,6 +712,7 @@ def process_shared_arguments(values, parser, argstring, all_visible, filename): return (True, args) + # # LinuxCNC (and GRBL) G-Code Parameter/word Patterns # __________________________________________________ diff --git a/src/Mod/Path/Path/Post/UtilsParse.py b/src/Mod/Path/Path/Post/UtilsParse.py index 3fb33334c5..58bdc53725 100644 --- a/src/Mod/Path/Path/Post/UtilsParse.py +++ b/src/Mod/Path/Path/Post/UtilsParse.py @@ -108,13 +108,16 @@ def drill_translate(values, outstring, cmd, params): strG0_RETRACT_Z = ( "G0 Z" - + format(float(RETRACT_Z.getValueAs(values["UNIT_FORMAT"])), axis_precision_string) + + format( + float(RETRACT_Z.getValueAs(values["UNIT_FORMAT"])), axis_precision_string + ) + "\n" ) strF_Feedrate = ( " F" + format( - float(drill_feedrate.getValueAs(values["UNIT_SPEED_FORMAT"])), feed_precision_string + float(drill_feedrate.getValueAs(values["UNIT_SPEED_FORMAT"])), + feed_precision_string, ) + "\n" ) @@ -126,9 +129,13 @@ def drill_translate(values, outstring, cmd, params): trBuff += ( linenumber(values) + "G0 X" - + format(float(drill_X.getValueAs(values["UNIT_FORMAT"])), axis_precision_string) + + format( + float(drill_X.getValueAs(values["UNIT_FORMAT"])), axis_precision_string + ) + " Y" - + format(float(drill_Y.getValueAs(values["UNIT_FORMAT"])), axis_precision_string) + + format( + float(drill_Y.getValueAs(values["UNIT_FORMAT"])), axis_precision_string + ) + "\n" ) if values["CURRENT_Z"] > RETRACT_Z: @@ -137,7 +144,10 @@ def drill_translate(values, outstring, cmd, params): trBuff += ( linenumber(values) + "G1 Z" - + format(float(RETRACT_Z.getValueAs(values["UNIT_FORMAT"])), axis_precision_string) + + format( + float(RETRACT_Z.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + strF_Feedrate ) last_Stop_Z = RETRACT_Z @@ -147,7 +157,9 @@ def drill_translate(values, outstring, cmd, params): trBuff += ( linenumber(values) + "G1 Z" - + format(float(drill_Z.getValueAs(values["UNIT_FORMAT"])), axis_precision_string) + + format( + float(drill_Z.getValueAs(values["UNIT_FORMAT"])), axis_precision_string + ) + strF_Feedrate ) # pause where applicable @@ -182,12 +194,18 @@ def drill_translate(values, outstring, cmd, params): ) if cmd == "G73": # Rapid up "a small amount". - chip_breaker_height = next_Stop_Z + values["CHIPBREAKING_AMOUNT"] + chip_breaker_height = ( + next_Stop_Z + values["CHIPBREAKING_AMOUNT"] + ) trBuff += ( linenumber(values) + "G0 Z" + format( - float(chip_breaker_height.getValueAs(values["UNIT_FORMAT"])), + float( + chip_breaker_height.getValueAs( + values["UNIT_FORMAT"] + ) + ), axis_precision_string, ) + "\n" @@ -210,8 +228,8 @@ def drill_translate(values, outstring, cmd, params): break # except Exception: - # print("exception occurred") - # pass + # print("exception occurred") + # pass if values["MOTION_MODE"] == "G91": trBuff += linenumber(values) + "G91\n" # Restore if changed @@ -365,7 +383,9 @@ def parse(values, pathobj): if command in ("G41", "G42"): outstring.append(param + str(int(c.Parameters[param]))) elif command in ("G41.1", "G42.1"): - pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + pos = Units.Quantity( + c.Parameters[param], FreeCAD.Units.Length + ) outstring.append( param + format( @@ -400,7 +420,9 @@ def parse(values, pathobj): elif command in ("G4", "G04", "G76", "G82", "G86", "G89"): outstring.append(param + str(float(c.Parameters[param]))) elif command in ("G5", "G05", "G64"): - pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + pos = Units.Quantity( + c.Parameters[param], FreeCAD.Units.Length + ) outstring.append( param + format( @@ -414,7 +436,9 @@ def parse(values, pathobj): if command == "G10": outstring.append(param + str(int(c.Parameters[param]))) elif command in ("G64", "G73", "G83"): - pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + pos = Units.Quantity( + c.Parameters[param], FreeCAD.Units.Length + ) outstring.append( param + format( diff --git a/src/Mod/Path/Path/Post/scripts/dxf_post.py b/src/Mod/Path/Path/Post/scripts/dxf_post.py index e08d66d398..3b1052ad7f 100644 --- a/src/Mod/Path/Path/Post/scripts/dxf_post.py +++ b/src/Mod/Path/Path/Post/scripts/dxf_post.py @@ -140,4 +140,3 @@ def parse(pathobj): objlist.append(obj) return objlist - diff --git a/src/Mod/Path/Path/Post/scripts/refactored_centroid_post.py b/src/Mod/Path/Path/Post/scripts/refactored_centroid_post.py index 825106a123..e1e1fa9271 100644 --- a/src/Mod/Path/Path/Post/scripts/refactored_centroid_post.py +++ b/src/Mod/Path/Path/Post/scripts/refactored_centroid_post.py @@ -206,7 +206,9 @@ def init_arguments_visible(arguments_visible): def init_arguments(values, argument_defaults, arguments_visible): """Initialize the shared argument definitions.""" - parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + parser = PostUtilsArguments.init_shared_arguments( + values, argument_defaults, arguments_visible + ) # # Add any argument definitions that are not shared with all other postprocessors here. # diff --git a/src/Mod/Path/Path/Post/scripts/refactored_grbl_post.py b/src/Mod/Path/Path/Post/scripts/refactored_grbl_post.py index f52952d8f4..cb8d88eea7 100644 --- a/src/Mod/Path/Path/Post/scripts/refactored_grbl_post.py +++ b/src/Mod/Path/Path/Post/scripts/refactored_grbl_post.py @@ -173,7 +173,9 @@ def init_arguments_visible(arguments_visible): def init_arguments(values, argument_defaults, arguments_visible): """Initialize the shared argument definitions.""" - parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + parser = PostUtilsArguments.init_shared_arguments( + values, argument_defaults, arguments_visible + ) # # Add any argument definitions that are not shared with all other postprocessors here. # diff --git a/src/Mod/Path/Path/Post/scripts/refactored_linuxcnc_post.py b/src/Mod/Path/Path/Post/scripts/refactored_linuxcnc_post.py index 23b33dbc07..d5db6983df 100644 --- a/src/Mod/Path/Path/Post/scripts/refactored_linuxcnc_post.py +++ b/src/Mod/Path/Path/Post/scripts/refactored_linuxcnc_post.py @@ -140,7 +140,9 @@ def init_arguments_visible(arguments_visible): def init_arguments(values, argument_defaults, arguments_visible): """Initialize the shared argument definitions.""" - parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + parser = PostUtilsArguments.init_shared_arguments( + values, argument_defaults, arguments_visible + ) # # Add any argument definitions that are not shared with all other postprocessors here. # diff --git a/src/Mod/Path/Path/Post/scripts/refactored_mach3_mach4_post.py b/src/Mod/Path/Path/Post/scripts/refactored_mach3_mach4_post.py index d4b6c4ac17..881f385f2e 100644 --- a/src/Mod/Path/Path/Post/scripts/refactored_mach3_mach4_post.py +++ b/src/Mod/Path/Path/Post/scripts/refactored_mach3_mach4_post.py @@ -147,7 +147,9 @@ def init_arguments_visible(arguments_visible): def init_arguments(values, argument_defaults, arguments_visible): """Initialize the shared argument definitions.""" - parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + parser = PostUtilsArguments.init_shared_arguments( + values, argument_defaults, arguments_visible + ) # # Add any argument definitions that are not shared with all other postprocessors here. # diff --git a/src/Mod/Path/Path/Post/scripts/refactored_test_post.py b/src/Mod/Path/Path/Post/scripts/refactored_test_post.py index 17f0d58be0..6ae414244d 100644 --- a/src/Mod/Path/Path/Post/scripts/refactored_test_post.py +++ b/src/Mod/Path/Path/Post/scripts/refactored_test_post.py @@ -141,7 +141,9 @@ def init_arguments_visible(arguments_visible): def init_arguments(values, argument_defaults, arguments_visible): """Initialize the shared argument definitions.""" - parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + parser = PostUtilsArguments.init_shared_arguments( + values, argument_defaults, arguments_visible + ) # # Add any argument definitions that are not shared with all other postprocessors here. # diff --git a/src/Mod/Path/Path/Preferences.py b/src/Mod/Path/Path/Preferences.py index 5b8eaafd28..013929e520 100644 --- a/src/Mod/Path/Path/Preferences.py +++ b/src/Mod/Path/Path/Preferences.py @@ -169,7 +169,6 @@ def searchPathsTool(sub): return paths - def toolsStoreAbsolutePaths(): return preferences().GetBool(UseAbsoluteToolPaths, False) diff --git a/src/Mod/Path/Path/Tool/Controller.py b/src/Mod/Path/Path/Tool/Controller.py index e8c2c1da0a..e910be6eaa 100644 --- a/src/Mod/Path/Path/Tool/Controller.py +++ b/src/Mod/Path/Path/Tool/Controller.py @@ -198,9 +198,13 @@ class ToolController: else: obj.Tool = None if toolVersion == 1: - Path.Log.error(f"{obj.Name} - legacy Tools no longer supported - ignoring") + Path.Log.error( + f"{obj.Name} - legacy Tools no longer supported - ignoring" + ) else: - Path.Log.error(f"{obj.Name} - unknown Tool version {toolVersion} - ignoring") + Path.Log.error( + f"{obj.Name} - unknown Tool version {toolVersion} - ignoring" + ) if ( obj.Tool and obj.Tool.ViewObject @@ -297,9 +301,7 @@ class ToolController: "App::PropertyLink", "Tool", "Base", - QT_TRANSLATE_NOOP( - "App::Property", "The tool used by this controller" - ), + QT_TRANSLATE_NOOP("App::Property", "The tool used by this controller"), ) @@ -319,6 +321,7 @@ def Create( if FreeCAD.GuiUp and assignViewProvider: from Path.Tool.Gui.Controller import ViewProvider + ViewProvider(obj.ViewObject) if assignTool: @@ -346,4 +349,5 @@ def FromTemplate(template, assignViewProvider=True): FreeCAD.ActiveDocument.removeObject(obj.Name) return None + FreeCAD.Console.PrintLog("Loading Path.Tool.Gui.Controller... done\n") diff --git a/src/Mod/Path/Path/Tool/Gui/BitCmd.py b/src/Mod/Path/Path/Tool/Gui/BitCmd.py index 007717868e..2cc53ad1c2 100644 --- a/src/Mod/Path/Path/Tool/Gui/BitCmd.py +++ b/src/Mod/Path/Path/Tool/Gui/BitCmd.py @@ -83,9 +83,7 @@ class CommandToolBitSave: def selectedTool(self): sel = FreeCADGui.Selection.getSelectionEx() - if 1 == len(sel) and isinstance( - sel[0].Object.Proxy, Path.Tool.Bit.ToolBit - ): + if 1 == len(sel) and isinstance(sel[0].Object.Proxy, Path.Tool.Bit.ToolBit): return sel[0].Object return None @@ -145,9 +143,7 @@ class CommandToolBitLoad: def selectedTool(self): sel = FreeCADGui.Selection.getSelectionEx() - if 1 == len(sel) and isinstance( - sel[0].Object.Proxy, Path.Tool.Bit.ToolBit - ): + if 1 == len(sel) and isinstance(sel[0].Object.Proxy, Path.Tool.Bit.ToolBit): return sel[0].Object return None diff --git a/src/Mod/Path/Path/Tool/Gui/BitLibrary.py b/src/Mod/Path/Path/Tool/Gui/BitLibrary.py index 85b791eeca..25c5f17916 100644 --- a/src/Mod/Path/Path/Tool/Gui/BitLibrary.py +++ b/src/Mod/Path/Path/Tool/Gui/BitLibrary.py @@ -933,7 +933,7 @@ class ToolBitLibrary(object): if not bit: continue - + Path.Log.track(bit) toolitem = tooltemplate.copy() diff --git a/src/Mod/Path/Path/Tool/Gui/Controller.py b/src/Mod/Path/Path/Tool/Gui/Controller.py index 630741908a..29987bdc62 100644 --- a/src/Mod/Path/Path/Tool/Gui/Controller.py +++ b/src/Mod/Path/Path/Tool/Gui/Controller.py @@ -190,8 +190,12 @@ class ToolControllerEditor(object): PathGuiUtil.populateCombobox(self.form, enumTups, comboToPropertyMap) self.vertFeed = PathGuiUtil.QuantitySpinBox(self.form.vertFeed, obj, "VertFeed") - self.horizFeed = PathGuiUtil.QuantitySpinBox(self.form.horizFeed, obj, "HorizFeed") - self.vertRapid = PathGuiUtil.QuantitySpinBox(self.form.vertRapid, obj, "VertRapid") + self.horizFeed = PathGuiUtil.QuantitySpinBox( + self.form.horizFeed, obj, "HorizFeed" + ) + self.vertRapid = PathGuiUtil.QuantitySpinBox( + self.form.vertRapid, obj, "VertRapid" + ) self.horizRapid = PathGuiUtil.QuantitySpinBox( self.form.horizRapid, obj, "HorizRapid" ) diff --git a/src/Mod/Path/PathCommands.py b/src/Mod/Path/PathCommands.py index e42c995038..cbb5f75178 100644 --- a/src/Mod/Path/PathCommands.py +++ b/src/Mod/Path/PathCommands.py @@ -162,9 +162,9 @@ class _ToggleOperation: try: for sel in FreeCADGui.Selection.getSelectionEx(): selProxy = Path.Dressup.Utils.baseOp(sel.Object).Proxy - if not isinstance( - selProxy, Path.Op.Base.ObjectOp - ) and not isinstance(selProxy, Path.Op.Gui.Array.ObjectArray): + if not isinstance(selProxy, Path.Op.Base.ObjectOp) and not isinstance( + selProxy, Path.Op.Gui.Array.ObjectArray + ): return False return True except (IndexError, AttributeError): diff --git a/src/Mod/Path/PathScripts/PathUtils.py b/src/Mod/Path/PathScripts/PathUtils.py index 11ce84ad09..fb16c65ae6 100644 --- a/src/Mod/Path/PathScripts/PathUtils.py +++ b/src/Mod/Path/PathScripts/PathUtils.py @@ -63,6 +63,7 @@ def waiting_effects(function): if not FreeCAD.GuiUp: return function(*args, **kwargs) from PySide import QtGui + QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) res = None try: diff --git a/src/Mod/Path/PathScripts/PathUtilsGui.py b/src/Mod/Path/PathScripts/PathUtilsGui.py index 8e27ecc2c1..b69fcd8e6b 100644 --- a/src/Mod/Path/PathScripts/PathUtilsGui.py +++ b/src/Mod/Path/PathScripts/PathUtilsGui.py @@ -45,9 +45,7 @@ class PathUtilsUserInput(object): # Return the first one and remove all from selection for sel in FreeCADGui.Selection.getSelectionEx(): if hasattr(sel.Object, "Proxy"): - if isinstance( - sel.Object.Proxy, PathToolController.ToolController - ): + if isinstance(sel.Object.Proxy, PathToolController.ToolController): if tc is None: tc = sel.Object FreeCADGui.Selection.removeSelection(sel.Object) diff --git a/src/Mod/Path/PathTests/TestPathDeburr.py b/src/Mod/Path/PathTests/TestPathDeburr.py index 716f1feb53..62eb793702 100644 --- a/src/Mod/Path/PathTests/TestPathDeburr.py +++ b/src/Mod/Path/PathTests/TestPathDeburr.py @@ -35,6 +35,7 @@ class MockToolBit(object): self.FlatRadius = 0 self.CuttingEdgeAngle = 60 + class TestPathDeburr(PathTestUtils.PathTestBase): def test00(self): """Verify chamfer depth and offset for an end mill.""" diff --git a/src/Mod/Path/PathTests/TestPathDressupDogboneII.py b/src/Mod/Path/PathTests/TestPathDressupDogboneII.py new file mode 100644 index 0000000000..63b725cc7b --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathDressupDogboneII.py @@ -0,0 +1,640 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 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 Path +import Path.Base.Generator.dogboneII as dogboneII +import Path.Base.Language as PathLanguage +import Path.Dressup.DogboneII +import PathTests.PathTestUtils as PathTestUtils +import math + +PI = math.pi + + +class MockTB(object): + def __init__(self, dia): + self.Name = "ToolBit" + self.Label = "ToolBit" + self.Diameter = FreeCAD.Units.Quantity(dia, FreeCAD.Units.Length) + + +class MockTC(object): + def __init__(self, dia=2): + self.Name = "TC" + self.Label = "TC" + self.Tool = MockTB(dia) + + +class MockOp(object): + def __init__(self, path, dia=2): + self.Path = Path.Path(path) + self.ToolController = MockTC(dia) + + +class MockFeaturePython(object): + def __init__(self, name): + self.prop = {} + self.addProperty("App::PropertyString", "Name", val=name) + self.addProperty("App::PropertyString", "Label", val=name) + self.addProperty("App::PropertyLink", "Proxy") + self.addProperty("Path::Path", "Path", val=Path.Path()) + + def addProperty(self, typ, name, grp=None, desc=None, val=None): + self.prop[name] = (typ, val) + + def setEditorMode(self, name, mode): + pass + + def __setattr__(self, name, val): + if name == "prop": + return super().__setattr__(name, val) + self.prop[name] = (self.prop[name][0], val) + + def __getattr__(self, name): + if name == "prop": + return super().__getattr__(name) + typ, val = self.prop.get(name, (None, None)) + if typ is None and val is None: + raise AttributeError + if typ == "App::PropertyLength": + if type(val) == float or type(val) == int: + return FreeCAD.Units.Quantity(val, FreeCAD.Units.Length) + return FreeCAD.Units.Quantity(val) + return val + + +def CreateDressup(path): + op = MockOp(path) + obj = MockFeaturePython("DogboneII") + db = Path.Dressup.DogboneII.Proxy(obj, op) + obj.Proxy = db + return obj + + +def MNVR(gcode, begin=None): + # 'turns out the replace() isn't really necessary + # leave it here anyway for clarity + return PathLanguage.Maneuver.FromGCode(gcode.replace("/", "\n"), begin) + + +def INSTR(gcode, begin=None): + return MNVR(gcode, begin).instr[0] + + +def KINK(gcode, begin=None): + maneuver = MNVR(gcode, begin) + if len(maneuver.instr) != 2: + return None + return dogboneII.Kink(maneuver.instr[0], maneuver.instr[1]) + + +class TestDressupDogboneII(PathTestUtils.PathTestBase): + """Unit tests for the DogboneII dressup.""" + + def assertEqualPath(self, path, s): + def cmd2str(cmd): + param = [ + f"{k}{v:g}" if Path.Geom.isRoughly(0, v - int(v)) else f"{k}{v:.2f}" + for k, v in cmd.Parameters.items() + ] + return f"{cmd.Name}{''.join(param)}" + + p = "/".join([cmd2str(cmd) for cmd in path.Commands]) + self.assertEqual(p, s) + + def test00(self): + """Verify adaptive length""" + + def adaptive(k, a, n): + return Path.Dressup.DogboneII.calc_length_adaptive(k, a, n, n) + + if True: + # horizontal bones + self.assertRoughly(adaptive(KINK("G1X1/G1X2"), 0, 1), 0) + self.assertRoughly(adaptive(KINK("G1X1/G1Y1"), 0, 1), 1) + self.assertRoughly(adaptive(KINK("G1X1/G1X2Y1"), 0, 1), 0.414214) + self.assertRoughly(adaptive(KINK("G1X1/G1X0Y1"), 0, 1), 2.414211) + self.assertRoughly(adaptive(KINK("G1X1/G1X0"), 0, 1), 1) + self.assertRoughly(adaptive(KINK("G1X1/G1X0Y-1"), 0, 1), 2.414211) + self.assertRoughly(adaptive(KINK("G1X1/G1X1Y-1"), 0, 1), 1) + self.assertRoughly(adaptive(KINK("G1X1/G1X2Y-1"), 0, 1), 0.414214) + self.assertRoughly(adaptive(KINK("G1X1Y1/G1X0Y2"), 0, 1), 0.414214) + + if True: + # more horizontal and some vertical bones + self.assertRoughly(adaptive(KINK("G1Y1/G1Y2"), 0, 1), 0) + self.assertRoughly(adaptive(KINK("G1Y1/G1Y1X1"), PI, 1), 1) + self.assertRoughly(adaptive(KINK("G1Y1/G1Y2X1"), PI, 1), 0.089820) + self.assertRoughly(adaptive(KINK("G1Y1/G1Y2X1"), PI / 2, 1), 0.414214) + self.assertRoughly(adaptive(KINK("G1Y1/G1Y0X1"), PI / 2, 1), 2.414211) + self.assertRoughly(adaptive(KINK("G1Y1/G1Y0"), 0, 1), 1) + self.assertRoughly(adaptive(KINK("G1Y1/G1Y0X-1"), PI / 2, 1), 2.414211) + self.assertRoughly(adaptive(KINK("G1Y1/G1Y1X-1"), 0, 1), 1) + self.assertRoughly(adaptive(KINK("G1Y1/G1Y2X-1"), 0, 1), 0.089820) + self.assertRoughly(adaptive(KINK("G1Y1/G1Y2X-1"), PI / 2, 1), 0.414214) + + if True: + # dogbones + self.assertRoughly(adaptive(KINK("G1X1/G1Y1"), -PI / 4, 1), 0.414214) + self.assertRoughly(adaptive(KINK("G1X1/G1X0Y1"), -PI / 8, 1), 1.613126) + self.assertRoughly(adaptive(KINK("G1X1/G1Y-1"), PI / 4, 1), 0.414214) + self.assertRoughly(adaptive(KINK("G1X1/G1X0Y-1"), PI / 8, 1), 1.613126) + self.assertRoughly(adaptive(KINK("G1Y1/G1X-1"), PI / 4, 1), 0.414214) + self.assertRoughly(adaptive(KINK("G1Y1/G1X1"), 3 * PI / 4, 1), 0.414214) + self.assertRoughly(adaptive(KINK("G1Y-1/G1X1"), -3 * PI / 4, 1), 0.414214) + self.assertRoughly(adaptive(KINK("G1Y-1/G1X-1"), -PI / 4, 1), 0.414214) + self.assertRoughly(adaptive(KINK("G1X1Y1/G1X0Y2"), 0, 1), 0.414214) + self.assertRoughly(adaptive(KINK("G1X-1Y1/G1X0Y2"), PI, 1), 0.414214) + self.assertRoughly(adaptive(KINK("G1X1Y1/G1X2Y0"), PI / 2, 2), 0.828428) + self.assertRoughly(adaptive(KINK("G1X-1Y-1/G1X-2Y0"), -PI / 2, 2), 0.828428) + self.assertRoughly(adaptive(KINK("G1X-1Y1/G1X-2Y0"), PI / 2, 2), 0.828428) + self.assertRoughly(adaptive(KINK("G1X1Y-1/G1X2Y0"), -PI / 2, 2), 0.828428) + + def test01(self): + """Verify nominal length""" + + def nominal(k, a, n): + return Path.Dressup.DogboneII.calc_length_nominal(k, a, n, 0) + + # neither angle nor kink matter + self.assertRoughly(nominal(KINK("G1X1/G1X2"), 0, 13), 13) + self.assertRoughly(nominal(KINK("G1X1/G1X2"), PI / 2, 13), 13) + self.assertRoughly(nominal(KINK("G1X1/G1X2"), PI, 13), 13) + self.assertRoughly(nominal(KINK("G1X1/G1X2"), -PI / 2, 13), 13) + self.assertRoughly(nominal(KINK("G1X8/G1X12"), 0, 13), 13) + self.assertRoughly(nominal(KINK("G1X9/G1X0"), 0, 13), 13) + self.assertRoughly(nominal(KINK("G1X7/G1X9"), 0, 13), 13) + self.assertRoughly(nominal(KINK("G1X5/G1X1"), 0, 13), 13) + + def test02(self): + """Verify custom length""" + + def custom(k, a, c): + return Path.Dressup.DogboneII.calc_length_custom(k, a, 0, c) + + # neither angle nor kink matter + self.assertRoughly(custom(KINK("G1X1/G1X2"), 0, 7), 7) + self.assertRoughly(custom(KINK("G1X1/G1X2"), PI / 2, 7), 7) + self.assertRoughly(custom(KINK("G1X1/G1X2"), PI, 7), 7) + self.assertRoughly(custom(KINK("G1X1/G1X2"), -PI / 2, 7), 7) + self.assertRoughly(custom(KINK("G1X8/G1X12"), 0, 7), 7) + self.assertRoughly(custom(KINK("G1X9/G1X0"), 0, 7), 7) + self.assertRoughly(custom(KINK("G1X7/G1X9"), 0, 7), 7) + self.assertRoughly(custom(KINK("G1X5/G1X1"), 0, 7), 7) + + def test10(self): + """Verify basic op dressup""" + + obj = CreateDressup("G1X10/G1Y20") + obj.Incision = Path.Dressup.DogboneII.Incision.Fixed + obj.Style = Path.Dressup.DogboneII.Style.Tbone_H + + # bones on right side + obj.Side = Path.Dressup.DogboneII.Side.Right + obj.Proxy.execute(obj) + self.assertEqualPath(obj.Path, "G1X10/G1X11/G1X10/G1Y20") + + # no bones on left side + obj.Side = Path.Dressup.DogboneII.Side.Left + obj.Proxy.execute(obj) + self.assertEqualPath(obj.Path, "G1X10/G1Y20") + + def test11(self): + """Verify retaining non-move instructions""" + + obj = CreateDressup("G1X10/(some comment)/G1Y20") + obj.Incision = Path.Dressup.DogboneII.Incision.Fixed + obj.Style = Path.Dressup.DogboneII.Style.Tbone_H + + # bone on right side + obj.Side = Path.Dressup.DogboneII.Side.Right + obj.Proxy.execute(obj) + self.assertEqualPath(obj.Path, "G1X10/(some comment)/G1X11/G1X10/G1Y20") + + # no bone on left side + obj.Side = Path.Dressup.DogboneII.Side.Left + obj.Proxy.execute(obj) + self.assertEqualPath(obj.Path, "G1X10/(some comment)/G1Y20") + + def test20(self): + """Verify bone on plunge moves""" + + obj = CreateDressup("G0Z10/G1Z0/G1X10/G1Y10/G1X0/G1Y0/G0Z10") + obj.Incision = Path.Dressup.DogboneII.Incision.Fixed + obj.Style = Path.Dressup.DogboneII.Style.Tbone_H + obj.Side = Path.Dressup.DogboneII.Side.Right + + obj.Proxy.execute(obj) + self.assertEqualPath( + obj.Path, + "G0Z10/G1Z0/G1X10/G1X11/G1X10/G1Y10/G1X11/G1X10/G1X0/G1X-1/G1X0/G1Y0/G1X-1/G1X0/G0Z10", + ) + + def test21(self): + """Verify ignoring plunge moves that don't connect""" + + obj = CreateDressup("G0Z10/G1Z0/G1X10/G1Y10/G1X0/G1Y5/G0Z10") + obj.Incision = Path.Dressup.DogboneII.Incision.Fixed + obj.Style = Path.Dressup.DogboneII.Style.Tbone_H + obj.Side = Path.Dressup.DogboneII.Side.Right + + obj.Proxy.execute(obj) + self.assertEqualPath( + obj.Path, + "G0Z10/G1Z0/G1X10/G1X11/G1X10/G1Y10/G1X11/G1X10/G1X0/G1X-1/G1X0/G1Y5/G0Z10", + ) + + def test30(self): + """Verify TBone_V style""" + + def check_tbone(d, i, path, out, right): + obj = CreateDressup(f"({d}.{i:02})/{path}") + obj.Incision = Path.Dressup.DogboneII.Incision.Fixed + if right: + obj.Side = Path.Dressup.DogboneII.Side.Right + else: + obj.Side = Path.Dressup.DogboneII.Side.Left + obj.Style = Path.Dressup.DogboneII.Style.Tbone_V + obj.Proxy.execute(obj) + self.assertEqualPath(obj.Path, f"({d}.{i:02})/{out}") + + # test data with a horizontal lead in + test_data_h = [ + # top right quadrant + ("G1X10Y0/G1X10Y10", "G1X10Y0/G1Y-1/G1Y0/G1X10Y10", True), + ("G1X10Y0/G1X20Y10", "G1X10Y0/G1Y-1/G1Y0/G1X20Y10", True), + ("G1X10Y0/G1X90Y10", "G1X10Y0/G1Y-1/G1Y0/G1X90Y10", True), + ("G1X10Y0/G1X0Y10", "G1X10Y0/G1Y-1/G1Y0/G1X0Y10", True), + # bottom right quadrant + ("G1X10Y0/G1X90Y-10", "G1X10Y0/G1Y1/G1Y0/G1X90Y-10", False), + ("G1X10Y0/G1X20Y-10", "G1X10Y0/G1Y1/G1Y0/G1X20Y-10", False), + ("G1X10Y0/G1X10Y-10", "G1X10Y0/G1Y1/G1Y0/G1X10Y-10", False), + ("G1X10Y0/G1X0Y-10", "G1X10Y0/G1Y1/G1Y0/G1X0Y-10", False), + # top left quadrant + ("G1X-10Y0/G1X-10Y10", "G1X-10Y0/G1Y-1/G1Y0/G1X-10Y10", False), + ("G1X-10Y0/G1X-20Y10", "G1X-10Y0/G1Y-1/G1Y0/G1X-20Y10", False), + ("G1X-10Y0/G1X-90Y10", "G1X-10Y0/G1Y-1/G1Y0/G1X-90Y10", False), + ("G1X-10Y0/G1X-0Y10", "G1X-10Y0/G1Y-1/G1Y0/G1X-0Y10", False), + # bottom left quadrant + ("G1X-10Y0/G1X-90Y-10", "G1X-10Y0/G1Y1/G1Y0/G1X-90Y-10", True), + ("G1X-10Y0/G1X-20Y-10", "G1X-10Y0/G1Y1/G1Y0/G1X-20Y-10", True), + ("G1X-10Y0/G1X-10Y-10", "G1X-10Y0/G1Y1/G1Y0/G1X-10Y-10", True), + ("G1X-10Y0/G1X-0Y-10", "G1X-10Y0/G1Y1/G1Y0/G1X-0Y-10", True), + ] + + for i, (path, out, right) in enumerate(test_data_h): + check_tbone("h", i, path, out, right) + + # test data with a vertical lead in + test_data_v = [ + # top right quadrant + ("G1X0Y10/G1X10Y10", "G1X0Y10/G1Y11/G1Y10/G1X10Y10", False), + ("G1X0Y10/G1X10Y20", "G1X0Y10/G1Y11/G1Y10/G1X10Y20", False), + ("G1X0Y10/G1X10Y90", "G1X0Y10/G1Y11/G1Y10/G1X10Y90", False), + ("G1X0Y10/G1X10Y0", "G1X0Y10/G1Y11/G1Y10/G1X10Y0", False), + # bottom right quadrant + ("G1X0Y-10/G1X10Y-90", "G1X0Y-10/G1Y-11/G1Y-10/G1X10Y-90", True), + ("G1X0Y-10/G1X10Y-20", "G1X0Y-10/G1Y-11/G1Y-10/G1X10Y-20", True), + ("G1X0Y-10/G1X10Y-10", "G1X0Y-10/G1Y-11/G1Y-10/G1X10Y-10", True), + ("G1X0Y-10/G1X10Y-0", "G1X0Y-10/G1Y-11/G1Y-10/G1X10Y-0", True), + # top left quadrant + ("G1X0Y10/G1X-10Y10", "G1X0Y10/G1Y11/G1Y10/G1X-10Y10", True), + ("G1X0Y10/G1X-10Y20", "G1X0Y10/G1Y11/G1Y10/G1X-10Y20", True), + ("G1X0Y10/G1X-10Y90", "G1X0Y10/G1Y11/G1Y10/G1X-10Y90", True), + ("G1X0Y10/G1X-10Y0", "G1X0Y10/G1Y11/G1Y10/G1X-10Y0", True), + # bottom left quadrant + ("G1X0Y-10/G1X-10Y-90", "G1X0Y-10/G1Y-11/G1Y-10/G1X-10Y-90", False), + ("G1X0Y-10/G1X-10Y-20", "G1X0Y-10/G1Y-11/G1Y-10/G1X-10Y-20", False), + ("G1X0Y-10/G1X-10Y-10", "G1X0Y-10/G1Y-11/G1Y-10/G1X-10Y-10", False), + ("G1X0Y-10/G1X-10Y-0", "G1X0Y-10/G1Y-11/G1Y-10/G1X-10Y-0", False), + ] + + for i, (path, out, right) in enumerate(test_data_v): + check_tbone("v", i, path, out, right) + + def test40(self): + """Verify TBone_S style""" + + def check_tbone_s(d, i, path, out, right): + obj = CreateDressup(f"(m{d}.{i:02})/{path}") + obj.Incision = Path.Dressup.DogboneII.Incision.Fixed + if right: + obj.Side = Path.Dressup.DogboneII.Side.Right + else: + obj.Side = Path.Dressup.DogboneII.Side.Left + obj.Style = Path.Dressup.DogboneII.Style.Tbone_S + obj.Proxy.execute(obj) + self.assertEqualPath(obj.Path, f"(m{d}.{i:02})/{out}") + + # short edge m0 + test_data_0 = [ + # CCW + ("G1X10/G1Y20", "G1X10/G1Y-1/G1Y0/G1Y20", True), + ("G1X10Y10/G1X-10Y30", "G1X10Y10/G1X10.71Y9.29/G1X10Y10/G1X-10Y30", True), + ("G1Y10/G1X-20", "G1Y10/G1X1/G1X0/G1X-20", True), + ( + "G1X-10Y10/G1X-30Y-10", + "G1X-10Y10/G1X-9.29Y10.71/G1X-10Y10/G1X-30Y-10", + True, + ), + ("G1X-10/G1Y-20", "G1X-10/G1Y1/G1Y0/G1Y-20", True), + ( + "G1X-10Y-10/G1X10Y-30", + "G1X-10Y-10/G1X-10.71Y-9.29/G1X-10Y-10/G1X10Y-30", + True, + ), + ("G1Y-10/G1X20", "G1Y-10/G1X-1/G1X0/G1X20", True), + ("G1X10Y-10/G1X30Y10", "G1X10Y-10/G1X9.29Y-10.71/G1X10Y-10/G1X30Y10", True), + # CW + ("G1X10/G1Y-20", "G1X10/G1Y1/G1Y0/G1Y-20", False), + ("G1X10Y10/G1X30Y-10", "G1X10Y10/G1X9.29Y10.71/G1X10Y10/G1X30Y-10", False), + ("G1Y10/G1X20", "G1Y10/G1X-1/G1X0/G1X20", False), + ( + "G1X-10Y10/G1X10Y30", + "G1X-10Y10/G1X-10.71Y9.29/G1X-10Y10/G1X10Y30", + False, + ), + ("G1X-10/G1Y20", "G1X-10/G1Y-1/G1Y0/G1Y20", False), + ( + "G1X-10Y-10/G1X-30Y10", + "G1X-10Y-10/G1X-9.29Y-10.71/G1X-10Y-10/G1X-30Y10", + False, + ), + ("G1Y-10/G1X-20", "G1Y-10/G1X1/G1X0/G1X-20", False), + ( + "G1X10Y-10/G1X-10Y-30", + "G1X10Y-10/G1X10.71Y-9.29/G1X10Y-10/G1X-10Y-30", + False, + ), + ] + + for i, (path, out, right) in enumerate(test_data_0): + check_tbone_s("0", i, path, out, right) + + # short edge m1 + test_data_1 = [ + # CCW + ("G1X20/G1Y10", "G1X20/G1X21/G1X20/G1Y10", True), + ("G1X20Y20/G1X10Y30", "G1X20Y20/G1X20.71Y20.71/G1X20Y20/G1X10Y30", True), + ("G1Y20/G1X-10", "G1Y20/G1Y21/G1Y20/G1X-10", True), + ( + "G1X-20Y20/G1X-30Y10", + "G1X-20Y20/G1X-20.71Y20.71/G1X-20Y20/G1X-30Y10", + True, + ), + ("G1X-20/G1Y-10", "G1X-20/G1X-21/G1X-20/G1Y-10", True), + ( + "G1X-20Y-20/G1X-10Y-30", + "G1X-20Y-20/G1X-20.71Y-20.71/G1X-20Y-20/G1X-10Y-30", + True, + ), + ("G1Y-20/G1X10", "G1Y-20/G1Y-21/G1Y-20/G1X10", True), + ( + "G1X20Y-20/G1X30Y-10", + "G1X20Y-20/G1X20.71Y-20.71/G1X20Y-20/G1X30Y-10", + True, + ), + # CW + ("G1X20/G1Y-10", "G1X20/G1X21/G1X20/G1Y-10", False), + ("G1X20Y20/G1X30Y10", "G1X20Y20/G1X20.71Y20.71/G1X20Y20/G1X30Y10", False), + ("G1Y20/G1X10", "G1Y20/G1Y21/G1Y20/G1X10", False), + ( + "G1X-20Y20/G1X-10Y30", + "G1X-20Y20/G1X-20.71Y20.71/G1X-20Y20/G1X-10Y30", + False, + ), + ("G1X-20/G1Y10", "G1X-20/G1X-21/G1X-20/G1Y10", False), + ( + "G1X-20Y-20/G1X-30Y-10", + "G1X-20Y-20/G1X-20.71Y-20.71/G1X-20Y-20/G1X-30Y-10", + False, + ), + ("G1Y-20/G1X-10", "G1Y-20/G1Y-21/G1Y-20/G1X-10", False), + ( + "G1X20Y-20/G1X10Y-30", + "G1X20Y-20/G1X20.71Y-20.71/G1X20Y-20/G1X10Y-30", + False, + ), + ] + + for i, (path, out, right) in enumerate(test_data_1): + check_tbone_s("1", i, path, out, right) + + def test50(self): + """Verify TBone_L style""" + + def check_tbone_l(d, i, path, out, right): + obj = CreateDressup(f"(m{d}.{i:02})/{path}") + obj.Incision = Path.Dressup.DogboneII.Incision.Fixed + if right: + obj.Side = Path.Dressup.DogboneII.Side.Right + else: + obj.Side = Path.Dressup.DogboneII.Side.Left + obj.Style = Path.Dressup.DogboneII.Style.Tbone_L + obj.Proxy.execute(obj) + self.assertEqualPath(obj.Path, f"(m{d}.{i:02})/{out}") + + # long edge m1 + test_data_1 = [ + # CCW + ("G1X10/G1Y20", "G1X10/G1X11/G1X10/G1Y20", True), + ("G1X10Y10/G1X-10Y30", "G1X10Y10/G1X10.71Y10.71/G1X10Y10/G1X-10Y30", True), + ("G1Y10/G1X-20", "G1Y10/G1Y11/G1Y10/G1X-20", True), + ( + "G1X-10Y10/G1X-30Y-10", + "G1X-10Y10/G1X-10.71Y10.71/G1X-10Y10/G1X-30Y-10", + True, + ), + ("G1X-10/G1Y-20", "G1X-10/G1X-11/G1X-10/G1Y-20", True), + ( + "G1X-10Y-10/G1X10Y-30", + "G1X-10Y-10/G1X-10.71Y-10.71/G1X-10Y-10/G1X10Y-30", + True, + ), + ("G1Y-10/G1X20", "G1Y-10/G1Y-11/G1Y-10/G1X20", True), + ( + "G1X10Y-10/G1X30Y10", + "G1X10Y-10/G1X10.71Y-10.71/G1X10Y-10/G1X30Y10", + True, + ), + # CW + ("G1X10/G1Y-20", "G1X10/G1X11/G1X10/G1Y-20", False), + ("G1X10Y10/G1X30Y-10", "G1X10Y10/G1X10.71Y10.71/G1X10Y10/G1X30Y-10", False), + ("G1Y10/G1X20", "G1Y10/G1Y11/G1Y10/G1X20", False), + ( + "G1X-10Y10/G1X10Y30", + "G1X-10Y10/G1X-10.71Y10.71/G1X-10Y10/G1X10Y30", + False, + ), + ("G1X-10/G1Y20", "G1X-10/G1X-11/G1X-10/G1Y20", False), + ( + "G1X-10Y-10/G1X-30Y10", + "G1X-10Y-10/G1X-10.71Y-10.71/G1X-10Y-10/G1X-30Y10", + False, + ), + ("G1Y-10/G1X-20", "G1Y-10/G1Y-11/G1Y-10/G1X-20", False), + ( + "G1X10Y-10/G1X-10Y-30", + "G1X10Y-10/G1X10.71Y-10.71/G1X10Y-10/G1X-10Y-30", + False, + ), + ] + + for i, (path, out, right) in enumerate(test_data_1): + check_tbone_l("1", i, path, out, right) + + # long edge m0 + test_data_0 = [ + # CCW + ("G1X20/G1Y10", "G1X20/G1Y-1/G1Y0/G1Y10", True), + ("G1X20Y20/G1X10Y30", "G1X20Y20/G1X20.71Y19.29/G1X20Y20/G1X10Y30", True), + ("G1Y20/G1X-10", "G1Y20/G1X1/G1X0/G1X-10", True), + ( + "G1X-20Y20/G1X-30Y10", + "G1X-20Y20/G1X-19.29Y20.71/G1X-20Y20/G1X-30Y10", + True, + ), + ("G1X-20/G1Y-10", "G1X-20/G1Y1/G1Y0/G1Y-10", True), + ( + "G1X-20Y-20/G1X-10Y-30", + "G1X-20Y-20/G1X-20.71Y-19.29/G1X-20Y-20/G1X-10Y-30", + True, + ), + ("G1Y-20/G1X10", "G1Y-20/G1X-1/G1X0/G1X10", True), + ( + "G1X20Y-20/G1X30Y-10", + "G1X20Y-20/G1X19.29Y-20.71/G1X20Y-20/G1X30Y-10", + True, + ), + # CW + ("G1X20/G1Y-10", "G1X20/G1Y1/G1Y0/G1Y-10", False), + ("G1X20Y20/G1X30Y10", "G1X20Y20/G1X19.29Y20.71/G1X20Y20/G1X30Y10", False), + ("G1Y20/G1X10", "G1Y20/G1X-1/G1X0/G1X10", False), + ( + "G1X-20Y20/G1X-10Y30", + "G1X-20Y20/G1X-20.71Y19.29/G1X-20Y20/G1X-10Y30", + False, + ), + ("G1X-20/G1Y10", "G1X-20/G1Y-1/G1Y0/G1Y10", False), + ( + "G1X-20Y-20/G1X-30Y-10", + "G1X-20Y-20/G1X-19.29Y-20.71/G1X-20Y-20/G1X-30Y-10", + False, + ), + ("G1Y-20/G1X-10", "G1Y-20/G1X1/G1X0/G1X-10", False), + ( + "G1X20Y-20/G1X10Y-30", + "G1X20Y-20/G1X20.71Y-19.29/G1X20Y-20/G1X10Y-30", + False, + ), + ] + + for i, (path, out, right) in enumerate(test_data_0): + check_tbone_l("0", i, path, out, right) + + def test60(self): + """Verify Dogbone style""" + + obj = CreateDressup("G1X10/G1Y20") + obj.Incision = Path.Dressup.DogboneII.Incision.Fixed + obj.Side = Path.Dressup.DogboneII.Side.Right + + obj.Style = Path.Dressup.DogboneII.Style.Dogbone + obj.Proxy.execute(obj) + self.assertEqualPath(obj.Path, "G1X10/G1X10.71Y-0.71/G1X10Y0/G1Y20") + + def test70(self): + """Verify custom length.""" + + obj = CreateDressup("G0Z10/G1Z0/G1X10/G1Y10/G1X0/G1Y0/G0Z10") + obj.Style = Path.Dressup.DogboneII.Style.Tbone_H + obj.Side = Path.Dressup.DogboneII.Side.Right + + obj.Incision = Path.Dressup.DogboneII.Incision.Custom + obj.Custom = 3 + obj.Proxy.execute(obj) + self.assertEqualPath( + obj.Path, + "G0Z10/G1Z0/G1X10/G1X13/G1X10/G1Y10/G1X13/G1X10/G1X0/G1X-3/G1X0/G1Y0/G1X-3/G1X0/G0Z10", + ) + + obj.Custom = 2 + obj.Proxy.execute(obj) + self.assertEqualPath( + obj.Path, + "G0Z10/G1Z0/G1X10/G1X12/G1X10/G1Y10/G1X12/G1X10/G1X0/G1X-2/G1X0/G1Y0/G1X-2/G1X0/G0Z10", + ) + + def test80(self): + """Verify adaptive length.""" + + obj = CreateDressup("G1X10/G1Y20") + obj.Incision = Path.Dressup.DogboneII.Incision.Adaptive + obj.Side = Path.Dressup.DogboneII.Side.Right + + obj.Style = Path.Dressup.DogboneII.Style.Dogbone + obj.Proxy.execute(obj) + self.assertEqualPath(obj.Path, "G1X10/G1X10.29Y-0.29/G1X10Y0/G1Y20") + + def test81(self): + """Verify adaptive length II.""" + + obj = CreateDressup("G1X10/G1X20Y20") + obj.Incision = Path.Dressup.DogboneII.Incision.Adaptive + obj.Side = Path.Dressup.DogboneII.Side.Right + + obj.Style = Path.Dressup.DogboneII.Style.Dogbone + obj.Proxy.execute(obj) + self.assertEqualPath(obj.Path, "G1X10/G1X10.09Y-0.15/G1X10Y0/G1X20Y20") + + def test90(self): + """Verify dogbone blacklist""" + + obj = CreateDressup("G0Z10/G1Z0/G1X10/G1Y10/G1X0/G1Y0/G0Z10") + obj.Incision = Path.Dressup.DogboneII.Incision.Fixed + obj.Style = Path.Dressup.DogboneII.Style.Tbone_H + obj.Side = Path.Dressup.DogboneII.Side.Right + obj.BoneBlacklist = [0, 2] + obj.Proxy.execute(obj) + self.assertEqualPath( + obj.Path, "G0Z10/G1Z0/G1X10/G1Y10/G1X11/G1X10/G1X0/G1Y0/G1X-1/G1X0/G0Z10" + ) + return obj + + def test91(self): + """Verify dogbone on dogbone""" + + obj = self.test90() + + obj2 = MockFeaturePython("DogboneII_") + db2 = Path.Dressup.DogboneII.Proxy(obj2, obj) + obj2.Proxy = db2 + obj2.Incision = Path.Dressup.DogboneII.Incision.Fixed + obj2.Style = Path.Dressup.DogboneII.Style.Tbone_H + obj2.Side = Path.Dressup.DogboneII.Side.Right + obj2.BoneBlacklist = [1] + obj2.Proxy.execute(obj2) + self.assertEqualPath( + obj2.Path, + "G0Z10/G1Z0/G1X10/G1X11/G1X10/G1Y10/G1X11/G1X10/G1X0/G1X-1/G1X0/G1Y0/G1X-1/G1X0/G0Z10", + ) diff --git a/src/Mod/Path/PathTests/TestPathDrillable.py b/src/Mod/Path/PathTests/TestPathDrillable.py index f333585e2e..f01988dce2 100644 --- a/src/Mod/Path/PathTests/TestPathDrillable.py +++ b/src/Mod/Path/PathTests/TestPathDrillable.py @@ -93,9 +93,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertTrue( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) # Passing explicit vector self.assertTrue( @@ -125,9 +123,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertFalse( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) # raised cylinder candidate = self.obj.getSubObject("Face32") @@ -136,9 +132,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertFalse( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) # cylinder on slope candidate = self.obj.getSubObject("Face24") @@ -146,9 +140,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertTrue( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) # Circular Faces candidate = self.obj.getSubObject("Face54") @@ -157,15 +149,11 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertTrue( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) # Passing explicit vector self.assertTrue( - Drillable.isDrillable( - self.obj.Shape, candidate, vector=App.Vector(0, 0, 1) - ) + Drillable.isDrillable(self.obj.Shape, candidate, vector=App.Vector(0, 0, 1)) ) # Drilling with smaller bit @@ -185,9 +173,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertTrue( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) # Passing explicit vector self.assertTrue( @@ -202,9 +188,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertTrue( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) # interrupted Face candidate = self.obj.getSubObject("Face50") @@ -212,9 +196,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertFalse( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) # donut face candidate = self.obj.getSubObject("Face48") @@ -222,9 +204,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertTrue( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) # Test edges # circular edge @@ -234,15 +214,11 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertTrue( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) # Passing explicit vector self.assertTrue( - Drillable.isDrillable( - self.obj.Shape, candidate, vector=App.Vector(0, 0, 1) - ) + Drillable.isDrillable(self.obj.Shape, candidate, vector=App.Vector(0, 0, 1)) ) # Drilling with smaller bit @@ -264,15 +240,11 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertTrue( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertTrue(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) # Passing explicit vector self.assertTrue( - Drillable.isDrillable( - self.obj.Shape, candidate, vector=App.Vector(0, 1, 0) - ) + Drillable.isDrillable(self.obj.Shape, candidate, vector=App.Vector(0, 1, 0)) ) # incomplete circular edge @@ -281,9 +253,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertFalse( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) # elliptical edge candidate = self.obj.getSubObject("Edge56") @@ -291,9 +261,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase): self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate)) # Passing None as vector - self.assertFalse( - Drillable.isDrillable(self.obj.Shape, candidate, vector=None) - ) + self.assertFalse(Drillable.isDrillable(self.obj.Shape, candidate, vector=None)) def test20(self): """Test getDrillableTargets""" @@ -303,7 +271,5 @@ class TestPathDrillable(PathTestUtils.PathTestBase): results = Drillable.getDrillableTargets(self.obj, vector=None) self.assertEqual(len(results), 20) - results = Drillable.getDrillableTargets( - self.obj, ToolDiameter=20, vector=None - ) + results = Drillable.getDrillableTargets(self.obj, ToolDiameter=20, vector=None) self.assertEqual(len(results), 5) diff --git a/src/Mod/Path/PathTests/TestPathGeneratorDogboneII.py b/src/Mod/Path/PathTests/TestPathGeneratorDogboneII.py new file mode 100644 index 0000000000..a9d71e9097 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathGeneratorDogboneII.py @@ -0,0 +1,354 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 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 Path +import Path.Base.Generator.dogboneII as dogboneII +import Path.Base.Language as PathLanguage +import PathTests.PathTestUtils as PathTestUtils +import math + + +# Path.Log.setLevel(Path.Log.Level.DEBUG) +Path.Log.setLevel(Path.Log.Level.NOTICE) + +PI = math.pi +DebugMode = Path.Log.getLevel(Path.Log.thisModule()) == Path.Log.Level.DEBUG + + +def createKinks(maneuver): + k = [] + moves = maneuver.getMoves() + if moves: + move0 = moves[0] + prev = move0 + for m in moves[1:]: + k.append(dogboneII.Kink(prev, m)) + prev = m + if Path.Geom.pointsCoincide(move0.positionBegin(), prev.positionEnd()): + k.append(dogboneII.Kink(prev, move0)) + return k + + +def findDogboneKinks(maneuver, threshold): + if threshold > 0: + return [k for k in createKinks(maneuver) if k.deflection() > threshold] + if threshold < 0: + return [k for k in createKinks(maneuver) if k.deflection() < threshold] + return createKinks(maneuver) + + +def MNVR(gcode, begin=None): + # 'turns out the replace() isn't really necessary + # leave it here anyway for clarity + return PathLanguage.Maneuver.FromGCode(gcode.replace("/", "\n"), begin) + + +def INSTR(gcode, begin=None): + return MNVR(gcode, begin).instr[0] + + +def KINK(gcode, begin=None): + maneuver = MNVR(gcode, begin) + if len(maneuver.instr) != 2: + return None + return dogboneII.Kink(maneuver.instr[0], maneuver.instr[1]) + + +def GEN(generator, length): + return generator(lambda k, a, n, c: n, length, 1) + + +class TestGeneratorDogboneII(PathTestUtils.PathTestBase): + """Unit tests for the dogboneII generator.""" + + def assertKinks(self, maneuver, s): + kinks = [f"{k.deflection():4.2f}" for k in createKinks(maneuver)] + self.assertEqual(f"[{', '.join(kinks)}]", s) + + def assertBones(self, maneuver, threshold, s): + bones = [ + f"({int(b.x())},{int(b.y())})" + for b in findDogboneKinks(maneuver, threshold) + ] + self.assertEqual(f"[{', '.join(bones)}]", s) + + def assertBone(self, bone, s, digits=0): + if DebugMode and FreeCAD.GuiUp: + Path.show(dogboneII.kink_to_path(bone.kink)) + FreeCAD.ActiveDocument.Objects[-1].Visibility = False + Path.show(dogboneII.bone_to_path(bone)) + FreeCAD.ActiveDocument.Objects[-1].Visibility = False + Path.Log.debug(f"{bone.kink} : {bone.angle / PI:.2f}") + + b = [i.str(digits) for i in bone.instr] + self.assertEqual(f"[{', '.join(b)}]", s) + + def test20(self): + """Verify kinks of maneuvers""" + self.assertKinks(MNVR("G1X1/G1Y1"), "[1.57]") + self.assertKinks(MNVR("G1X1/G1Y-1"), "[-1.57]") + self.assertKinks(MNVR("G1X1/G1Y1/G1X0"), "[1.57, 1.57]") + self.assertKinks(MNVR("G1X1/G1Y1/G1X0/G1Y0"), "[1.57, 1.57, 1.57, 1.57]") + + self.assertKinks(MNVR("G1Y1/G1X1"), "[-1.57]") + self.assertKinks(MNVR("G1Y1/G1X1/G1Y0"), "[-1.57, -1.57]") + self.assertKinks(MNVR("G1Y1/G1X1/G1Y0/G1X0"), "[-1.57, -1.57, -1.57, -1.57]") + + # tangential arc moves + self.assertKinks(MNVR("G1X1/G3Y2J1"), "[0.00]") + self.assertKinks(MNVR("G1X1/G3Y2J1G1X0"), "[0.00, 0.00]") + + # folding back arc moves + self.assertKinks(MNVR("G1X1/G2Y2J1"), "[-3.14]") + self.assertKinks(MNVR("G1X1/G2Y2J1G1X0"), "[-3.14, 3.14]") + + def test30(self): + """Verify dogbone detection""" + self.assertBones( + MNVR("G1X1/G1Y1/G1X0/G1Y0"), PI / 4, "[(1,0), (1,1), (0,1), (0,0)]" + ) + self.assertBones(MNVR("G1X1/G1Y1/G1X0/G1Y0"), -PI / 4, "[]") + + # no bones on flat angle + self.assertBones(MNVR("G1X1/G1X3Y1/G1X0/G1Y0"), PI / 4, "[(3,1), (0,1), (0,0)]") + self.assertBones(MNVR("G1X1/G1X3Y1/G1X0/G1Y0"), -PI / 4, "[]") + + # no bones on tangential arc + self.assertBones(MNVR("G1X1/G3Y2J1/G1X0/G1Y0"), PI / 4, "[(0,2), (0,0)]") + self.assertBones(MNVR("G1X1/G3Y2J1/G1X0/G1Y0"), -PI / 4, "[]") + + # a bone on perpendicular arc + self.assertBones( + MNVR("G1X1/G3X3I1/G1Y1/G1X0/G1Y0"), PI / 4, "[(3,1), (0,1), (0,0)]" + ) + self.assertBones(MNVR("G1X1/G3X3I1/G1Y1/G1X0/G1Y0"), -PI / 4, "[(1,0)]") + + def test40(self): + """Verify horizontal t-bone creation""" + # Uses test data from test30, if that broke, this can't succeed + + horizontal = GEN(dogboneII.GeneratorTBoneHorizontal, 1) + + # single move right + maneuver = MNVR("G1X1/G1Y1") + kinks = findDogboneKinks(maneuver, PI / 4) + self.assertEqual(len(kinks), 1) + k = kinks[0] + p = k.position() + self.assertEqual(f"({int(p.x)}, {int(p.y)})", "(1, 0)") + bone = horizontal.generate(k) + self.assertBone(bone, "[G1{X: 2}, G1{X: 1}]") + + # full loop CCW + kinks = findDogboneKinks(MNVR("G1X1/G1Y1/G1X0/G1Y0"), PI / 4) + bones = [horizontal.generate(k) for k in kinks] + self.assertEqual(len(bones), 4) + self.assertBone(bones[0], "[G1{X: 2}, G1{X: 1}]") + self.assertBone(bones[1], "[G1{X: 2}, G1{X: 1}]") + self.assertBone(bones[2], "[G1{X: -1}, G1{X: 0}]") + self.assertBone(bones[3], "[G1{X: -1}, G1{X: 0}]") + + # single move left + maneuver = MNVR("G1X1/G1Y-1") + kinks = findDogboneKinks(maneuver, -PI / 4) + self.assertEqual(len(kinks), 1) + k = kinks[0] + p = k.position() + self.assertEqual(f"({int(p.x)}, {int(p.y)})", "(1, 0)") + bone = horizontal.generate(k) + self.assertBone(bone, "[G1{X: 2}, G1{X: 1}]") + + # full loop CW + kinks = findDogboneKinks(MNVR("G1X1/G1Y-1/G1X0/G1Y0"), -PI / 4) + bones = [horizontal.generate(k) for k in kinks] + self.assertEqual(len(bones), 4) + self.assertBone(bones[0], "[G1{X: 2}, G1{X: 1}]") + self.assertBone(bones[1], "[G1{X: 2}, G1{X: 1}]") + self.assertBone(bones[2], "[G1{X: -1}, G1{X: 0}]") + self.assertBone(bones[3], "[G1{X: -1}, G1{X: 0}]") + + # bones on arcs + kinks = findDogboneKinks(MNVR("G1X1/G3X3I1/G1Y1/G1X0/G1Y0"), PI / 4) + bones = [horizontal.generate(k) for k in kinks] + self.assertEqual(len(bones), 3) + self.assertBone(bones[0], "[G1{X: 4}, G1{X: 3}]") + self.assertBone(bones[1], "[G1{X: -1}, G1{X: 0}]") + self.assertBone(bones[2], "[G1{X: -1}, G1{X: 0}]") + + # bones on arcs + kinks = findDogboneKinks(MNVR("G1X1/G3X3I1/G1Y1/G1X0/G1Y0"), -PI / 4) + bones = [horizontal.generate(k) for k in kinks] + self.assertEqual(len(bones), 1) + self.assertBone(bones[0], "[G1{X: 2}, G1{X: 1}]") + + def test50(self): + """Verify vertical t-bone creation""" + # Uses test data from test30, if that broke, this can't succeed + + vertical = GEN(dogboneII.GeneratorTBoneVertical, 1) + + # single move right + maneuver = MNVR("G1X1/G1Y1") + kinks = findDogboneKinks(maneuver, PI / 4) + self.assertEqual(len(kinks), 1) + k = kinks[0] + p = k.position() + self.assertEqual(f"({int(p.x)}, {int(p.y)})", "(1, 0)") + bone = vertical.generate(k) + self.assertBone(bone, "[G1{Y: -1}, G1{Y: 0}]") + + # full loop CCW + kinks = findDogboneKinks(MNVR("G1X1/G1Y1/G1X0/G1Y0"), PI / 4) + bones = [vertical.generate(k) for k in kinks] + self.assertEqual(len(bones), 4) + self.assertBone(bones[0], "[G1{Y: -1}, G1{Y: 0}]") + self.assertBone(bones[1], "[G1{Y: 2}, G1{Y: 1}]") + self.assertBone(bones[2], "[G1{Y: 2}, G1{Y: 1}]") + self.assertBone(bones[3], "[G1{Y: -1}, G1{Y: 0}]") + + # single move left + maneuver = MNVR("G1X1/G1Y-1") + kinks = findDogboneKinks(maneuver, -PI / 4) + self.assertEqual(len(kinks), 1) + k = kinks[0] + p = k.position() + self.assertEqual(f"({int(p.x)}, {int(p.y)})", "(1, 0)") + bone = vertical.generate(k) + self.assertBone(bone, "[G1{Y: 1}, G1{Y: 0}]") + + # full loop CW + kinks = findDogboneKinks(MNVR("G1X1/G1Y-1/G1X0/G1Y0"), -PI / 4) + bones = [vertical.generate(k) for k in kinks] + self.assertEqual(len(bones), 4) + self.assertBone(bones[0], "[G1{Y: 1}, G1{Y: 0}]") + self.assertBone(bones[1], "[G1{Y: -2}, G1{Y: -1}]") + self.assertBone(bones[2], "[G1{Y: -2}, G1{Y: -1}]") + self.assertBone(bones[3], "[G1{Y: 1}, G1{Y: 0}]") + + # bones on arcs + kinks = findDogboneKinks(MNVR("G1X1/G3X3I1/G1Y1/G1X0/G1Y0"), PI / 4) + bones = [vertical.generate(k) for k in kinks] + self.assertEqual(len(bones), 3) + self.assertBone(bones[0], "[G1{Y: 2}, G1{Y: 1}]") + self.assertBone(bones[1], "[G1{Y: 2}, G1{Y: 1}]") + self.assertBone(bones[2], "[G1{Y: -1}, G1{Y: 0}]") + + # bones on arcs + kinks = findDogboneKinks(MNVR("G1X1/G3X3I1/G1Y1/G1X0/G1Y0"), -PI / 4) + bones = [vertical.generate(k) for k in kinks] + self.assertEqual(len(bones), 1) + self.assertBone(bones[0], "[G1{Y: 1}, G1{Y: 0}]") + + def test60(self): + """Verify t-bones on edges""" + + on_short_1 = GEN(dogboneII.GeneratorTBoneOnShort, 1) + on_short_5 = GEN(dogboneII.GeneratorTBoneOnShort, 5) + + # horizontal short edge + bone = on_short_1.generate(KINK("G1X1/G1Y2")) + self.assertBone(bone, "[G1{Y: -1}, G1{Y: 0}]") + + bone = on_short_1.generate(KINK("G1X-1/G1Y2")) + self.assertBone(bone, "[G1{Y: -1}, G1{Y: 0}]") + + # vertical short edge + bone = on_short_1.generate(KINK("G1Y1/G1X2")) + self.assertBone(bone, "[G1{X: -1}, G1{X: 0}]") + + bone = on_short_1.generate(KINK("G1Y1/G1X-2")) + self.assertBone(bone, "[G1{X: 1}, G1{X: 0}]") + + # some other angle + bone = on_short_5.generate(KINK("G1X1Y1/G1Y-1")) + self.assertBone(bone, "[G1{X: -2.5, Y: 4.5}, G1{X: 1.0, Y: 1.0}]", 2) + + bone = on_short_5.generate(KINK("G1X-1Y-1/G1Y1")) + self.assertBone(bone, "[G1{X: 2.5, Y: -4.5}, G1{X: -1.0, Y: -1.0}]", 2) + + # some other angle + bone = on_short_5.generate(KINK("G1X2Y1/G1Y-3")) + self.assertBone(bone, "[G1{X: -0.24, Y: 5.5}, G1{X: 2.0, Y: 1.0}]", 2) + + bone = on_short_5.generate(KINK("G1X-2Y-1/G1Y3")) + self.assertBone(bone, "[G1{X: 0.24, Y: -5.5}, G1{X: -2.0, Y: -1.0}]", 2) + + # short edge - the 2nd + bone = on_short_1.generate(KINK("G1Y2/G1X1")) + self.assertBone(bone, "[G1{Y: 3}, G1{Y: 2}]") + bone = on_short_1.generate(KINK("G1Y2/G1X-1")) + self.assertBone(bone, "[G1{Y: 3}, G1{Y: 2}]") + + bone = on_short_5.generate(KINK("G1Y-3/G1X2Y-2")) + self.assertBone(bone, "[G1{X: 2.2, Y: -7.5}, G1{X: 0.0, Y: -3.0}]", 2) + + bone = on_short_5.generate(KINK("G1Y3/G1X-2Y2")) + self.assertBone(bone, "[G1{X: -2.2, Y: 7.5}, G1{X: 0.0, Y: 3.0}]", 2) + + # long edge + on_long_1 = GEN(dogboneII.GeneratorTBoneOnLong, 1) + on_long_5 = GEN(dogboneII.GeneratorTBoneOnLong, 5) + + bone = on_long_1.generate( + KINK("G1X2/G1Y1"), + ) + self.assertBone(bone, "[G1{Y: -1}, G1{Y: 0}]") + bone = on_long_1.generate(KINK("G1X-2/G1Y1")) + self.assertBone(bone, "[G1{Y: -1}, G1{Y: 0}]") + + bone = on_long_5.generate(KINK("G1Y-1/G1X2Y0")) + self.assertBone(bone, "[G1{X: 2.2, Y: -5.5}, G1{X: 0.0, Y: -1.0}]", 2) + + bone = on_long_5.generate(KINK("G1Y1/G1X-2Y0")) + self.assertBone(bone, "[G1{X: -2.2, Y: 5.5}, G1{X: 0.0, Y: 1.0}]", 2) + + def test70(self): + """Verify dogbone angles""" + self.assertRoughly(180 * KINK("G1X1/G1Y+1").normAngle() / PI, -45) + self.assertRoughly(180 * KINK("G1X1/G1Y-1").normAngle() / PI, 45) + + self.assertRoughly(180 * KINK("G1X1/G1X2Y1").normAngle() / PI, -67.5) + self.assertRoughly(180 * KINK("G1X1/G1X2Y-1").normAngle() / PI, 67.5) + + self.assertRoughly(180 * KINK("G1Y1/G1X+1").normAngle() / PI, 135) + self.assertRoughly(180 * KINK("G1Y1/G1X-1").normAngle() / PI, 45) + + self.assertRoughly(180 * KINK("G1X-1/G1Y+1").normAngle() / PI, -135) + self.assertRoughly(180 * KINK("G1X-1/G1Y-1").normAngle() / PI, 135) + + self.assertRoughly(180 * KINK("G1Y-1/G1X-1").normAngle() / PI, -45) + self.assertRoughly(180 * KINK("G1Y-1/G1X+1").normAngle() / PI, -135) + + def test71(self): + """Verify dogbones""" + + dogbone = GEN(dogboneII.GeneratorDogbone, 1) + + bone = dogbone.generate(KINK("G1X1/G1Y1")) + self.assertBone(bone, "[G1{X: 1.7, Y: -0.71}, G1{X: 1.0, Y: 0.0}]", 2) + + bone = dogbone.generate(KINK("G1X1/G1X3Y-1")) + self.assertBone(bone, "[G1{X: 1.2, Y: 0.97}, G1{X: 1.0, Y: 0.0}]", 2) + + bone = dogbone.generate(KINK("G1X1Y1/G1X2")) + self.assertBone(bone, "[G1{X: 0.62, Y: 1.9}, G1{X: 1.0, Y: 1.0}]", 2) diff --git a/src/Mod/Path/PathTests/TestPathGeom.py b/src/Mod/Path/PathTests/TestPathGeom.py index 0a2fa38ef6..d6ffd864e0 100644 --- a/src/Mod/Path/PathTests/TestPathGeom.py +++ b/src/Mod/Path/PathTests/TestPathGeom.py @@ -89,22 +89,28 @@ class TestPathGeom(PathTestBase): ) self.assertRoughly( - Path.Geom.diffAngle(+math.pi / 4, +0 * math.pi / 4, "CCW") / math.pi, 7 / 4.0 + Path.Geom.diffAngle(+math.pi / 4, +0 * math.pi / 4, "CCW") / math.pi, + 7 / 4.0, ) self.assertRoughly( - Path.Geom.diffAngle(+math.pi / 4, +3 * math.pi / 4, "CCW") / math.pi, 2 / 4.0 + Path.Geom.diffAngle(+math.pi / 4, +3 * math.pi / 4, "CCW") / math.pi, + 2 / 4.0, ) self.assertRoughly( - Path.Geom.diffAngle(+math.pi / 4, -1 * math.pi / 4, "CCW") / math.pi, 6 / 4.0 + Path.Geom.diffAngle(+math.pi / 4, -1 * math.pi / 4, "CCW") / math.pi, + 6 / 4.0, ) self.assertRoughly( - Path.Geom.diffAngle(-math.pi / 4, +0 * math.pi / 4, "CCW") / math.pi, 1 / 4.0 + Path.Geom.diffAngle(-math.pi / 4, +0 * math.pi / 4, "CCW") / math.pi, + 1 / 4.0, ) self.assertRoughly( - Path.Geom.diffAngle(-math.pi / 4, +3 * math.pi / 4, "CCW") / math.pi, 4 / 4.0 + Path.Geom.diffAngle(-math.pi / 4, +3 * math.pi / 4, "CCW") / math.pi, + 4 / 4.0, ) self.assertRoughly( - Path.Geom.diffAngle(-math.pi / 4, -1 * math.pi / 4, "CCW") / math.pi, 0 / 4.0 + Path.Geom.diffAngle(-math.pi / 4, -1 * math.pi / 4, "CCW") / math.pi, + 0 / 4.0, ) def test02(self): @@ -607,7 +613,9 @@ class TestPathGeom(PathTestBase): def cmds(center, radius, up=True): norm = Vector(0, 0, 1) if up else Vector(0, 0, -1) - return Path.Geom.cmdsForEdge(Part.Edge(Part.Circle(center, norm, radius)))[0] + return Path.Geom.cmdsForEdge(Part.Edge(Part.Circle(center, norm, radius)))[ + 0 + ] def cmd(g, end, off): return Path.Command( diff --git a/src/Mod/Path/PathTests/TestPathHelpers.py b/src/Mod/Path/PathTests/TestPathHelpers.py index c70b3dabfc..4ada315b97 100644 --- a/src/Mod/Path/PathTests/TestPathHelpers.py +++ b/src/Mod/Path/PathTests/TestPathHelpers.py @@ -41,6 +41,7 @@ def createTool(name="t1", diameter=1.75): } return PathToolBit.Factory.CreateFromAttrs(attrs, name) + class TestPathHelpers(PathTestBase): def setUp(self): self.doc = FreeCAD.newDocument("TestPathUtils") @@ -133,7 +134,9 @@ class TestPathHelpers(PathTestBase): self.assertTrue(len(results) == 2) e1 = results[0] self.assertTrue(isinstance(e1.Curve, Part.Circle)) - self.assertTrue(Path.Geom.pointsCoincide(edge.Curve.Location, e1.Curve.Location)) + self.assertTrue( + Path.Geom.pointsCoincide(edge.Curve.Location, e1.Curve.Location) + ) self.assertTrue(edge.Curve.Radius == e1.Curve.Radius) # filter a 180 degree arc diff --git a/src/Mod/Path/PathTests/TestPathLanguage.py b/src/Mod/Path/PathTests/TestPathLanguage.py new file mode 100644 index 0000000000..16fc76f136 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathLanguage.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 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 Path.Base.Language as PathLanguage +import PathTests.PathTestUtils as PathTestUtils +import math + +PI = math.pi + + +def MNVR(gcode, begin=None): + # 'turns out the replace() isn't really necessary + # leave it here anyway for clarity + return PathLanguage.Maneuver.FromGCode(gcode.replace("/", "\n"), begin) + + +def INSTR(gcode, begin=None): + return MNVR(gcode, begin).instr[0] + + +class TestPathLanguage(PathTestUtils.PathTestBase): + """Unit tests for the Language classes.""" + + def assertTangents(self, instr, t1): + """Assert that the two tangent angles are identical""" + t0 = instr.anglesOfTangents() + self.assertRoughly(t0[0], t1[0]) + self.assertRoughly(t0[1], t1[1]) + + def test00(self): + """Verify G0 instruction construction""" + self.assertEqual(str(MNVR("")), "") + self.assertEqual(len(MNVR("").instr), 0) + + self.assertEqual(str(MNVR("G0")), "G0{}") + self.assertEqual(str(MNVR("G0X3")), "G0{'X': 3.0}") + self.assertEqual(str(MNVR("G0X3Y7")), "G0{'X': 3.0, 'Y': 7.0}") + self.assertEqual( + str(MNVR("G0X3Y7/G0Z0")), "G0{'X': 3.0, 'Y': 7.0}\nG0{'Z': 0.0}" + ) + self.assertEqual(len(MNVR("G0X3Y7").instr), 1) + self.assertEqual(len(MNVR("G0X3Y7/G0Z0").instr), 2) + self.assertEqual(type(MNVR("G0X3Y7").instr[0]), PathLanguage.MoveStraight) + + def test10(self): + """Verify G1 instruction construction""" + self.assertEqual(str(MNVR("G1")), "G1{}") + self.assertEqual(str(MNVR("G1X3")), "G1{'X': 3.0}") + self.assertEqual(str(MNVR("G1X3Y7")), "G1{'X': 3.0, 'Y': 7.0}") + self.assertEqual( + str(MNVR("G1X3Y7/G1Z0")), "G1{'X': 3.0, 'Y': 7.0}\nG1{'Z': 0.0}" + ) + self.assertEqual(len(MNVR("G1X3Y7").instr), 1) + self.assertEqual(len(MNVR("G1X3Y7/G1Z0").instr), 2) + self.assertEqual(type(MNVR("G1X3Y7").instr[0]), PathLanguage.MoveStraight) + + def test20(self): + """Verify G2 instruction construction""" + self.assertEqual(str(MNVR("G2X2Y2I1")), "G2{'I': 1.0, 'X': 2.0, 'Y': 2.0}") + self.assertEqual(len(MNVR("G2X2Y2I1").instr), 1) + self.assertEqual(type(MNVR("G2X2Y2I1").instr[0]), PathLanguage.MoveArcCW) + + def test30(self): + """Verify G3 instruction construction""" + self.assertEqual(str(MNVR("G3X2Y2I1")), "G3{'I': 1.0, 'X': 2.0, 'Y': 2.0}") + self.assertEqual(len(MNVR("G3X2Y2I1").instr), 1) + self.assertEqual(type(MNVR("G3X2Y2I1").instr[0]), PathLanguage.MoveArcCCW) + + def test40(self): + """Verify pathLength correctness""" + self.assertRoughly(MNVR("G1X3").instr[0].pathLength(), 3) + self.assertRoughly(MNVR("G1X-7").instr[0].pathLength(), 7) + self.assertRoughly(MNVR("G1X3").instr[0].pathLength(), 3) + + self.assertRoughly(MNVR("G1X3Y4").instr[0].pathLength(), 5) + self.assertRoughly(MNVR("G1X3Y-4").instr[0].pathLength(), 5) + self.assertRoughly(MNVR("G1X-3Y-4").instr[0].pathLength(), 5) + self.assertRoughly(MNVR("G1X-3Y4").instr[0].pathLength(), 5) + + self.assertRoughly(MNVR("G2X2I1").instr[0].pathLength(), PI) + self.assertRoughly(MNVR("G2X1Y1I1").instr[0].pathLength(), PI / 2) + + self.assertRoughly(MNVR("G3X2I1").instr[0].pathLength(), PI) + self.assertRoughly(MNVR("G3X1Y1I1").instr[0].pathLength(), 3 * PI / 2) + + def test50(self): + """Verify tangents of moves.""" + + self.assertTangents(INSTR("G1 X0 Y0"), (0, 0)) # by declaration + self.assertTangents(INSTR("G1 X1 Y0"), (0, 0)) + self.assertTangents(INSTR("G1 X-1 Y0"), (PI, PI)) + self.assertTangents(INSTR("G1 X0 Y1"), (PI / 2, PI / 2)) + self.assertTangents(INSTR("G1 X0 Y-1"), (-PI / 2, -PI / 2)) + self.assertTangents(INSTR("G1 X1 Y1"), (PI / 4, PI / 4)) + self.assertTangents(INSTR("G1 X-1 Y1"), (3 * PI / 4, 3 * PI / 4)) + self.assertTangents(INSTR("G1 X-1 Y -1"), (-3 * PI / 4, -3 * PI / 4)) + self.assertTangents(INSTR("G1 X1 Y-1"), (-PI / 4, -PI / 4)) + + self.assertTangents(INSTR("G2 X2 Y0 I1 J0"), (PI / 2, -PI / 2)) + self.assertTangents(INSTR("G2 X2 Y2 I1 J1"), (3 * PI / 4, -PI / 4)) + self.assertTangents(INSTR("G2 X0 Y-2 I0 J-1"), (0, -PI)) + + self.assertTangents(INSTR("G3 X2 Y0 I1 J0"), (-PI / 2, PI / 2)) + self.assertTangents(INSTR("G3 X2 Y2 I1 J1"), (-PI / 4, 3 * PI / 4)) + self.assertTangents(INSTR("G3 X0 Y-2 I0 J-1"), (PI, 0)) diff --git a/src/Mod/Path/PathTests/TestPathOpUtil.py b/src/Mod/Path/PathTests/TestPathOpUtil.py index c066352745..74fc34ad9a 100644 --- a/src/Mod/Path/PathTests/TestPathOpUtil.py +++ b/src/Mod/Path/PathTests/TestPathOpUtil.py @@ -363,9 +363,7 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertRoughly(33, edge.Curve.Radius) # the other way around everything's the same except the axis is negative - wire = PathOpUtil.offsetWire( - getWire(obj.Tool), getPositiveShape(obj), 3, False - ) + wire = PathOpUtil.offsetWire(getWire(obj.Tool), getPositiveShape(obj), 3, False) self.assertEqual(1, len(wire.Edges)) edge = wire.Edges[0] self.assertCoincide(Vector(), edge.Curve.Center) @@ -394,9 +392,7 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertTrue(PathOpUtil.isWireClockwise(wire)) # change offset orientation - wire = PathOpUtil.offsetWire( - getWire(obj.Tool), getPositiveShape(obj), 3, False - ) + wire = PathOpUtil.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( @@ -432,9 +428,7 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) # change offset orientation - wire = PathOpUtil.offsetWire( - getWire(obj.Tool), getPositiveShape(obj), 3, False - ) + wire = PathOpUtil.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( @@ -467,9 +461,7 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertCoincide(Vector(0, 0, -1), e.Curve.Axis) # change offset orientation - wire = PathOpUtil.offsetWire( - getWire(obj.Tool), getPositiveShape(obj), 3, False - ) + wire = PathOpUtil.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( @@ -494,9 +486,7 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertRoughly(27, edge.Curve.Radius) # the other way around everything's the same except the axis is negative - wire = PathOpUtil.offsetWire( - getWire(obj.Tool), getNegativeShape(obj), 3, False - ) + wire = PathOpUtil.offsetWire(getWire(obj.Tool), getNegativeShape(obj), 3, False) self.assertEqual(1, len(wire.Edges)) edge = wire.Edges[0] self.assertCoincide(Vector(), edge.Curve.Center) @@ -518,9 +508,7 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertFalse(PathOpUtil.isWireClockwise(wire)) # change offset orientation - wire = PathOpUtil.offsetWire( - getWire(obj.Tool), getNegativeShape(obj), 3, False - ) + wire = PathOpUtil.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: @@ -543,9 +531,7 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertFalse(PathOpUtil.isWireClockwise(wire)) # change offset orientation - wire = PathOpUtil.offsetWire( - getWire(obj.Tool), getNegativeShape(obj), 3, False - ) + wire = PathOpUtil.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: @@ -572,9 +558,7 @@ class TestPathOpUtil(PathTestUtils.PathTestBase): self.assertCoincide(Vector(0, 0, +1), e.Curve.Axis) # change offset orientation - wire = PathOpUtil.offsetWire( - getWire(obj.Tool), getNegativeShape(obj), 3, False - ) + wire = PathOpUtil.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( diff --git a/src/Mod/Path/PathTests/TestPathPreferences.py b/src/Mod/Path/PathTests/TestPathPreferences.py index 0febdf5b35..3c1bf9bb7c 100644 --- a/src/Mod/Path/PathTests/TestPathPreferences.py +++ b/src/Mod/Path/PathTests/TestPathPreferences.py @@ -39,7 +39,9 @@ class TestPathPreferences(PathTestUtils.PathTestBase): def test02(self): """Path/Post/scripts is part of the posts search path.""" paths = Path.Preferences.searchPathsPost() - self.assertEqual(len([p for p in paths if p.endswith("/Path/Post/scripts/")]), 1) + self.assertEqual( + len([p for p in paths if p.endswith("/Path/Post/scripts/")]), 1 + ) def test03(self): """Available post processors include linuxcnc, grbl and opensbp.""" @@ -51,7 +53,9 @@ class TestPathPreferences(PathTestUtils.PathTestBase): def test10(self): """Default paths for tools are resolved correctly""" - self.assertTrue(Path.Preferences.pathDefaultToolsPath().endswith("/Path/Tools/")) + self.assertTrue( + Path.Preferences.pathDefaultToolsPath().endswith("/Path/Tools/") + ) self.assertTrue( Path.Preferences.pathDefaultToolsPath("Bit").endswith("/Path/Tools/Bit") ) diff --git a/src/Mod/Path/PathTests/TestPathSetupSheet.py b/src/Mod/Path/PathTests/TestPathSetupSheet.py index 9bc25b0589..40ceda865b 100644 --- a/src/Mod/Path/PathTests/TestPathSetupSheet.py +++ b/src/Mod/Path/PathTests/TestPathSetupSheet.py @@ -26,7 +26,7 @@ import Path.Base.SetupSheet as PathSetupSheet import json import sys -#Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) +# Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) from PathTests.PathTestUtils import PathTestBase @@ -34,14 +34,15 @@ from PathTests.PathTestUtils import PathTestBase def refstring(string): return string.replace(" u'", " '") -class SomeOp (object): + +class SomeOp(object): def __init__(self, obj): Path.Log.track(obj, type(obj)) - obj.addProperty('App::PropertyPercent', 'StepOver', 'Base', 'Some help you are') + obj.addProperty("App::PropertyPercent", "StepOver", "Base", "Some help you are") @classmethod def SetupProperties(cls): - return ['StepOver'] + return ["StepOver"] @classmethod def Create(cls, name, obj=None, parentJob=None): @@ -51,6 +52,7 @@ class SomeOp (object): obj.Proxy = SomeOp(obj) return obj + class TestPathSetupSheet(PathTestBase): def setUp(self): self.doc = FreeCAD.newDocument("TestPathSetupSheet") @@ -321,28 +323,32 @@ class TestPathSetupSheet(PathTestBase): def test20(self): """Verify SetupSheet template op attributes roundtrip.""" - opname = 'whoop' + opname = "whoop" o1 = PathSetupSheet.Create() PathSetupSheet.RegisterOperation(opname, SomeOp.Create, SomeOp.SetupProperties) - ptt = PathSetupSheet._RegisteredOps[opname].prototype('whoopsy') - pptt = ptt.getProperty('StepOver') - pptt.setupProperty(o1, PathSetupSheet.OpPropertyName(opname, pptt.name), PathSetupSheet.OpPropertyGroup(opname), 75) + ptt = PathSetupSheet._RegisteredOps[opname].prototype("whoopsy") + pptt = ptt.getProperty("StepOver") + pptt.setupProperty( + o1, + PathSetupSheet.OpPropertyName(opname, pptt.name), + PathSetupSheet.OpPropertyGroup(opname), + 75, + ) # save setup sheet in json "file" attrs = o1.Proxy.templateAttributes(False, False, False, False, [opname]) encdd = o1.Proxy.encodeTemplateAttributes(attrs) - j1 = json.dumps({'SetupSheet' : encdd}, sort_keys=True, indent=2) + j1 = json.dumps({"SetupSheet": encdd}, sort_keys=True, indent=2) # restore setup sheet from json "file" j2 = json.loads(j1) o2 = PathSetupSheet.Create() - o2.Proxy.setFromTemplate(j2['SetupSheet']) + o2.Proxy.setFromTemplate(j2["SetupSheet"]) op = SomeOp.Create(opname) self.assertEqual(op.StepOver, 0) o2.Proxy.setOperationProperties(op, opname) self.assertEqual(op.StepOver, 75) - diff --git a/src/Mod/Path/PathTests/TestPathThreadMilling.py b/src/Mod/Path/PathTests/TestPathThreadMilling.py index a1a29dde92..4a9913b000 100644 --- a/src/Mod/Path/PathTests/TestPathThreadMilling.py +++ b/src/Mod/Path/PathTests/TestPathThreadMilling.py @@ -29,13 +29,13 @@ from PathTests.PathTestUtils import PathTestBase class TestObject(object): - def __init__(self, orientation, direction, zTop, zBottom): self.ThreadOrientation = orientation self.Direction = direction self.StartDepth = FreeCAD.Units.Quantity(zTop, FreeCAD.Units.Length) self.FinalDepth = FreeCAD.Units.Quantity(zBottom, FreeCAD.Units.Length) + def radii(internal, major, minor, toolDia, toolCrest): """test radii function for simple testing""" if internal: @@ -56,13 +56,17 @@ class TestPathThreadMilling(PathTestBase): self.assertRoughly(have[i], want[i]) def assertSetupInternal(self, obj, c, begin, end): - cmd, zBegin, zEnd = PathThreadMilling.threadSetupInternal(obj, obj.StartDepth.Value, obj.FinalDepth.Value) + cmd, zBegin, zEnd = PathThreadMilling.threadSetupInternal( + obj, obj.StartDepth.Value, obj.FinalDepth.Value + ) self.assertEqual(cmd, c) self.assertEqual(zBegin, begin) self.assertEqual(zEnd, end) def assertSetupExternal(self, obj, c, begin, end): - cmd, zBegin, zEnd = PathThreadMilling.threadSetupExternal(obj, obj.StartDepth.Value, obj.FinalDepth.Value) + cmd, zBegin, zEnd = PathThreadMilling.threadSetupExternal( + obj, obj.StartDepth.Value, obj.FinalDepth.Value + ) self.assertEqual(cmd, c) self.assertEqual(zBegin, begin) self.assertEqual(zEnd, end) @@ -120,30 +124,45 @@ class TestPathThreadMilling(PathTestBase): hand = PathThreadMilling.RightHand - self.assertSetupInternal(TestObject(hand, PathThreadMilling.DirectionConventional, 1, 0), "G2", 1, 0) - self.assertSetupInternal(TestObject(hand, PathThreadMilling.DirectionClimb, 1, 0), "G3", 0, 1) + self.assertSetupInternal( + TestObject(hand, PathThreadMilling.DirectionConventional, 1, 0), "G2", 1, 0 + ) + self.assertSetupInternal( + TestObject(hand, PathThreadMilling.DirectionClimb, 1, 0), "G3", 0, 1 + ) def test41(self): """Verify internal left hand thread setup.""" hand = PathThreadMilling.LeftHand - self.assertSetupInternal(TestObject(hand, PathThreadMilling.DirectionConventional, 1, 0), "G2", 0, 1) - self.assertSetupInternal(TestObject(hand, PathThreadMilling.DirectionClimb, 1, 0), "G3", 1, 0) + self.assertSetupInternal( + TestObject(hand, PathThreadMilling.DirectionConventional, 1, 0), "G2", 0, 1 + ) + self.assertSetupInternal( + TestObject(hand, PathThreadMilling.DirectionClimb, 1, 0), "G3", 1, 0 + ) def test50(self): """Verify exteranl right hand thread setup.""" hand = PathThreadMilling.RightHand - self.assertSetupExternal(TestObject(hand, PathThreadMilling.DirectionClimb, 1, 0), "G2", 1, 0) - self.assertSetupExternal(TestObject(hand, PathThreadMilling.DirectionConventional, 1, 0), "G3", 0, 1) + self.assertSetupExternal( + TestObject(hand, PathThreadMilling.DirectionClimb, 1, 0), "G2", 1, 0 + ) + self.assertSetupExternal( + TestObject(hand, PathThreadMilling.DirectionConventional, 1, 0), "G3", 0, 1 + ) def test51(self): """Verify exteranl left hand thread setup.""" hand = PathThreadMilling.LeftHand - self.assertSetupExternal(TestObject(hand, PathThreadMilling.DirectionClimb, 1, 0), "G2", 0, 1) - self.assertSetupExternal(TestObject(hand, PathThreadMilling.DirectionConventional, 1, 0), "G3", 1, 0) - + self.assertSetupExternal( + TestObject(hand, PathThreadMilling.DirectionClimb, 1, 0), "G2", 0, 1 + ) + self.assertSetupExternal( + TestObject(hand, PathThreadMilling.DirectionConventional, 1, 0), "G3", 1, 0 + ) diff --git a/src/Mod/Path/PathTests/TestRefactoredTestPost.py b/src/Mod/Path/PathTests/TestRefactoredTestPost.py index 70614bc28d..dec1e23014 100644 --- a/src/Mod/Path/PathTests/TestRefactoredTestPost.py +++ b/src/Mod/Path/PathTests/TestRefactoredTestPost.py @@ -253,7 +253,9 @@ G21 """ self.docobj.Path = Path.Path([]) postables = [self.docobj] - gcode: str = postprocessor.export(postables, "gcode.tmp", "--output_all_arguments") + gcode: str = postprocessor.export( + postables, "gcode.tmp", "--output_all_arguments" + ) # The argparse help routine turns out to be sensitive to the # number of columns in the terminal window that the tests # are run from. This affects the indenting in the output. @@ -315,7 +317,9 @@ G21 def test00120(self): """Test axis-precision.""" - self.compare_third_line("G0 X10 Y20 Z30", "G0 X10.00 Y20.00 Z30.00", "--axis-precision=2") + self.compare_third_line( + "G0 X10 Y20 Z30", "G0 X10.00 Y20.00 Z30.00", "--axis-precision=2" + ) def test00130(self): """Test comments.""" @@ -605,26 +609,42 @@ G21 """Test G10 command Generation.""" self.compare_third_line("G10 L1 P2 Z1.23456", "G10 L1 Z1.235 P2", "") self.compare_third_line( - "G10 L1 P2 R1.23456 I2.34567 J3.456789 Q3", "G10 L1 I2.346 J3.457 R1.235 P2 Q3", "" + "G10 L1 P2 R1.23456 I2.34567 J3.456789 Q3", + "G10 L1 I2.346 J3.457 R1.235 P2 Q3", + "", ) self.compare_third_line( - "G10 L2 P3 X1.23456 Y2.34567 Z3.456789", "G10 L2 X1.235 Y2.346 Z3.457 P3", "" - ) - self.compare_third_line("G10 L2 P0 X0 Y0 Z0", "G10 L2 X0.000 Y0.000 Z0.000 P0", "") - self.compare_third_line( - "G10 L10 P1 X1.23456 Y2.34567 Z3.456789", "G10 L10 X1.235 Y2.346 Z3.457 P1", "" + "G10 L2 P3 X1.23456 Y2.34567 Z3.456789", + "G10 L2 X1.235 Y2.346 Z3.457 P3", + "", ) self.compare_third_line( - "G10 L10 P2 R1.23456 I2.34567 J3.456789 Q3", "G10 L10 I2.346 J3.457 R1.235 P2 Q3", "" + "G10 L2 P0 X0 Y0 Z0", "G10 L2 X0.000 Y0.000 Z0.000 P0", "" ) self.compare_third_line( - "G10 L11 P1 X1.23456 Y2.34567 Z3.456789", "G10 L11 X1.235 Y2.346 Z3.457 P1", "" + "G10 L10 P1 X1.23456 Y2.34567 Z3.456789", + "G10 L10 X1.235 Y2.346 Z3.457 P1", + "", ) self.compare_third_line( - "G10 L11 P2 R1.23456 I2.34567 J3.456789 Q3", "G10 L11 I2.346 J3.457 R1.235 P2 Q3", "" + "G10 L10 P2 R1.23456 I2.34567 J3.456789 Q3", + "G10 L10 I2.346 J3.457 R1.235 P2 Q3", + "", ) self.compare_third_line( - "G10 L20 P9 X1.23456 Y2.34567 Z3.456789", "G10 L20 X1.235 Y2.346 Z3.457 P9", "" + "G10 L11 P1 X1.23456 Y2.34567 Z3.456789", + "G10 L11 X1.235 Y2.346 Z3.457 P1", + "", + ) + self.compare_third_line( + "G10 L11 P2 R1.23456 I2.34567 J3.456789 Q3", + "G10 L11 I2.346 J3.457 R1.235 P2 Q3", + "", + ) + self.compare_third_line( + "G10 L20 P9 X1.23456 Y2.34567 Z3.456789", + "G10 L20 X1.235 Y2.346 Z3.457 P9", + "", ) def test01170(self): @@ -779,7 +799,9 @@ G21 Path.Command( "G52 X1.234567 Y2.345678 Z3.456789 A4.567891 B5.678912 C6.789123 U7.891234 V8.912345 W9.123456" ), - Path.Command("G52 X0 Y0.0 Z0.00 A0.000 B0.0000 C0.00000 U0.000000 V0 W0"), + Path.Command( + "G52 X0 Y0.0 Z0.00 A0.000 B0.0000 C0.00000 U0.000000 V0 W0" + ), ], """G90 G21 @@ -793,7 +815,9 @@ G52 X0.000 Y0.000 Z0.000 A0.000 B0.000 C0.000 U0.000 V0.000 W0.000 Path.Command( "G52 X1.234567 Y2.345678 Z3.456789 A4.567891 B5.678912 C6.789123 U7.891234 V8.912345 W9.123456" ), - Path.Command("G52 X0 Y0.0 Z0.00 A0.000 B0.0000 C0.00000 U0.000000 V0 W0"), + Path.Command( + "G52 X0 Y0.0 Z0.00 A0.000 B0.0000 C0.00000 U0.000000 V0 W0" + ), ], """G90 G20 @@ -916,7 +940,9 @@ G52 X0.0000 Y0.0000 Z0.0000 A0.0000 B0.0000 C0.0000 U0.0000 V0.0000 W0.0000 self.compare_third_line("G64", "G64", "") self.compare_third_line("G64 P3.456789", "G64 P3.457", "") self.compare_third_line("G64 P3.456789 Q4.567891", "G64 P3.457 Q4.568", "") - self.compare_third_line("G64 P3.456789 Q4.567891", "G64 P0.1361 Q0.1798", "--inches") + self.compare_third_line( + "G64 P3.456789 Q4.567891", "G64 P0.1361 Q0.1798", "--inches" + ) def test01730(self): """Test G73 command Generation.""" diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 28e76ad1a0..00328d2e59 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -27,10 +27,13 @@ from PathTests.TestPathCore import TestPathCore from PathTests.TestPathDeburr import TestPathDeburr from PathTests.TestPathDepthParams import depthTestCases from PathTests.TestPathDressupDogbone import TestDressupDogbone +from PathTests.TestPathDressupDogboneII import TestDressupDogboneII from PathTests.TestPathDressupHoldingTags import TestHoldingTags from PathTests.TestPathDrillable import TestPathDrillable from PathTests.TestPathDrillGenerator import TestPathDrillGenerator +from PathTests.TestPathGeneratorDogboneII import TestGeneratorDogboneII from PathTests.TestPathGeom import TestPathGeom +from PathTests.TestPathLanguage import TestPathLanguage # from PathTests.TestPathHelix import TestPathHelix from PathTests.TestPathHelpers import TestPathHelpers @@ -72,7 +75,10 @@ False if depthTestCases.__name__ else True False if TestApp.__name__ else True False if TestBuildPostList.__name__ else True False if TestDressupDogbone.__name__ else True +False if TestDressupDogboneII.__name__ else True +False if TestGeneratorDogboneII.__name__ else True False if TestHoldingTags.__name__ else True +False if TestPathLanguage.__name__ else True False if TestOutputNameSubstitution.__name__ else True False if TestPathAdaptive.__name__ else True False if TestPathCore.__name__ else True