Merge pull request #5229 from sliptonic/feature/drill-refactor
[PATH] Refactoring drilling op to use generators
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
99
src/Mod/Path/PathFeedRate.py
Normal file
99
src/Mod/Path/PathFeedRate.py
Normal 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
|
||||
135
src/Mod/Path/PathMachineState.py
Normal file
135
src/Mod/Path/PathMachineState.py
Normal 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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
237
src/Mod/Path/PathScripts/drillableLib.py
Normal file
237
src/Mod/Path/PathScripts/drillableLib.py
Normal 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
|
||||
BIN
src/Mod/Path/PathTests/Drilling_1.FCStd
Normal file
BIN
src/Mod/Path/PathTests/Drilling_1.FCStd
Normal file
Binary file not shown.
313
src/Mod/Path/PathTests/TestPathDrillable.py
Normal file
313
src/Mod/Path/PathTests/TestPathDrillable.py
Normal 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)
|
||||
139
src/Mod/Path/PathTests/TestPathHelpers.py
Normal file
139
src/Mod/Path/PathTests/TestPathHelpers.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user