Merge pull request #5229 from sliptonic/feature/drill-refactor

[PATH]  Refactoring drilling op to use generators
This commit is contained in:
sliptonic
2022-01-19 11:25:41 -06:00
committed by GitHub
18 changed files with 1281 additions and 594 deletions

View File

@@ -10,6 +10,8 @@ set(Path_Scripts
Init.py
PathCommands.py
TestPathApp.py
PathMachineState.py
PathFeedRate.py
)
if(BUILD_GUI)
@@ -25,6 +27,7 @@ INSTALL(
SET(PathScripts_SRCS
PathCommands.py
PathScripts/drillableLib.py
PathScripts/PathAdaptive.py
PathScripts/PathAdaptiveGui.py
PathScripts/PathAreaOp.py
@@ -144,6 +147,7 @@ SET(PathScripts_SRCS
SET(Generator_SRCS
Generators/drill_generator.py
Generators/rotation_generator.py
)
SET(PathScripts_post_SRCS
@@ -208,6 +212,7 @@ SET(Tools_Shape_SRCS
SET(PathTests_SRCS
PathTests/__init__.py
PathTests/boxtest.fcstd
PathTests/Drilling_1.FCStd
PathTests/PathTestUtils.py
PathTests/test_adaptive.fcstd
PathTests/test_centroid_00.ngc
@@ -221,8 +226,11 @@ SET(PathTests_SRCS
PathTests/TestPathDressupDogbone.py
PathTests/TestPathDressupHoldingTags.py
PathTests/TestPathDrillGenerator.py
PathTests/TestPathDrillable.py
PathTests/TestPathRotationGenerator.py
PathTests/TestPathGeom.py
PathTests/TestPathHelix.py
PathTests/TestPathHelpers.py
PathTests/TestPathLog.py
PathTests/TestPathOpTools.py
PathTests/TestPathPost.py

View File

@@ -39,8 +39,6 @@ else:
def generate(edge, dwelltime=0.0, peckdepth=0.0, repeat=1):
startPoint = edge.Vertexes[0].Point
endPoint = edge.Vertexes[1].Point

View File

@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2021 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 PathScripts.PathLog as PathLog
import PathMachineState
import PathScripts.PathGeom as PathGeom
import Part
from PathScripts.PathGeom import CmdMoveRapid, CmdMoveAll
__title__ = "Feed Rate Helper Utility"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Helper for adding Feed Rate to Path Commands"
"""
TODO: This needs to be able to handle feedrates for axes other than X,Y,Z
"""
if True:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
def setFeedRate(commandlist, ToolController):
"""Set the appropriate feed rate for a list of Path commands using the information from a Tool Controler
Every motion command in the list will have a feed rate parameter added or overwritten based
on the information stored in the tool controller. If a motion is a plunge (vertical) motion, the
VertFeed value will be used, otherwise the HorizFeed value will be used instead."""
def _isVertical(currentposition, command):
x = (
command.Parameters["X"]
if "X" in command.Parameters.keys()
else currentposition.x
)
y = (
command.Parameters["Y"]
if "Y" in command.Parameters.keys()
else currentposition.y
)
z = (
command.Parameters["Z"]
if "Z" in command.Parameters.keys()
else currentposition.z
)
endpoint = FreeCAD.Vector(x, y, z)
if currentposition == endpoint:
return True
return PathGeom.isVertical(Part.makeLine(currentposition, endpoint))
machine = PathMachineState.MachineState()
for command in commandlist:
if command.Name not in CmdMoveAll:
continue
if _isVertical(machine.getPosition(), command):
rate = (
ToolController.VertRapid.Value
if command.Name in CmdMoveRapid
else ToolController.VertFeed.Value
)
else:
rate = (
ToolController.HorizRapid.Value
if command.Name in CmdMoveRapid
else ToolController.HorizFeed.Value
)
params = command.Parameters
params["F"] = rate
command.Parameters = params
machine.addCommand(command)
return commandlist

View File

@@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2021 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 *
# * *
# ***************************************************************************
__title__ = "Path Machine State"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Dataclass to implement a machinestate tracker"
__contributors__ = ""
import PathScripts.PathLog as PathLog
import FreeCAD
from dataclasses import dataclass, field
from PathScripts.PathGeom import CmdMoveRapid, CmdMoveAll, CmdMoveDrill
if True:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
@dataclass
class MachineState:
WCSLIST = [
"G53",
"G54",
"G55",
"G56",
"G57",
"G58",
"G59",
"G59.1",
"G59.2",
"G59.3",
"G59.4",
"G59.5",
"G59.6",
"G59.7",
"G59.8",
"G59.9",
]
X: float = field(default=0)
Y: float = field(default=0)
Z: float = field(default=0)
A: float = field(default=0)
B: float = field(default=0)
C: float = field(default=0)
F: float = field(default=None)
Coolant: bool = field(default=False)
WCS: str = field(default="G54")
Spindle: str = field(default="off")
S: int = field(default=0)
T: int = field(default=None)
def addCommand(self, command):
"""Processes a command and updates the internal state of the machine. Returns true if the command has alterned the machine state"""
oldstate = self.getState()
if command.Name == "M6":
self.T = int(command.Parameters["T"])
return not oldstate == self.getState()
if command.Name in ["M3", "M4"]:
self.S = command.Parameters["S"]
self.Spindle = "CW" if command.Name == "M3" else "CCW"
return not oldstate == self.getState()
if command.Name in ["M2", "M5"]:
self.S = 0
self.Spindle = "off"
return not oldstate == self.getState()
if command.Name in self.WCSLIST:
self.WCS = command.Name
return not oldstate == self.getState()
if command.Name in CmdMoveDrill:
oldZ = self.Z
for p in command.Parameters:
self.__setattr__(p, command.Parameters[p])
self.__setattr__("Z", oldZ)
return not oldstate == self.getState()
for p in command.Parameters:
self.__setattr__(p, command.Parameters[p])
return not oldstate == self.getState()
def getState(self):
"""
Returns a dictionary of the current machine state
"""
state = {}
state['X'] = self.X
state['Y'] = self.Y
state['Z'] = self.Z
state['A'] = self.A
state['B'] = self.B
state['C'] = self.C
state['F'] = self.F
state['Coolant'] = self.Coolant
state['WCS'] = self.WCS
state['Spindle'] = self.Spindle
state['S'] = self.S
state['T'] = self.T
return state
def getPosition(self):
"""
Returns a vector of the current machine position
"""
# This is technical debt. The actual position may include a rotation
# component as well. We should probably be returning a placement
return FreeCAD.Vector(self.X, self.Y, self.Z)

View File

@@ -327,21 +327,21 @@ class ObjectOp(PathOp.ObjectOp):
shapes.append(shp)
if len(shapes) > 1:
jobs = list()
locations = []
for s in shapes:
if s[2] == 'OpenEdge':
shp = Part.makeCompound(s[0])
else:
shp = s[0]
jobs.append({
locations.append({
'x': shp.BoundBox.XMax,
'y': shp.BoundBox.YMax,
'shape': s
})
jobs = PathUtils.sort_jobs(jobs, ['x', 'y'])
locations = PathUtils.sort_locations(locations, ['x', 'y'])
shapes = [j['shape'] for j in jobs]
shapes = [j['shape'] for j in locations]
sims = []
for shape, isHole, sub in shapes:

View File

@@ -23,7 +23,9 @@
import FreeCAD
import PathScripts.PathLog as PathLog
import PathScripts.PathOp as PathOp
import PathScripts.PathUtils as PathUtils
# import PathScripts.PathUtils as PathUtils
import PathScripts.drillableLib as drillableLib
from PySide import QtCore
@@ -46,9 +48,11 @@ def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# PathLog.trackModule(PathLog.thisModule())
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
class ObjectOp(PathOp.ObjectOp):
"""Base class for proxy objects of all operations on circular holes."""
@@ -172,7 +176,6 @@ class ObjectOp(PathOp.ObjectOp):
return False
holes = []
for base, subs in obj.Base:
for sub in subs:
PathLog.debug("processing {} in {}".format(sub, base.Name))
@@ -204,85 +207,19 @@ class ObjectOp(PathOp.ObjectOp):
def findAllHoles(self, obj):
"""findAllHoles(obj) ... find all holes of all base models and assign as features."""
PathLog.track()
if not self.getJob(obj):
job = self.getJob(obj)
if not job:
return
matchvector = None if job.JobType == "Multiaxis" else FreeCAD.Vector(0, 0, 1)
tooldiameter = obj.ToolController.Tool.Diameter
features = []
for base in self.model:
features.extend(self.findHoles(obj, base))
features.extend(
drillableLib.getDrillableTargets(
base, ToolDiameter=tooldiameter, vector=matchvector
)
)
obj.Base = features
obj.Disabled = []
def findHoles(self, obj, baseobject):
"""findHoles(obj, baseobject) ... inspect baseobject and identify all features that resemble a straight cricular hole."""
shape = baseobject.Shape
PathLog.track("obj: {} shape: {}".format(obj, shape))
holelist = []
features = []
# tooldiameter = float(obj.ToolController.Proxy.getTool(obj.ToolController).Diameter)
tooldiameter = None
PathLog.debug(
"search for holes larger than tooldiameter: {}: ".format(tooldiameter)
)
if DraftGeomUtils.isPlanar(shape):
PathLog.debug("shape is planar")
for i in range(len(shape.Edges)):
candidateEdgeName = "Edge" + str(i + 1)
e = shape.getElement(candidateEdgeName)
if PathUtils.isDrillable(shape, e, tooldiameter):
PathLog.debug(
"edge candidate: {} (hash {})is drillable ".format(
e, e.hashCode()
)
)
x = e.Curve.Center.x
y = e.Curve.Center.y
diameter = e.BoundBox.XLength
holelist.append(
{
"featureName": candidateEdgeName,
"feature": e,
"x": x,
"y": y,
"d": diameter,
"enabled": True,
}
)
features.append((baseobject, candidateEdgeName))
PathLog.debug(
"Found hole feature %s.%s"
% (baseobject.Label, candidateEdgeName)
)
else:
PathLog.debug("shape is not planar")
for i in range(len(shape.Faces)):
candidateFaceName = "Face" + str(i + 1)
f = shape.getElement(candidateFaceName)
if PathUtils.isDrillable(shape, f, tooldiameter):
PathLog.debug("face candidate: {} is drillable ".format(f))
if hasattr(f.Surface, "Center"):
x = f.Surface.Center.x
y = f.Surface.Center.y
diameter = f.BoundBox.XLength
else:
center = f.Edges[0].Curve.Center
x = center.x
y = center.y
diameter = f.Edges[0].Curve.Radius * 2
holelist.append(
{
"featureName": candidateFaceName,
"feature": f,
"x": x,
"y": y,
"d": diameter,
"enabled": True,
}
)
features.append((baseobject, candidateFaceName))
PathLog.debug(
"Found hole feature %s.%s"
% (baseobject.Label, candidateFaceName)
)
PathLog.debug("holes found: {}".format(holelist))
return features

View File

@@ -23,15 +23,19 @@
from __future__ import print_function
from Generators import drill_generator as generator
from PySide import QtCore
import FreeCAD
import Part
import Path
import PathFeedRate
import PathMachineState
import PathScripts.PathCircularHoleBase as PathCircularHoleBase
import PathScripts.PathLog as PathLog
import PathScripts.PathOp as PathOp
import PathScripts.PathUtils as PathUtils
from PySide import QtCore
__title__ = "Path Drilling Operation"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
@@ -39,9 +43,11 @@ __doc__ = "Path Drilling operation."
__contributors__ = "IMBack!"
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# PathLog.trackModule(PathLog.thisModule())
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# Qt translation handling
def translate(context, text, disambig=None):
@@ -49,104 +55,199 @@ def translate(context, text, disambig=None):
class ObjectDrilling(PathCircularHoleBase.ObjectOp):
'''Proxy object for Drilling operation.'''
"""Proxy object for Drilling operation."""
def circularHoleFeatures(self, obj):
'''circularHoleFeatures(obj) ... drilling works on anything, turn on all Base geometries and Locations.'''
return PathOp.FeatureBaseGeometry | PathOp.FeatureLocations | PathOp.FeatureCoolant
"""circularHoleFeatures(obj) ... drilling works on anything, turn on all Base geometries and Locations."""
return (
PathOp.FeatureBaseGeometry | PathOp.FeatureLocations | PathOp.FeatureCoolant
)
def initCircularHoleOperation(self, obj):
'''initCircularHoleOperation(obj) ... add drilling specific properties to obj.'''
obj.addProperty("App::PropertyLength", "PeckDepth", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Incremental Drill depth before retracting to clear chips"))
obj.addProperty("App::PropertyBool", "PeckEnabled", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable pecking"))
obj.addProperty("App::PropertyFloat", "DwellTime", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "The time to dwell between peck cycles"))
obj.addProperty("App::PropertyBool", "DwellEnabled", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable dwell"))
obj.addProperty("App::PropertyBool", "AddTipLength", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Calculate the tip length and subtract from final depth"))
obj.addProperty("App::PropertyEnumeration", "ReturnLevel", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Controls how tool retracts Default=G99"))
obj.addProperty("App::PropertyDistance", "RetractHeight", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "The height where feed starts and height during retract tool when path is finished while in a peck operation"))
obj.addProperty("App::PropertyEnumeration", "ExtraOffset", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "How far the drill depth is extended"))
"""initCircularHoleOperation(obj) ... add drilling specific properties to obj."""
obj.addProperty(
"App::PropertyLength",
"PeckDepth",
"Drill",
QtCore.QT_TRANSLATE_NOOP(
"App::Property",
"Incremental Drill depth before retracting to clear chips",
),
)
obj.addProperty(
"App::PropertyBool",
"PeckEnabled",
"Drill",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable pecking"),
)
obj.addProperty(
"App::PropertyFloat",
"DwellTime",
"Drill",
QtCore.QT_TRANSLATE_NOOP(
"App::Property", "The time to dwell between peck cycles"
),
)
obj.addProperty(
"App::PropertyBool",
"DwellEnabled",
"Drill",
QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable dwell"),
)
obj.addProperty(
"App::PropertyBool",
"AddTipLength",
"Drill",
QtCore.QT_TRANSLATE_NOOP(
"App::Property",
"Calculate the tip length and subtract from final depth",
),
)
obj.addProperty(
"App::PropertyEnumeration",
"ReturnLevel",
"Drill",
QtCore.QT_TRANSLATE_NOOP(
"App::Property", "Controls how tool retracts Default=G99"
),
)
obj.addProperty(
"App::PropertyDistance",
"RetractHeight",
"Drill",
QtCore.QT_TRANSLATE_NOOP(
"App::Property",
"The height where feed starts and height during retract tool when path is finished while in a peck operation",
),
)
obj.addProperty(
"App::PropertyEnumeration",
"ExtraOffset",
"Drill",
QtCore.QT_TRANSLATE_NOOP(
"App::Property", "How far the drill depth is extended"
),
)
obj.ReturnLevel = ['G99', 'G98'] # Canned Cycle Return Level
obj.ExtraOffset = ['None', 'Drill Tip', '2x Drill Tip'] # Canned Cycle Return Level
obj.ReturnLevel = ["G99", "G98"] # Canned Cycle Return Level
obj.ExtraOffset = [
"None",
"Drill Tip",
"2x Drill Tip",
] # Canned Cycle Return Level
def circularHoleExecute(self, obj, holes):
'''circularHoleExecute(obj, holes) ... generate drill operation for each hole in holes.'''
"""circularHoleExecute(obj, holes) ... generate drill operation for each hole in holes."""
PathLog.track()
machine = PathMachineState.MachineState()
self.commandlist.append(Path.Command("(Begin Drilling)"))
# rapid to clearance height
self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
command = Path.Command("G0", {"Z": obj.ClearanceHeight.Value})
machine.addCommand(command)
self.commandlist.append(command)
tiplength = 0.0
if obj.ExtraOffset == 'Drill Tip':
tiplength = PathUtils.drillTipLength(self.tool)
elif obj.ExtraOffset == '2x Drill Tip':
tiplength = PathUtils.drillTipLength(self.tool) * 2
self.commandlist.append(Path.Command("G90")) # Absolute distance mode
holes = PathUtils.sort_jobs(holes, ['x', 'y'])
self.commandlist.append(Path.Command('G90'))
# Calculate offsets to add to target edge
endoffset = 0.0
if obj.ExtraOffset == "Drill Tip":
endoffset = PathUtils.drillTipLength(self.tool)
elif obj.ExtraOffset == "2x Drill Tip":
endoffset = PathUtils.drillTipLength(self.tool) * 2
# http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g98-g99
self.commandlist.append(Path.Command(obj.ReturnLevel))
cmd = "G81"
cmdParams = {}
cmdParams['Z'] = obj.FinalDepth.Value - tiplength
cmdParams['F'] = self.vertFeed
cmdParams['R'] = obj.RetractHeight.Value
holes = PathUtils.sort_locations(holes, ["x", "y"])
if obj.PeckEnabled and obj.PeckDepth.Value > 0:
cmd = "G83"
cmdParams['Q'] = obj.PeckDepth.Value
elif obj.DwellEnabled and obj.DwellTime > 0:
cmd = "G82"
cmdParams['P'] = obj.DwellTime
# This section is technical debt. The computation of the
# target shapes should be factored out for re-use.
# This will likely mean refactoring upstream CircularHoleBase to pass
# spotshapes instead of holes.
# parentJob = PathUtils.findParentJob(obj)
# startHeight = obj.StartDepth.Value + parentJob.SetupSheet.SafeHeightOffset.Value
startHeight = obj.StartDepth.Value + self.job.SetupSheet.SafeHeightOffset.Value
for p in holes:
params = {}
params['X'] = p['x']
params['Y'] = p['y']
edgelist = []
for hole in holes:
v1 = FreeCAD.Vector(hole["x"], hole["y"], obj.StartDepth.Value)
v2 = FreeCAD.Vector(hole["x"], hole["y"], obj.FinalDepth.Value - endoffset)
edgelist.append(Part.makeLine(v1, v2))
# iterate the edgelist and generate gcode
for edge in edgelist:
PathLog.debug(edge)
# move to hole location
self.commandlist.append(Path.Command('G0', {'X': p['x'], 'Y': p['y'], 'F': self.horizRapid}))
self.commandlist.append(Path.Command('G0', {'Z': startHeight, 'F': self.vertRapid}))
self.commandlist.append(Path.Command('G1', {'Z': obj.StartDepth.Value, 'F': self.vertFeed}))
# Update changes to parameters
params.update(cmdParams)
command = Path.Command("G0", {"X": hole["x"], "Y": hole["y"]})
self.commandlist.append(command)
machine.addCommand(command)
# Perform canned drilling cycle
self.commandlist.append(Path.Command(cmd, params))
command = Path.Command("G0", {"Z": startHeight})
self.commandlist.append(command)
machine.addCommand(command)
# Cancel canned drilling cycle
self.commandlist.append(Path.Command('G80'))
self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value}))
command = Path.Command("G1", {"Z": obj.StartDepth.Value})
self.commandlist.append(command)
machine.addCommand(command)
# Technical Debt: We are assuming the edges are aligned.
# This assumption should be corrected and the necessary rotations
# performed to align the edge with the Z axis for drilling
# Perform drilling
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
try:
drillcommands = generator.generate(edge, dwelltime, peckdepth, repeat)
except ValueError as e: # any targets that fail the generator are ignored
PathLog.info(e)
continue
for command in drillcommands:
self.commandlist.append(command)
machine.addCommand(command)
# Cancel canned drilling cycle
self.commandlist.append(Path.Command("G80"))
command = Path.Command("G0", {"Z": obj.SafeHeight.Value})
self.commandlist.append(command)
machine.addCommand(command)
# Apply feedrates to commands
PathFeedRate.setFeedRate(self.commandlist, obj.ToolController)
def opSetDefaultValues(self, obj, job):
'''opSetDefaultValues(obj, job) ... set default value for RetractHeight'''
"""opSetDefaultValues(obj, job) ... set default value for RetractHeight"""
obj.ExtraOffset = "None"
if hasattr(job.SetupSheet, 'RetractHeight'):
if hasattr(job.SetupSheet, "RetractHeight"):
obj.RetractHeight = job.SetupSheet.RetractHeight
elif self.applyExpression(obj, 'RetractHeight', 'StartDepth+SetupSheet.SafeHeightOffset'):
elif self.applyExpression(
obj, "RetractHeight", "StartDepth+SetupSheet.SafeHeightOffset"
):
if not job:
obj.RetractHeight = 10
else:
obj.RetractHeight.Value = obj.StartDepth.Value + 1.0
if hasattr(job.SetupSheet, 'PeckDepth'):
if hasattr(job.SetupSheet, "PeckDepth"):
obj.PeckDepth = job.SetupSheet.PeckDepth
elif self.applyExpression(obj, 'PeckDepth', 'OpToolDiameter*0.75'):
elif self.applyExpression(obj, "PeckDepth", "OpToolDiameter*0.75"):
obj.PeckDepth = 1
if hasattr(job.SetupSheet, 'DwellTime'):
if hasattr(job.SetupSheet, "DwellTime"):
obj.DwellTime = job.SetupSheet.DwellTime
else:
obj.DwellTime = 1
def SetupProperties():
setup = []
setup.append("PeckDepth")
@@ -161,7 +262,7 @@ def SetupProperties():
def Create(name, obj=None, parentJob=None):
'''Create(name) ... Creates and returns a Drilling operation.'''
"""Create(name) ... Creates and returns a Drilling operation."""
if obj is None:
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
@@ -169,4 +270,4 @@ def Create(name, obj=None, parentJob=None):
if obj.Proxy:
obj.Proxy.findAllHoles(obj)
return obj
return obj

View File

@@ -87,8 +87,9 @@ CmdMoveRapid = ["G0", "G00"]
CmdMoveStraight = ["G1", "G01"]
CmdMoveCW = ["G2", "G02"]
CmdMoveCCW = ["G3", "G03"]
CmdMoveDrill = ["G81", "G82", "G83"]
CmdMoveArc = CmdMoveCW + CmdMoveCCW
CmdMove = CmdMoveStraight + CmdMoveArc
CmdMove = CmdMoveStraight + CmdMoveArc + CmdMoveDrill
CmdMoveAll = CmdMove + CmdMoveRapid

View File

@@ -29,7 +29,7 @@ import PathScripts.PathOp as PathOp
from PathScripts.PathUtils import fmt
from PathScripts.PathUtils import findParentJob
from PathScripts.PathUtils import sort_jobs
from PathScripts.PathUtils import sort_locations
from PySide import QtCore
__title__ = "Path Helix Drill Operation"
@@ -79,7 +79,7 @@ class ObjectHelix(PathCircularHoleBase.ObjectOp):
output = ''
output += "G0 Z" + fmt(zsafe)
holes = sort_jobs(holes, ['x', 'y'])
holes = sort_locations(holes, ['x', 'y'])
for hole in holes:
output += self.helix_cut(obj, hole['x'], hole['y'], hole['r'] / 2, float(obj.StartRadius.Value), (float(obj.StepOver.Value) / 50.0) * self.radius)
PathLog.debug(output)

View File

@@ -181,6 +181,14 @@ class ObjectJob:
),
)
obj.addProperty(
"App::PropertyEnumeration",
"JobType",
"Base",
QtCore.QT_TRANSLATE_NOOP("PathJob", "Select the Type of Job"),
)
obj.setEditorMode("JobType", 2) # Hide
obj.addProperty(
"App::PropertyBool",
"SplitOutput",
@@ -208,6 +216,8 @@ class ObjectJob:
obj.OrderOutputBy = ["Fixture", "Tool", "Operation"]
obj.Fixtures = ["G54"]
obj.JobType = ["2D", "2.5D", "Lathe", "Multiaxis"]
obj.PostProcessorOutputFile = PathPreferences.defaultOutputFile()
obj.PostProcessor = postProcessors = PathPreferences.allEnabledPostProcessors()
defaultPostProcessor = PathPreferences.defaultPostProcessor()
@@ -478,6 +488,17 @@ class ObjectJob:
)
obj.SplitOutput = False
if not hasattr(obj, "JobType"):
obj.addProperty(
"App::PropertyEnumeration",
"JobType",
"Base",
QtCore.QT_TRANSLATE_NOOP("PathJob", "Select the Type of Job"),
)
obj.setEditorMode("JobType", 2) # Hide
obj.JobType = ["2D", "2.5D", "Lathe", "Multiaxis"]
def onChanged(self, obj, prop):
if prop == "PostProcessor" and obj.PostProcessor:
processor = PostProcessor.load(obj.PostProcessor)

View File

@@ -28,6 +28,7 @@ import PathScripts.PathAreaOp as PathAreaOp
import PathScripts.PathLog as PathLog
import PathScripts.PathOp as PathOp
import PathScripts.PathUtils as PathUtils
import PathScripts.drillableLib as drillableLib
import math
import numpy
from PySide.QtCore import QT_TRANSLATE_NOOP
@@ -444,7 +445,7 @@ class ObjectProfile(PathAreaOp.ObjectOp):
for baseShape, wire in holes:
cont = False
f = Part.makeFace(wire, "Part::FaceMakerSimple")
drillable = PathUtils.isDrillable(baseShape, wire)
drillable = drillableLib.isDrillable(baseShape, f)
if obj.processCircles:
if drillable:

View File

@@ -21,13 +21,13 @@
# * *
# ***************************************************************************
'''Selection gates and observers to control selectability while building Path operations '''
"""Selection gates and observers to control selectability while building Path operations """
import FreeCAD
import FreeCADGui
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathUtils as PathUtils
import PathScripts.drillableLib as drillableLib
import math
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
@@ -41,12 +41,12 @@ class PathBaseGate(object):
class EGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
return sub and sub[0:4] == 'Edge'
return sub and sub[0:4] == "Edge"
class MESHGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
return obj.TypeId[0:4] == 'Mesh'
return obj.TypeId[0:4] == "Mesh"
class VCARVEGate:
@@ -59,20 +59,20 @@ class VCARVEGate:
if math.fabs(shape.Volume) < 1e-9 and len(shape.Wires) > 0:
return True
if shape.ShapeType == 'Face':
if shape.ShapeType == "Face":
return True
elif shape.ShapeType == 'Solid':
if sub and sub[0:4] == 'Face':
elif shape.ShapeType == "Solid":
if sub and sub[0:4] == "Face":
return True
elif shape.ShapeType == 'Compound':
if sub and sub[0:4] == 'Face':
elif shape.ShapeType == "Compound":
if sub and sub[0:4] == "Face":
return True
if sub:
subShape = shape.getElement(sub)
if subShape.ShapeType == 'Edge':
if subShape.ShapeType == "Edge":
return False
return False
@@ -88,35 +88,35 @@ class ENGRAVEGate(PathBaseGate):
if math.fabs(shape.Volume) < 1e-9 and len(shape.Wires) > 0:
return True
if shape.ShapeType == 'Edge':
if shape.ShapeType == "Edge":
return True
if sub:
subShape = shape.getElement(sub)
if subShape.ShapeType == 'Edge':
if subShape.ShapeType == "Edge":
return True
return False
class CHAMFERGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
try:
shape = obj.Shape
except Exception: # pylint: disable=broad-except
except Exception: # pylint: disable=broad-except
return False
if math.fabs(shape.Volume) < 1e-9 and len(shape.Wires) > 0:
return True
if 'Edge' == shape.ShapeType or 'Face' == shape.ShapeType:
if "Edge" == shape.ShapeType or "Face" == shape.ShapeType:
return True
if sub:
subShape = shape.getElement(sub)
if subShape.ShapeType == 'Edge':
if subShape.ShapeType == "Edge":
return True
elif (subShape.ShapeType == 'Face'):
elif subShape.ShapeType == "Face":
return True
return False
@@ -124,16 +124,19 @@ class CHAMFERGate(PathBaseGate):
class DRILLGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
PathLog.debug('obj: {} sub: {}'.format(obj, sub))
if hasattr(obj, "Shape") and sub:
shape = obj.Shape
subobj = shape.getElement(sub)
return PathUtils.isDrillable(shape, subobj, includePartials=True)
else:
PathLog.debug("obj: {} sub: {}".format(obj, sub))
if not hasattr(obj, "Shape") and sub:
return False
shape = obj.Shape
subobj = shape.getElement(sub)
if subobj.ShapeType not in ["Edge", "Face"]:
return False
return drillableLib.isDrillable(shape, subobj, vector=None)
class FACEGate(PathBaseGate): # formerly PROFILEGate class using allow_ORIG method as allow()
class FACEGate(
PathBaseGate
): # formerly PROFILEGate class using allow_ORIG method as allow()
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
profileable = False
@@ -142,15 +145,15 @@ class FACEGate(PathBaseGate): # formerly PROFILEGate class using allow_ORIG me
except Exception: # pylint: disable=broad-except
return False
if obj.ShapeType == 'Compound':
if sub and sub[0:4] == 'Face':
if obj.ShapeType == "Compound":
if sub and sub[0:4] == "Face":
profileable = True
elif obj.ShapeType == 'Face': # 3D Face, not flat, planar?
profileable = True # Was False
elif obj.ShapeType == "Face": # 3D Face, not flat, planar?
profileable = True # Was False
elif obj.ShapeType == 'Solid':
if sub and sub[0:4] == 'Face':
elif obj.ShapeType == "Solid":
if sub and sub[0:4] == "Face":
profileable = True
return profileable
@@ -163,27 +166,27 @@ class FACEGate(PathBaseGate): # formerly PROFILEGate class using allow_ORIG me
except Exception: # pylint: disable=broad-except
return False
if obj.ShapeType == 'Edge':
if obj.ShapeType == "Edge":
profileable = False
elif obj.ShapeType == 'Compound':
if sub and sub[0:4] == 'Face':
elif obj.ShapeType == "Compound":
if sub and sub[0:4] == "Face":
profileable = True
if sub and sub[0:4] == 'Edge':
if sub and sub[0:4] == "Edge":
profileable = False
elif obj.ShapeType == 'Face':
elif obj.ShapeType == "Face":
profileable = False
elif obj.ShapeType == 'Solid':
if sub and sub[0:4] == 'Face':
elif obj.ShapeType == "Solid":
if sub and sub[0:4] == "Face":
profileable = True
if sub and sub[0:4] == 'Edge':
if sub and sub[0:4] == "Edge":
profileable = False
elif obj.ShapeType == 'Wire':
elif obj.ShapeType == "Wire":
profileable = False
return profileable
@@ -191,7 +194,7 @@ class FACEGate(PathBaseGate): # formerly PROFILEGate class using allow_ORIG me
class PROFILEGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
if sub and sub[0:4] == 'Edge':
if sub and sub[0:4] == "Edge":
return True
try:
@@ -199,18 +202,18 @@ class PROFILEGate(PathBaseGate):
except Exception: # pylint: disable=broad-except
return False
if obj.ShapeType == 'Compound':
if sub and sub[0:4] == 'Face':
if obj.ShapeType == "Compound":
if sub and sub[0:4] == "Face":
return True
elif obj.ShapeType == 'Face':
elif obj.ShapeType == "Face":
return True
elif obj.ShapeType == 'Solid':
if sub and sub[0:4] == 'Face':
elif obj.ShapeType == "Solid":
if sub and sub[0:4] == "Face":
return True
elif obj.ShapeType == 'Wire':
elif obj.ShapeType == "Wire":
return True
return False
@@ -225,18 +228,18 @@ class POCKETGate(PathBaseGate):
except Exception: # pylint: disable=broad-except
return False
if obj.ShapeType == 'Edge':
if obj.ShapeType == "Edge":
pocketable = False
elif obj.ShapeType == 'Face':
elif obj.ShapeType == "Face":
pocketable = True
elif obj.ShapeType == 'Solid':
if sub and sub[0:4] == 'Face':
elif obj.ShapeType == "Solid":
if sub and sub[0:4] == "Face":
pocketable = True
elif obj.ShapeType == 'Compound':
if sub and sub[0:4] == 'Face':
elif obj.ShapeType == "Compound":
if sub and sub[0:4] == "Face":
pocketable = True
return pocketable
@@ -266,22 +269,22 @@ class PROBEGate:
class TURNGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
PathLog.debug('obj: {} sub: {}'.format(obj, sub))
PathLog.debug("obj: {} sub: {}".format(obj, sub))
if hasattr(obj, "Shape") and sub:
shape = obj.Shape
subobj = shape.getElement(sub)
return PathUtils.isDrillable(shape, subobj, includePartials=True)
return drillableLib.isDrillable(shape, subobj, vector=None)
else:
return False
class ALLGate(PathBaseGate):
def allow(self, doc, obj, sub): # pylint: disable=unused-argument
if sub and sub[0:6] == 'Vertex':
if sub and sub[0:6] == "Vertex":
return True
if sub and sub[0:4] == 'Edge':
if sub and sub[0:4] == "Edge":
return True
if sub and sub[0:4] == 'Face':
if sub and sub[0:4] == "Face":
return True
return False
@@ -348,7 +351,7 @@ def slotselect():
def surfaceselect():
gate = False
if(MESHGate() or FACEGate()):
if MESHGate() or FACEGate():
gate = True
FreeCADGui.Selection.addSelectionGate(gate)
if not PathPreferences.suppressSelectionModeWarning():
@@ -380,30 +383,30 @@ def turnselect():
def select(op):
opsel = {}
opsel['Contour'] = contourselect # (depreciated)
opsel['Deburr'] = chamferselect
opsel['Drilling'] = drillselect
opsel['Engrave'] = engraveselect
opsel['Helix'] = drillselect
opsel['MillFace'] = pocketselect
opsel['Pocket'] = pocketselect
opsel['Pocket 3D'] = pocketselect
opsel['Pocket Shape'] = pocketselect
opsel['Profile Edges'] = eselect # (depreciated)
opsel['Profile Faces'] = fselect # (depreciated)
opsel['Profile'] = profileselect
opsel['Slot'] = slotselect
opsel['Surface'] = surfaceselect
opsel['Waterline'] = surfaceselect
opsel['Adaptive'] = adaptiveselect
opsel['Vcarve'] = vcarveselect
opsel['Probe'] = probeselect
opsel['Custom'] = customselect
opsel['Thread Milling'] = drillselect
opsel['TurnFace'] = turnselect
opsel['TurnProfile'] = turnselect
opsel['TurnPartoff'] = turnselect
opsel['TurnRough'] = turnselect
opsel["Contour"] = contourselect # (depreciated)
opsel["Deburr"] = chamferselect
opsel["Drilling"] = drillselect
opsel["Engrave"] = engraveselect
opsel["Helix"] = drillselect
opsel["MillFace"] = pocketselect
opsel["Pocket"] = pocketselect
opsel["Pocket 3D"] = pocketselect
opsel["Pocket Shape"] = pocketselect
opsel["Profile Edges"] = eselect # (depreciated)
opsel["Profile Faces"] = fselect # (depreciated)
opsel["Profile"] = profileselect
opsel["Slot"] = slotselect
opsel["Surface"] = surfaceselect
opsel["Waterline"] = surfaceselect
opsel["Adaptive"] = adaptiveselect
opsel["Vcarve"] = vcarveselect
opsel["Probe"] = probeselect
opsel["Custom"] = customselect
opsel["Thread Milling"] = drillselect
opsel["TurnFace"] = turnselect
opsel["TurnProfile"] = turnselect
opsel["TurnPartoff"] = turnselect
opsel["TurnRough"] = turnselect
return opsel[op]

View File

@@ -20,19 +20,17 @@
# * *
# ***************************************************************************
"""PathUtils -common functions used in PathScripts for filtering, sorting, and generating gcode toolpath data """
import FreeCAD
import Path
# import PathScripts
import PathScripts.PathJob as PathJob
import PathScripts.PathGeom as PathGeom
import math
import numpy
from FreeCAD import Vector
from PathScripts import PathLog
from PySide import QtCore
from PySide import QtGui
import Path
import PathScripts.PathGeom as PathGeom
import PathScripts.PathJob as PathJob
import math
from numpy import linspace
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
@@ -73,119 +71,6 @@ def waiting_effects(function):
return new_function
def isDrillable(obj, candidate, tooldiameter=None, includePartials=False):
"""
Checks candidates to see if they can be drilled.
Candidates can be either faces - circular or cylindrical or circular edges.
The tooldiameter can be optionally passed. if passed, the check will return
False for any holes smaller than the tooldiameter.
obj=Shape
candidate = Face or Edge
tooldiameter=float
"""
PathLog.track(
"obj: {} candidate: {} tooldiameter {}".format(obj, candidate, tooldiameter)
)
if list == type(obj):
for shape in obj:
if isDrillable(shape, candidate, tooldiameter, includePartials):
return (True, shape)
return (False, None)
drillable = False
try:
if candidate.ShapeType == "Face":
face = candidate
# eliminate flat faces
if (round(face.ParameterRange[0], 8) == 0.0) and (
round(face.ParameterRange[1], 8) == round(math.pi * 2, 8)
):
for (
edge
) in face.Edges: # Find seam edge and check if aligned to Z axis.
if isinstance(edge.Curve, Part.Line):
PathLog.debug("candidate is a circle")
v0 = edge.Vertexes[0].Point
v1 = edge.Vertexes[1].Point
# check if the cylinder seam is vertically aligned. Eliminate tilted holes
if (
numpy.isclose(v1.sub(v0).x, 0, rtol=1e-05, atol=1e-06)
) and (numpy.isclose(v1.sub(v0).y, 0, rtol=1e-05, atol=1e-06)):
drillable = True
# vector of top center
lsp = Vector(
face.BoundBox.Center.x,
face.BoundBox.Center.y,
face.BoundBox.ZMax,
)
# vector of bottom center
lep = Vector(
face.BoundBox.Center.x,
face.BoundBox.Center.y,
face.BoundBox.ZMin,
)
# check if the cylindrical 'lids' are inside the base
# object. This eliminates extruded circles but allows
# actual holes.
if obj.isInside(lsp, 1e-6, False) or obj.isInside(
lep, 1e-6, False
):
PathLog.track(
"inside check failed. lsp: {} lep: {}".format(
lsp, lep
)
)
drillable = False
# eliminate elliptical holes
elif not hasattr(face.Surface, "Radius"):
PathLog.debug("candidate face has no radius attribute")
drillable = False
else:
if tooldiameter is not None:
drillable = face.Surface.Radius >= tooldiameter / 2
else:
drillable = True
elif type(face.Surface) == Part.Plane and PathGeom.pointsCoincide(
face.Surface.Axis, FreeCAD.Vector(0, 0, 1)
):
if len(face.Edges) == 1 and type(face.Edges[0].Curve) == Part.Circle:
center = face.Edges[0].Curve.Center
if obj.isInside(center, 1e-6, False):
if tooldiameter is not None:
drillable = face.Edges[0].Curve.Radius >= tooldiameter / 2
else:
drillable = True
else:
for edge in candidate.Edges:
if isinstance(edge.Curve, Part.Circle) and (
includePartials or edge.isClosed()
):
PathLog.debug("candidate is a circle or ellipse")
if not hasattr(edge.Curve, "Radius"):
PathLog.debug("No radius. Ellipse.")
drillable = False
else:
PathLog.debug("Has Radius, Circle")
if tooldiameter is not None:
drillable = edge.Curve.Radius >= tooldiameter / 2
if not drillable:
FreeCAD.Console.PrintMessage(
"Found a drillable hole with diameter: {}: "
"too small for the current tool with "
"diameter: {}".format(
edge.Curve.Radius * 2, tooldiameter
)
)
else:
drillable = True
PathLog.debug("candidate is drillable: {}".format(drillable))
except Exception as ex: # pylint: disable=broad-except
PathLog.warning(
translate("Path", "Issue determine drillability: {}").format(ex)
)
return drillable
# set at 4 decimal places for testing
def fmt(val):
return format(val, ".4f")
@@ -262,7 +147,7 @@ def horizontalFaceLoop(obj, face, faceList=None):
# verify they form a valid hole by getting the outline and comparing
# the resulting XY footprint with that of the faces
comp = Part.makeCompound([obj.Shape.getElement(f) for f in faces])
outline = TechDraw.findShapeOutline(comp, 1, FreeCAD.Vector(0, 0, 1))
outline = TechDraw.findShapeOutline(comp, 1, Vector(0, 0, 1))
# findShapeOutline always returns closed wires, by removing the
# trace-backs single edge spikes don't contribute to the bound box
@@ -289,31 +174,30 @@ def horizontalFaceLoop(obj, face, faceList=None):
def filterArcs(arcEdge):
"""filterArcs(Edge) -used to split arcs that over 180 degrees. Returns list"""
"""filterArcs(Edge) -used to split an arc that is over 180 degrees. Returns list"""
PathLog.track()
s = arcEdge
if isinstance(s.Curve, Part.Circle):
splitlist = []
angle = abs(s.LastParameter - s.FirstParameter)
# overhalfcircle = False
goodarc = False
if angle > math.pi:
pass
# overhalfcircle = True
splitlist = []
if isinstance(arcEdge.Curve, Part.Circle):
angle = abs(arcEdge.LastParameter - arcEdge.FirstParameter) # Angle in radians
goodarc = angle <= math.pi
if goodarc:
splitlist.append(arcEdge)
else:
goodarc = True
if not goodarc:
arcstpt = s.valueAt(s.FirstParameter)
arcmid = s.valueAt(
(s.LastParameter - s.FirstParameter) * 0.5 + s.FirstParameter
arcstpt = arcEdge.valueAt(arcEdge.FirstParameter)
arcmid = arcEdge.valueAt(
(arcEdge.LastParameter - arcEdge.FirstParameter) * 0.5
+ arcEdge.FirstParameter
)
arcquad1 = s.valueAt(
(s.LastParameter - s.FirstParameter) * 0.25 + s.FirstParameter
arcquad1 = arcEdge.valueAt(
(arcEdge.LastParameter - arcEdge.FirstParameter) * 0.25
+ arcEdge.FirstParameter
) # future midpt for arc1
arcquad2 = s.valueAt(
(s.LastParameter - s.FirstParameter) * 0.75 + s.FirstParameter
arcquad2 = arcEdge.valueAt(
(arcEdge.LastParameter - arcEdge.FirstParameter) * 0.75
+ arcEdge.FirstParameter
) # future midpt for arc2
arcendpt = s.valueAt(s.LastParameter)
arcendpt = arcEdge.valueAt(arcEdge.LastParameter)
# reconstruct with 2 arcs
arcseg1 = Part.ArcOfCircle(arcstpt, arcquad1, arcmid)
arcseg2 = Part.ArcOfCircle(arcmid, arcquad2, arcendpt)
@@ -322,9 +206,8 @@ def filterArcs(arcEdge):
eseg2 = arcseg2.toShape()
splitlist.append(eseg1)
splitlist.append(eseg2)
else:
splitlist.append(s)
elif isinstance(s.Curve, Part.LineSegment):
elif isinstance(arcEdge.Curve, Part.LineSegment):
pass
return splitlist
@@ -334,9 +217,7 @@ def makeWorkplane(shape):
Creates a workplane circle at the ZMin level.
"""
PathLog.track()
loc = FreeCAD.Vector(
shape.BoundBox.Center.x, shape.BoundBox.Center.y, shape.BoundBox.ZMin
)
loc = Vector(shape.BoundBox.Center.x, shape.BoundBox.Center.y, shape.BoundBox.ZMin)
c = Part.makeCircle(10, loc)
return c
@@ -388,11 +269,11 @@ def getEnvelope(partshape, subshape=None, depthparams=None):
eLength = partshape.BoundBox.ZLength - sec.BoundBox.ZMin
# Shift the section based on selection and depthparams.
newPlace = FreeCAD.Placement(FreeCAD.Vector(0, 0, zShift), sec.Placement.Rotation)
newPlace = FreeCAD.Placement(Vector(0, 0, zShift), sec.Placement.Rotation)
sec.Placement = newPlace
# Extrude the section to top of Boundbox or desired height
envelopeshape = sec.extrude(FreeCAD.Vector(0, 0, eLength))
envelopeshape = sec.extrude(Vector(0, 0, eLength))
if PathLog.getLevel(PathLog.thisModule()) == PathLog.Level.DEBUG:
removalshape = FreeCAD.ActiveDocument.addObject("Part::Feature", "Envelope")
removalshape.Shape = envelopeshape
@@ -561,211 +442,16 @@ def addToJob(obj, jobname=None):
return job
def rapid(x=None, y=None, z=None):
"""Returns gcode string to perform a rapid move."""
retstr = "G00"
if (x is not None) or (y is not None) or (z is not None):
if x is not None:
retstr += " X" + str("%.4f" % x)
if y is not None:
retstr += " Y" + str("%.4f" % y)
if z is not None:
retstr += " Z" + str("%.4f" % z)
else:
return ""
return retstr + "\n"
def feed(x=None, y=None, z=None, horizFeed=0, vertFeed=0):
"""Return gcode string to perform a linear feed."""
retstr = "G01 F"
if (x is None) and (y is None):
retstr += str("%.4f" % horizFeed)
else:
retstr += str("%.4f" % vertFeed)
if (x is not None) or (y is not None) or (z is not None):
if x is not None:
retstr += " X" + str("%.4f" % x)
if y is not None:
retstr += " Y" + str("%.4f" % y)
if z is not None:
retstr += " Z" + str("%.4f" % z)
else:
return ""
return retstr + "\n"
def arc(cx, cy, sx, sy, ex, ey, horizFeed=0, ez=None, ccw=False):
"""
Return gcode string to perform an arc.
Assumes XY plane or helix around Z
Don't worry about starting Z- assume that's dealt with elsewhere
If start/end radii aren't within eps, abort.
cx, cy -- arc center coordinates
sx, sy -- arc start coordinates
ex, ey -- arc end coordinates
ez -- ending Z coordinate. None unless helix.
horizFeed -- horiz feed speed
ccw -- arc direction
"""
eps = 0.01
if (
math.sqrt((cx - sx) ** 2 + (cy - sy) ** 2)
- math.sqrt((cx - ex) ** 2 + (cy - ey) ** 2)
) >= eps:
PathLog.error(translate("Path", "Illegal arc: Start and end radii not equal"))
return ""
retstr = ""
if ccw:
retstr += "G03 F" + str(horizFeed)
else:
retstr += "G02 F" + str(horizFeed)
retstr += " X" + str("%.4f" % ex) + " Y" + str("%.4f" % ey)
if ez is not None:
retstr += " Z" + str("%.4f" % ez)
retstr += " I" + str("%.4f" % (cx - sx)) + " J" + str("%.4f" % (cy - sy))
return retstr + "\n"
def helicalPlunge(plungePos, rampangle, destZ, startZ, toold, plungeR, horizFeed):
"""
Return gcode string to perform helical entry move.
plungePos -- vector of the helical entry location
destZ -- the lowest Z position or milling level
startZ -- Starting Z position for helical move
rampangle -- entry angle
toold -- tool diameter
plungeR -- the radius of the entry helix
"""
# toold = self.radius * 2
helixCmds = "(START HELICAL PLUNGE)\n"
if plungePos is None:
raise Exception("Helical plunging requires a position!")
helixX = plungePos.x + toold / 2 * plungeR
helixY = plungePos.y
helixCirc = math.pi * toold * plungeR
dzPerRev = math.sin(rampangle / 180.0 * math.pi) * helixCirc
# Go to the start of the helix position
helixCmds += rapid(helixX, helixY)
helixCmds += rapid(z=startZ)
# Helix as required to get to the requested depth
lastZ = startZ
curZ = max(startZ - dzPerRev, destZ)
done = False
while not done:
done = curZ == destZ
# NOTE: FreeCAD doesn't render this, but at least LinuxCNC considers it valid
# helixCmds += arc(plungePos.x, plungePos.y, helixX, helixY, helixX, helixY, ez = curZ, ccw=True)
# Use two half-helixes; FreeCAD renders that correctly,
# and it fits with the other code breaking up 360-degree arcs
helixCmds += arc(
plungePos.x,
plungePos.y,
helixX,
helixY,
helixX - toold * plungeR,
helixY,
horizFeed,
ez=(curZ + lastZ) / 2.0,
ccw=True,
)
helixCmds += arc(
plungePos.x,
plungePos.y,
helixX - toold * plungeR,
helixY,
helixX,
helixY,
horizFeed,
ez=curZ,
ccw=True,
)
lastZ = curZ
curZ = max(curZ - dzPerRev, destZ)
return helixCmds
def rampPlunge(edge, rampangle, destZ, startZ):
"""
Return gcode string to linearly ramp down to milling level.
edge -- edge to follow
rampangle -- entry angle
destZ -- Final Z depth
startZ -- Starting Z depth
FIXME: This ramps along the first edge, assuming it's long
enough, NOT just wiggling back and forth by ~0.75 * toolD.
Not sure if that's any worse, but it's simpler
I think this should be changed to be limited to a maximum ramp size. Otherwise machine time will get longer than it needs to be.
"""
rampCmds = "(START RAMP PLUNGE)\n"
if edge is None:
raise Exception("Ramp plunging requires an edge!")
sPoint = edge.Vertexes[0].Point
ePoint = edge.Vertexes[1].Point
# Evidently edges can get flipped- pick the right one in this case
if ePoint == sPoint:
# print "FLIP"
ePoint = edge.Vertexes[-1].Point
rampDist = edge.Length
rampDZ = math.sin(rampangle / 180.0 * math.pi) * rampDist
rampCmds += rapid(sPoint.x, sPoint.y)
rampCmds += rapid(z=startZ)
# Ramp down to the requested depth
curZ = max(startZ - rampDZ, destZ)
done = False
while not done:
done = curZ == destZ
# If it's an arc, handle it!
if isinstance(edge.Curve, Part.Circle):
raise Exception("rampPlunge: Screw it, not handling an arc.")
# Straight feed! Easy!
else:
rampCmds += feed(ePoint.x, ePoint.y, curZ)
rampCmds += feed(sPoint.x, sPoint.y)
curZ = max(curZ - rampDZ, destZ)
return rampCmds
def sort_jobs(locations, keys, attractors=None):
def sort_locations(locations, keys, attractors=None):
"""sort holes by the nearest neighbor method
keys: two-element list of keys for X and Y coordinates. for example ['x','y']
originally written by m0n5t3r for PathHelix
"""
from queue import PriorityQueue
from collections import defaultdict
if attractors is None:
attractors = []
try:
from queue import PriorityQueue
except ImportError:
from Queue import PriorityQueue
from collections import defaultdict
attractors = attractors or [keys[0]]
@@ -1032,7 +718,7 @@ class depth_params(object):
than max_size."""
steps_needed = math.ceil((start - stop) / max_size)
depths = list(numpy.linspace(stop, start, steps_needed, endpoint=False))
depths = list(linspace(stop, start, steps_needed, endpoint=False))
return depths
@@ -1044,7 +730,7 @@ class depth_params(object):
fullsteps = int((start - stop) / size)
last_step = start - (fullsteps * size)
depths = list(numpy.linspace(last_step, start, fullsteps, endpoint=False))
depths = list(linspace(last_step, start, fullsteps, endpoint=False))
if last_step == stop:
return depths
@@ -1108,7 +794,7 @@ def RtoIJ(startpoint, command):
chord = endpoint.sub(startpoint)
# Take its perpendicular (we assume the arc is in the XY plane)
perp = chord.cross(FreeCAD.Vector(0, 0, 1))
perp = chord.cross(Vector(0, 0, 1))
# use pythagoras to get the perp length
plength = math.sqrt(radius ** 2 - (chord.Length / 2) ** 2)

View File

@@ -0,0 +1,237 @@
import PathScripts.PathLog as PathLog
import FreeCAD as App
import Part
import numpy
import math
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
def isDrillableCylinder(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1)):
"""
checks if a candidate cylindrical face is drillable
"""
matchToolDiameter = tooldiameter is not None
matchVector = vector is not None
PathLog.debug(
"\n match tool diameter {} \n match vector {}".format(
matchToolDiameter, matchVector
)
)
def raisedFeature(obj, candidate):
# check if the cylindrical 'lids' are inside the base
# object. This eliminates extruded circles but allows
# actual holes.
startLidCenter = App.Vector(
candidate.BoundBox.Center.x,
candidate.BoundBox.Center.y,
candidate.BoundBox.ZMax,
)
endLidCenter = App.Vector(
candidate.BoundBox.Center.x,
candidate.BoundBox.Center.y,
candidate.BoundBox.ZMin,
)
return obj.isInside(startLidCenter, 1e-6, False) or obj.isInside(
endLidCenter, 1e-6, False
)
def getSeam(candidate):
# Finds the vertical seam edge in a cylinder
for e in candidate.Edges:
if isinstance(e.Curve, Part.Line): # found the seam
return e
if not candidate.ShapeType == "Face":
raise TypeError("expected a Face")
if not isinstance(candidate.Surface, Part.Cylinder):
raise TypeError("expected a cylinder")
if len(candidate.Edges) != 3:
raise TypeError("cylinder does not have 3 edges. Not supported yet")
if raisedFeature(obj, candidate):
PathLog.debug("The cylindrical face is a raised feature")
return False
if not matchToolDiameter and not matchVector:
return True
elif matchToolDiameter and tooldiameter / 2 > candidate.Surface.Radius:
PathLog.debug("The tool is larger than the target")
return False
elif matchVector and not (compareVecs(getSeam(candidate).Curve.Direction, vector)):
PathLog.debug("The feature is not aligned with the given vector")
return False
else:
return True
def isDrillableCircle(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1)):
"""
checks if a flat face or edge is drillable
"""
matchToolDiameter = tooldiameter is not None
matchVector = vector is not None
PathLog.debug(
"\n match tool diameter {} \n match vector {}".format(
matchToolDiameter, matchVector
)
)
if candidate.ShapeType == "Face":
if not type(candidate.Surface) == Part.Plane:
PathLog.debug("Drilling on non-planar faces not supported")
return False
if (
len(candidate.Edges) == 1 and type(candidate.Edges[0].Curve) == Part.Circle
): # Regular circular face
edge = candidate.Edges[0]
elif (
len(candidate.Edges) == 2
and type(candidate.Edges[0].Curve) == Part.Circle
and type(candidate.Edges[1].Curve) == Part.Circle
): # process a donut
e1 = candidate.Edges[0]
e2 = candidate.Edges[1]
edge = e1 if e1.Curve.Radius < e2.Curve.Radius else e2
else:
PathLog.debug(
"expected a Face with one or two circular edges got a face with {} edges".format(
len(candidate.Edges)
)
)
return False
else: # edge
edge = candidate
if not (isinstance(edge.Curve, Part.Circle) and edge.isClosed()):
PathLog.debug("expected a closed circular edge")
return False
if not hasattr(edge.Curve, "Radius"):
PathLog.debug("The Feature edge has no radius - Ellipse.")
return False
if not matchToolDiameter and not matchVector:
return True
elif matchToolDiameter and tooldiameter / 2 > edge.Curve.Radius:
PathLog.debug("The tool is larger than the target")
return False
elif matchVector and not (compareVecs(edge.Curve.Axis, vector)):
PathLog.debug("The feature is not aligned with the given vector")
return False
else:
return True
def isDrillable(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1)):
"""
Checks candidates to see if they can be drilled at the given vector.
Candidates can be either faces - circular or cylindrical or circular edges.
The tooldiameter can be optionally passed. if passed, the check will return
False for any holes smaller than the tooldiameter.
vector defaults to (0,0,1) which aligns with the Z axis. By default will return False
for any candidate not drillable in this orientation. Pass 'None' to vector to test whether
the hole is drillable at any orientation.
obj=Shape
candidate = Face or Edge
tooldiameter=float
vector=App.Vector or None
"""
PathLog.debug(
"obj: {} candidate: {} tooldiameter {} vector {}".format(
obj, candidate, tooldiameter, vector
)
)
if list == type(obj):
for shape in obj:
if isDrillable(shape, candidate, tooldiameter, vector):
return (True, shape)
return (False, None)
if candidate.ShapeType not in ["Face", "Edge"]:
raise TypeError("expected a Face or Edge. Got a {}".format(candidate.ShapeType))
try:
if candidate.ShapeType == "Face" and isinstance(
candidate.Surface, Part.Cylinder
):
return isDrillableCylinder(obj, candidate, tooldiameter, vector)
else:
return isDrillableCircle(obj, candidate, tooldiameter, vector)
except TypeError as e:
PathLog.debug(e)
return False
# raise TypeError("{}".format(e))
def compareVecs(vec1, vec2):
"""
compare the two vectors to see if they are aligned for drilling
alignment can indicate the vectors are the same or exactly opposite
"""
angle = vec1.getAngle(vec2)
angle = 0 if math.isnan(angle) else math.degrees(angle)
PathLog.debug("vector angle: {}".format(angle))
return numpy.isclose(angle, 0, rtol=1e-05, atol=1e-06) or numpy.isclose(
angle, 180, rtol=1e-05, atol=1e-06
)
def getDrillableTargets(obj, ToolDiameter=None, vector=App.Vector(0, 0, 1)):
"""
Returns a list of tuples for drillable subelements from the given object
[(obj,'Face1'),(obj,'Face3')]
Finds cylindrical faces that are larger than the tool diameter (if provided) and
oriented with the vector. If vector is None, all drillables are returned
"""
shp = obj.Shape
results = []
for i in range(1, len(shp.Faces)):
fname = "Face{}".format(i)
PathLog.debug(fname)
candidate = obj.getSubObject(fname)
if not isinstance(candidate.Surface, Part.Cylinder):
continue
try:
drillable = isDrillable(
shp, candidate, tooldiameter=ToolDiameter, vector=vector
)
PathLog.debug("fname: {} : drillable {}".format(fname, drillable))
except Exception as e:
PathLog.debug(e)
continue
if drillable:
results.append((obj, fname))
return results

Binary file not shown.

View File

@@ -0,0 +1,313 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2021 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
import FreeCAD as App
import PathScripts.PathLog as PathLog
import PathTests.PathTestUtils as PathTestUtils
import PathScripts.drillableLib as drillableLib
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
class TestPathDrillable(PathTestUtils.PathTestBase):
def setUp(self):
self.doc = App.open(App.getHomePath() + "/Mod/Path/PathTests/Drilling_1.FCStd")
self.obj = self.doc.getObject("Pocket010")
def tearDown(self):
App.closeDocument(self.doc.Name)
def test00(self):
"""Test CompareVecs"""
# Vec and origin
v1 = App.Vector(0, 0, 10)
v2 = App.Vector(0, 0, 0)
self.assertTrue(drillableLib.compareVecs(v1, v2))
# two valid vectors
v1 = App.Vector(0, 10, 0)
v2 = App.Vector(0, 20, 0)
self.assertTrue(drillableLib.compareVecs(v1, v2))
# two valid vectors not aligned
v1 = App.Vector(0, 10, 0)
v2 = App.Vector(10, 0, 0)
self.assertFalse(drillableLib.compareVecs(v1, v2))
def test10(self):
"""Test isDrillable"""
# Invalid types
candidate = self.obj.getSubObject("Vertex1")
self.assertRaises(
TypeError, lambda: drillableLib.isDrillable(self.obj.Shape, candidate)
)
# # partial cylinder
# candidate = self.obj.getSubObject("Face10")
# self.assertRaises(
# TypeError, lambda: drillableLib.isDrillable(self.obj.Shape, candidate)
# )
# Test cylinder faces
# thru-hole
candidate = self.obj.getSubObject("Face25")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
# Drilling with smaller bit
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=20)
)
# Drilling with bit too large
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=30)
)
# off-axis hole
candidate = self.obj.getSubObject("Face42")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Passing explicit vector
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, vector=App.Vector(0, 1, 0)
)
)
# Drilling with smaller bit
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, tooldiameter=10, vector=App.Vector(0, 1, 0)
)
)
# Drilling with bit too large
self.assertFalse(
drillableLib.isDrillable(
self.obj.Shape, candidate, tooldiameter=30, vector=App.Vector(0, 1, 0)
)
)
# ellipse hole
candidate = self.obj.getSubObject("Face20")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# raised cylinder
candidate = self.obj.getSubObject("Face30")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# cylinder on slope
candidate = self.obj.getSubObject("Face26")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Circular Faces
candidate = self.obj.getSubObject("Face51")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Passing explicit vector
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, vector=App.Vector(0, 0, 1)
)
)
# Drilling with smaller bit
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=10)
)
# Drilling with bit too large
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=30)
)
# off-axis circular face hole
candidate = self.obj.getSubObject("Face54")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Passing explicit vector
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, vector=App.Vector(0, 1, 0)
)
)
# raised face
candidate = self.obj.getSubObject("Face45")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# interrupted Face
candidate = self.obj.getSubObject("Face46")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# donut face
candidate = self.obj.getSubObject("Face44")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Test edges
# circular edge
candidate = self.obj.getSubObject("Edge53")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Passing explicit vector
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, vector=App.Vector(0, 0, 1)
)
)
# Drilling with smaller bit
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=10)
)
# Drilling with bit too large
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=30)
)
# off-axis circular edge
candidate = self.obj.getSubObject("Edge72")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Passing explicit vector
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, vector=App.Vector(0, 1, 0)
)
)
# incomplete circular edge
candidate = self.obj.getSubObject("Edge108")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# elliptical edge
candidate = self.obj.getSubObject("Edge54")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
def test20(self):
"""Test getDrillableTargets"""
results = drillableLib.getDrillableTargets(self.obj)
self.assertEqual(len(results), 15)
results = drillableLib.getDrillableTargets(self.obj, vector=None)
self.assertEqual(len(results), 18)
results = drillableLib.getDrillableTargets(self.obj, ToolDiameter= 20, vector=None)
self.assertEqual(len(results), 5)

View File

@@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2021 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 Part
import Path
import PathFeedRate
import PathMachineState
import PathScripts.PathGeom as PathGeom
import PathScripts.PathToolController as PathToolController
import PathScripts.PathUtils as PathUtils
from PathTests.PathTestUtils import PathTestBase
class TestPathHelpers(PathTestBase):
def setUp(self):
self.doc = FreeCAD.newDocument("TestPathUtils")
c1 = Path.Command("G0 Z10")
c2 = Path.Command("G0 X20 Y10")
c3 = Path.Command("G1 X20 Y10 Z5")
c4 = Path.Command("G1 X20 Y20")
self.commandlist = [c1, c2, c3, c4]
def tearDown(self):
FreeCAD.closeDocument("TestPathUtils")
def test00(self):
"""Test that FeedRate Helper populates horiz and vert feed rate based on TC"""
t = Path.Tool("test", "5.0")
tc = PathToolController.Create("TC0", t)
tc.VertRapid = 5
tc.HorizRapid = 10
tc.VertFeed = 15
tc.HorizFeed = 20
resultlist = PathFeedRate.setFeedRate(self.commandlist, tc)
print(resultlist)
self.assertTrue(resultlist[0].Parameters["F"] == 5)
self.assertTrue(resultlist[1].Parameters["F"] == 10)
self.assertTrue(resultlist[2].Parameters["F"] == 15)
self.assertTrue(resultlist[3].Parameters["F"] == 20)
def test01(self):
"""Test that Machine State initializes and stores position correctly"""
machine = PathMachineState.MachineState()
state = machine.getState()
self.assertTrue(state["X"] == 0)
self.assertTrue(state["Y"] == 0)
self.assertTrue(state["Z"] == 0)
self.assertTrue(machine.WCS == "G54")
for c in self.commandlist:
result = machine.addCommand(c)
state = machine.getState()
self.assertTrue(state["X"] == 20)
self.assertTrue(state["Y"] == 20)
self.assertTrue(state["Z"] == 5)
machine.addCommand(Path.Command("M3 S200"))
self.assertTrue(machine.S == 200)
self.assertTrue(machine.Spindle == "CW")
machine.addCommand(Path.Command("M4 S200"))
self.assertTrue(machine.Spindle == "CCW")
machine.addCommand(Path.Command("M2"))
self.assertTrue(machine.Spindle == "off")
self.assertTrue(machine.S == 0)
machine.addCommand(Path.Command("G57"))
self.assertTrue(machine.WCS == "G57")
machine.addCommand(Path.Command("M6 T5"))
self.assertTrue(machine.T == 5)
# Test that non-change commands return false
result = machine.addCommand(Path.Command("G0 X20"))
self.assertFalse(result)
result = machine.addCommand(Path.Command("G0 X30"))
self.assertTrue(result)
# Test that Drilling moves are handled correctly
result = machine.addCommand(Path.Command("G81 X50 Y50 Z0"))
state = machine.getState()
self.assertTrue(state["X"] == 50)
self.assertTrue(state["Y"] == 50)
self.assertTrue(state["Z"] == 5)
def test02(self):
"""Test PathUtils filterarcs"""
# filter a full circle
c = Part.Circle()
c.Radius = 5
edge = c.toShape()
results = PathUtils.filterArcs(edge)
self.assertTrue(len(results) == 2)
e1 = results[0]
self.assertTrue(isinstance(e1.Curve, Part.Circle))
self.assertTrue(PathGeom.pointsCoincide(edge.Curve.Location, e1.Curve.Location))
self.assertTrue(edge.Curve.Radius == e1.Curve.Radius)
# filter a 180 degree arc
results = PathUtils.filterArcs(e1)
self.assertTrue(len(results) == 1)
# Handle a straight segment
v1 = FreeCAD.Vector(0, 0, 0)
v2 = FreeCAD.Vector(10, 0, 0)
l = Part.makeLine(v1, v2)
results = PathUtils.filterArcs(l)
self.assertTrue(len(results) == 0)

View File

@@ -28,14 +28,17 @@ from PathTests.TestPathDeburr import TestPathDeburr
from PathTests.TestPathDepthParams import depthTestCases
from PathTests.TestPathDressupDogbone import TestDressupDogbone
from PathTests.TestPathDressupHoldingTags import TestHoldingTags
from PathTests.TestPathDrillable import TestPathDrillable
from PathTests.TestPathDrillGenerator import TestPathDrillGenerator
from PathTests.TestPathGeom import TestPathGeom
# from PathTests.TestPathHelix import TestPathHelix
from PathTests.TestPathHelpers import TestPathHelpers
from PathTests.TestPathLog import TestPathLog
from PathTests.TestPathOpTools import TestPathOpTools
# from PathTests.TestPathPost import PathPostTestCases
from PathTests.TestPathPreferences import TestPathPreferences
from PathTests.TestPathPropertyBag import TestPathPropertyBag
from PathTests.TestPathRotationGenerator import TestPathRotationGenerator
from PathTests.TestPathSetupSheet import TestPathSetupSheet
from PathTests.TestPathStock import TestPathStock
from PathTests.TestPathThreadMilling import TestPathThreadMilling
@@ -48,19 +51,24 @@ from PathTests.TestPathVcarve import TestPathVcarve
from PathTests.TestPathVoronoi import TestPathVoronoi
# dummy usage to get flake8 and lgtm quiet
False if depthTestCases.__name__ else True
False if TestApp.__name__ else True
False if TestDressupDogbone.__name__ else True
False if TestHoldingTags.__name__ else True
False if TestPathAdaptive.__name__ else True
False if TestPathCore.__name__ else True
False if TestPathDeburr.__name__ else True
False if TestPathGeom.__name__ else True
False if depthTestCases.__name__ else True
False if TestDressupDogbone.__name__ else True
False if TestHoldingTags.__name__ else True
# False if TestPathHelix.__name__ else True
False if TestPathDrillable.__name__ else True
False if TestPathDrillGenerator.__name__ else True
False if TestPathGeom.__name__ else True
False if TestPathHelpers.__name__ else True
False if TestPathLog.__name__ else True
False if TestPathOpTools.__name__ else True
# False if TestPathPost.__name__ else True
False if TestPathPreferences.__name__ else True
False if TestPathPropertyBag.__name__ else True
False if TestPathRotationGenerator.__name__ else True
False if TestPathSetupSheet.__name__ else True
False if TestPathStock.__name__ else True
False if TestPathThreadMilling.__name__ else True
@@ -71,4 +79,4 @@ False if TestPathTooltable.__name__ else True
False if TestPathUtil.__name__ else True
False if TestPathVcarve.__name__ else True
False if TestPathVoronoi.__name__ else True
False if TestPathDrillGenerator.__name__ else True