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:
mlampert
2022-11-02 13:25:09 -07:00
committed by GitHub
parent fac648fff5
commit ebc1190d8b
64 changed files with 2708 additions and 252 deletions

View File

@@ -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

View 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)

View File

@@ -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.

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View 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)

View File

@@ -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

View 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

View File

@@ -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."

View File

@@ -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()

View File

@@ -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")

View 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")

View File

@@ -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",

View File

@@ -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")

View File

@@ -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")

View File

@@ -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()

View File

@@ -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"

View File

@@ -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.

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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:

View File

@@ -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))

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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]")

View File

@@ -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()

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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
# __________________________________________________

View File

@@ -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(

View File

@@ -140,4 +140,3 @@ def parse(pathobj):
objlist.append(obj)
return objlist

View File

@@ -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.
#

View File

@@ -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.
#

View File

@@ -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.
#

View File

@@ -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.
#

View File

@@ -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.
#

View File

@@ -169,7 +169,6 @@ def searchPathsTool(sub):
return paths
def toolsStoreAbsolutePaths():
return preferences().GetBool(UseAbsoluteToolPaths, False)

View File

@@ -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")

View File

@@ -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

View File

@@ -933,7 +933,7 @@ class ToolBitLibrary(object):
if not bit:
continue
Path.Log.track(bit)
toolitem = tooltemplate.copy()

View File

@@ -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"
)

View File

@@ -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):

View File

@@ -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:

View File

@@ -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)

View File

@@ -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."""

View 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",
)

View File

@@ -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)

View 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)

View File

@@ -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(

View File

@@ -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

View 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))

View File

@@ -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(

View File

@@ -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")
)

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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."""

View File

@@ -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