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
This commit is contained in:
@@ -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
|
||||
|
||||
220
src/Mod/Path/Path/Base/Generator/dogboneII.py
Normal file
220
src/Mod/Path/Path/Base/Generator/dogboneII.py
Normal file
@@ -0,0 +1,220 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * 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)
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
269
src/Mod/Path/Path/Base/Language.py
Normal file
269
src/Mod/Path/Path/Base/Language.py
Normal file
@@ -0,0 +1,269 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * 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)
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
368
src/Mod/Path/Path/Dressup/DogboneII.py
Normal file
368
src/Mod/Path/Path/Dressup/DogboneII.py
Normal file
@@ -0,0 +1,368 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * 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
|
||||
@@ -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."
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
378
src/Mod/Path/Path/Dressup/Gui/DogboneII.py
Normal file
378
src/Mod/Path/Path/Dressup/Gui/DogboneII.py
Normal file
@@ -0,0 +1,378 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * 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")
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
# __________________________________________________
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -140,4 +140,3 @@ def parse(pathobj):
|
||||
objlist.append(obj)
|
||||
|
||||
return objlist
|
||||
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
@@ -169,7 +169,6 @@ def searchPathsTool(sub):
|
||||
return paths
|
||||
|
||||
|
||||
|
||||
def toolsStoreAbsolutePaths():
|
||||
return preferences().GetBool(UseAbsoluteToolPaths, False)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -933,7 +933,7 @@ class ToolBitLibrary(object):
|
||||
|
||||
if not bit:
|
||||
continue
|
||||
|
||||
|
||||
Path.Log.track(bit)
|
||||
|
||||
toolitem = tooltemplate.copy()
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
640
src/Mod/Path/PathTests/TestPathDressupDogboneII.py
Normal file
640
src/Mod/Path/PathTests/TestPathDressupDogboneII.py
Normal file
@@ -0,0 +1,640 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * 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",
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
354
src/Mod/Path/PathTests/TestPathGeneratorDogboneII.py
Normal file
354
src/Mod/Path/PathTests/TestPathGeneratorDogboneII.py
Normal file
@@ -0,0 +1,354 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * 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)
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
124
src/Mod/Path/PathTests/TestPathLanguage.py
Normal file
124
src/Mod/Path/PathTests/TestPathLanguage.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * 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))
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user