Moved the rest of the operations into Path.Op (.Gui) module

This commit is contained in:
Markus Lampert
2022-08-14 11:14:51 -07:00
parent 3f068108a8
commit d6293308b4
22 changed files with 94 additions and 94 deletions

View File

@@ -1,564 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2015 Yorik van Havre <yorik@uncreated.net> *
# * *
# * 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 FreeCADGui
import Path
import PathScripts
from Path.Dressup.Utils import toolController
from PySide import QtCore
import math
import random
from PySide.QtCore import QT_TRANSLATE_NOOP
__doc__ = """Path Array object and FreeCAD command"""
translate = FreeCAD.Qt.translate
class ObjectArray:
def __init__(self, obj):
obj.addProperty(
"App::PropertyLinkList",
"Base",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The path(s) to array"),
)
obj.addProperty(
"App::PropertyEnumeration",
"Type",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Pattern method"),
)
obj.addProperty(
"App::PropertyVectorDistance",
"Offset",
"Path",
QT_TRANSLATE_NOOP(
"App::Property",
"The spacing between the array copies in Linear pattern",
),
)
obj.addProperty(
"App::PropertyInteger",
"CopiesX",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "The number of copies in X direction in Linear pattern"
),
)
obj.addProperty(
"App::PropertyInteger",
"CopiesY",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "The number of copies in Y direction in Linear pattern"
),
)
obj.addProperty(
"App::PropertyAngle",
"Angle",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Total angle in Polar pattern"),
)
obj.addProperty(
"App::PropertyInteger",
"Copies",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "The number of copies in Linear 1D and Polar pattern"
),
)
obj.addProperty(
"App::PropertyVector",
"Centre",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "The centre of rotation in Polar pattern"
),
)
obj.addProperty(
"App::PropertyBool",
"SwapDirection",
"Path",
QT_TRANSLATE_NOOP(
"App::Property",
"Make copies in X direction before Y in Linear 2D pattern",
),
)
obj.addProperty(
"App::PropertyInteger",
"JitterPercent",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Percent of copies to randomly offset"),
)
obj.addProperty(
"App::PropertyVectorDistance",
"JitterMagnitude",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Maximum random offset of copies"),
)
obj.addProperty(
"App::PropertyInteger",
"JitterSeed",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Seed value for jitter randomness"),
)
obj.addProperty(
"App::PropertyLink",
"ToolController",
"Path",
QT_TRANSLATE_NOOP(
"App::Property",
"The tool controller that will be used to calculate the path",
),
)
obj.addProperty(
"App::PropertyBool",
"Active",
"Path",
QT_TRANSLATE_NOOP(
"PathOp", "Make False, to prevent operation from generating code"
),
)
obj.Active = True
obj.Type = ["Linear1D", "Linear2D", "Polar"]
self.setEditorModes(obj)
obj.Proxy = self
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def setEditorModes(self, obj):
if obj.Type == "Linear1D":
angleMode = centreMode = copiesXMode = copiesYMode = swapDirectionMode = 2
copiesMode = offsetMode = 0
elif obj.Type == "Linear2D":
angleMode = copiesMode = centreMode = 2
copiesXMode = copiesYMode = offsetMode = swapDirectionMode = 0
elif obj.Type == "Polar":
angleMode = copiesMode = centreMode = 0
copiesXMode = copiesYMode = offsetMode = swapDirectionMode = 2
if not hasattr(obj, "JitterSeed"):
obj.addProperty(
"App::PropertyInteger",
"JitterSeed",
"Path",
QtCore.QT_TRANSLATE_NOOP(
"App::Property", "Seed value for jitter randomness"
),
)
obj.JitterSeed = 0
obj.setEditorMode("Angle", angleMode)
obj.setEditorMode("Copies", copiesMode)
obj.setEditorMode("Centre", centreMode)
obj.setEditorMode("CopiesX", copiesXMode)
obj.setEditorMode("CopiesY", copiesYMode)
obj.setEditorMode("Offset", offsetMode)
obj.setEditorMode("SwapDirection", swapDirectionMode)
obj.setEditorMode("JitterPercent", 0)
obj.setEditorMode("JitterMagnitude", 0)
obj.setEditorMode("JitterSeed", 0)
obj.setEditorMode("ToolController", 2)
def onChanged(self, obj, prop):
if prop == "Type":
self.setEditorModes(obj)
def onDocumentRestored(self, obj):
"""onDocumentRestored(obj) ... Called automatically when document is restored."""
if not hasattr(obj, "Active"):
obj.addProperty(
"App::PropertyBool",
"Active",
"Path",
QT_TRANSLATE_NOOP(
"PathOp", "Make False, to prevent operation from generating code"
),
)
obj.Active = True
self.setEditorModes(obj)
def execute(self, obj):
# backwards compatibility for PathArrays created before support for multiple bases
if isinstance(obj.Base, list):
base = obj.Base
else:
base = [obj.Base]
if len(base) == 0:
return
obj.ToolController = toolController(base[0])
# Do not generate paths and clear current Path data if operation not
if not obj.Active:
if obj.Path:
obj.Path = Path.Path()
return
# use seed if specified, otherwise default to object name for consistency during recomputes
seed = obj.JitterSeed or obj.Name
pa = PathArray(
obj.Base,
obj.Type,
obj.Copies,
obj.Offset,
obj.CopiesX,
obj.CopiesY,
obj.Angle,
obj.Centre,
obj.SwapDirection,
obj.JitterMagnitude,
obj.JitterPercent,
seed,
)
obj.Path = pa.getPath()
class PathArray:
"""class PathArray ...
This class receives one or more base operations and repeats those operations
at set intervals based upon array type requested and the related settings for that type."""
def __init__(
self,
baseList,
arrayType,
copies,
offsetVector,
copiesX,
copiesY,
angle,
centre,
swapDirection,
jitterMagnitude=FreeCAD.Vector(0, 0, 0),
jitterPercent=0,
seed="FreeCAD",
):
self.baseList = list()
self.arrayType = arrayType # ['Linear1D', 'Linear2D', 'Polar']
self.copies = copies
self.offsetVector = offsetVector
self.copiesX = copiesX
self.copiesY = copiesY
self.angle = angle
self.centre = centre
self.swapDirection = swapDirection
self.jitterMagnitude = jitterMagnitude
self.jitterPercent = jitterPercent
self.seed = seed
if baseList:
if isinstance(baseList, list):
self.baseList = baseList
else:
self.baseList = [baseList]
def rotatePath(self, path, angle, centre):
"""
Rotates Path around given centre vector
Only X and Y is considered
"""
CmdMoveRapid = ["G0", "G00"]
CmdMoveStraight = ["G1", "G01"]
CmdMoveCW = ["G2", "G02"]
CmdMoveCCW = ["G3", "G03"]
CmdDrill = ["G73", "G81", "G82", "G83"]
CmdMoveArc = CmdMoveCW + CmdMoveCCW
CmdMove = CmdMoveStraight + CmdMoveArc
commands = []
ang = angle / 180 * math.pi
currX = 0
currY = 0
for cmd in path.Commands:
if (
(cmd.Name in CmdMoveRapid)
or (cmd.Name in CmdMove)
or (cmd.Name in CmdDrill)
):
params = cmd.Parameters
x = params.get("X")
if x is None:
x = currX
currX = x
y = params.get("Y")
if y is None:
y = currY
currY = y
# "move" the centre to origin
x = x - centre.x
y = y - centre.y
# rotation around origin:
nx = x * math.cos(ang) - y * math.sin(ang)
ny = y * math.cos(ang) + x * math.sin(ang)
# "move" the centre back and update
params.update({"X": nx + centre.x, "Y": ny + centre.y})
# Arcs need to have the I and J params rotated as well
if cmd.Name in CmdMoveArc:
i = params.get("I")
if i is None:
i = 0
j = params.get("J")
if j is None:
j = 0
ni = i * math.cos(ang) - j * math.sin(ang)
nj = j * math.cos(ang) + i * math.sin(ang)
params.update({"I": ni, "J": nj})
cmd.Parameters = params
commands.append(cmd)
newPath = Path.Path(commands)
return newPath
# Private method
def _calculateJitter(self, pos):
"""_calculateJitter(pos) ...
Returns the position argument with a random vector shift applied."""
if self.jitterPercent == 0:
pass
elif random.randint(0, 100) < self.jitterPercent:
pos.x = pos.x + random.uniform(
-self.jitterMagnitude.x, self.jitterMagnitude.x
)
pos.y = pos.y + random.uniform(
-self.jitterMagnitude.y, self.jitterMagnitude.y
)
pos.z = pos.z + random.uniform(
-self.jitterMagnitude.z, self.jitterMagnitude.z
)
return pos
# Public method
def getPath(self):
"""getPath() ... Call this method on an instance of the class to generate and return
path data for the requested path array."""
if len(self.baseList) == 0:
Path.Log.error(translate("PathArray", "No base objects for PathArray."))
return None
base = self.baseList
for b in base:
if not b.isDerivedFrom("Path::Feature"):
return
if not b.Path:
return
b_tool_controller = toolController(b)
if not b_tool_controller:
return
if b_tool_controller != toolController(base[0]):
# this may be important if Job output is split by tool controller
Path.Log.warning(
translate(
"PathArray",
"Arrays of paths having different tool controllers are handled according to the tool controller of the first path.",
)
)
# build copies
output = ""
random.seed(self.seed)
if self.arrayType == "Linear1D":
for i in range(self.copies):
pos = FreeCAD.Vector(
self.offsetVector.x * (i + 1),
self.offsetVector.y * (i + 1),
self.offsetVector.z * (i + 1),
)
pos = self._calculateJitter(pos)
for b in base:
pl = FreeCAD.Placement()
pl.move(pos)
np = Path.Path([cm.transform(pl) for cm in b.Path.Commands])
output += np.toGCode()
elif self.arrayType == "Linear2D":
if self.swapDirection:
for i in range(self.copiesY + 1):
for j in range(self.copiesX + 1):
if (i % 2) == 0:
pos = FreeCAD.Vector(
self.offsetVector.x * j,
self.offsetVector.y * i,
self.offsetVector.z * i,
)
else:
pos = FreeCAD.Vector(
self.offsetVector.x * (self.copiesX - j),
self.offsetVector.y * i,
self.offsetVector.z * i,
)
pos = self._calculateJitter(pos)
for b in base:
pl = FreeCAD.Placement()
# do not process the index 0,0. It will be processed by the base Paths themselves
if not (i == 0 and j == 0):
pl.move(pos)
np = Path.Path(
[cm.transform(pl) for cm in b.Path.Commands]
)
output += np.toGCode()
else:
for i in range(self.copiesX + 1):
for j in range(self.copiesY + 1):
if (i % 2) == 0:
pos = FreeCAD.Vector(
self.offsetVector.x * i,
self.offsetVector.y * j,
self.offsetVector.z * i,
)
else:
pos = FreeCAD.Vector(
self.offsetVector.x * i,
self.offsetVector.y * (self.copiesY - j),
self.offsetVector.z * i,
)
pos = self._calculateJitter(pos)
for b in base:
pl = FreeCAD.Placement()
# do not process the index 0,0. It will be processed by the base Paths themselves
if not (i == 0 and j == 0):
pl.move(pos)
np = Path.Path(
[cm.transform(pl) for cm in b.Path.Commands]
)
output += np.toGCode()
# Eif
else:
for i in range(self.copies):
for b in base:
ang = 360
if self.copies > 0:
ang = self.angle / self.copies * (1 + i)
np = self.rotatePath(b.Path, ang, self.centre)
output += np.toGCode()
# return output
return Path.Path(output)
class ViewProviderArray:
def __init__(self, vobj):
self.Object = vobj.Object
vobj.Proxy = self
def attach(self, vobj):
self.Object = vobj.Object
return
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def claimChildren(self):
if hasattr(self, "Object"):
if hasattr(self.Object, "Base"):
if self.Object.Base:
return self.Object.Base
return []
class CommandPathArray:
def GetResources(self):
return {
"Pixmap": "Path_Array",
"MenuText": QT_TRANSLATE_NOOP("Path_Array", "Array"),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_Array", "Creates an array from selected path(s)"
),
}
def IsActive(self):
selections = [
sel.isDerivedFrom("Path::Feature")
for sel in FreeCADGui.Selection.getSelection()
]
return selections and all(selections)
def Activated(self):
# check that the selection contains exactly what we want
selection = FreeCADGui.Selection.getSelection()
for sel in selection:
if not (sel.isDerivedFrom("Path::Feature")):
FreeCAD.Console.PrintError(
translate(
"Path_Array", "Arrays can be created only from Path operations."
)
+ "\n"
)
return
# if everything is ok, execute and register the transaction in the
# undo/redo stack
FreeCAD.ActiveDocument.openTransaction("Create Array")
FreeCADGui.addModule("PathScripts.PathArray")
FreeCADGui.addModule("PathScripts.PathUtils")
FreeCADGui.doCommand(
'obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","Array")'
)
FreeCADGui.doCommand("PathScripts.PathArray.ObjectArray(obj)")
baseString = "[%s]" % ",".join(
["FreeCAD.ActiveDocument.%s" % sel.Name for sel in selection]
)
FreeCADGui.doCommand("obj.Base = %s" % baseString)
FreeCADGui.doCommand("obj.ViewObject.Proxy = 0")
FreeCADGui.doCommand("PathScripts.PathUtils.addToJob(obj)")
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand("Path_Array", CommandPathArray())

View File

@@ -1,139 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2015 Dan Falck <ddfalck@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 *
# * *
# ***************************************************************************
"""Used for CNC machine comments for Path module. Create a comment and place it in the Document tree."""
import FreeCAD
import FreeCADGui
import Path
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
translate = FreeCAD.Qt.translate
class Comment:
def __init__(self, obj):
obj.addProperty(
"App::PropertyString",
"Comment",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Comment or note for CNC program"),
)
obj.Proxy = self
mode = 2
obj.setEditorMode("Placement", mode)
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def onChanged(self, obj, prop):
pass
def execute(self, obj):
output = ""
output += "(" + str(obj.Comment) + ")\n"
path = Path.Path(output)
obj.Path = path
class _ViewProviderComment:
def __init__(self, vobj): # mandatory
vobj.Proxy = self
mode = 2
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
def __getstate__(self): # mandatory
return None
def __setstate__(self, state): # mandatory
return None
def getIcon(self): # optional
return ":/icons/Path_Comment.svg"
def onChanged(self, vobj, prop): # optional
mode = 2
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
class CommandPathComment:
def GetResources(self):
return {
"Pixmap": "Path_Comment",
"MenuText": QT_TRANSLATE_NOOP("Path_Comment", "Comment"),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_Comment", "Add a Comment to your CNC program"
),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
for o in FreeCAD.ActiveDocument.Objects:
if o.Name[:3] == "Job":
return True
return False
def Activated(self):
FreeCAD.ActiveDocument.openTransaction("Create a Comment in your CNC program")
FreeCADGui.addModule("PathScripts.PathComment")
snippet = """
import Path
import PathScripts
from PathScripts import PathUtils
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","Comment")
PathScripts.PathComment.Comment(obj)
PathScripts.PathComment._ViewProviderComment(obj.ViewObject)
PathUtils.addToJob(obj)
"""
FreeCADGui.doCommand(snippet)
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand("Path_Comment", CommandPathComment())
FreeCAD.Console.PrintLog("Loading PathComment... done\n")

View File

@@ -1,151 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * *
# * 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 FreeCADGui
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
__doc__ = """Path Copy object and FreeCAD command"""
translate = FreeCAD.Qt.translate
class ObjectPathCopy:
def __init__(self, obj):
obj.addProperty(
"App::PropertyLink",
"Base",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The path to be copied"),
)
obj.addProperty(
"App::PropertyLink",
"ToolController",
"Path",
QT_TRANSLATE_NOOP(
"App::Property",
"The tool controller that will be used to calculate the path",
),
)
obj.Proxy = self
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def execute(self, obj):
if obj.Base:
if hasattr(obj.Base, "ToolController"):
obj.ToolController = obj.Base.ToolController
if obj.Base.Path:
obj.Path = obj.Base.Path.copy()
class ViewProviderPathCopy:
def __init__(self, vobj):
self.Object = vobj.Object
vobj.Proxy = self
def attach(self, vobj):
self.Object = vobj.Object
return
def getIcon(self):
return ":/icons/Path_Copy.svg"
def __getstate__(self):
return None
def __setstate__(self, state):
return None
class CommandPathCopy:
def GetResources(self):
return {
"Pixmap": "Path_Copy",
"MenuText": QT_TRANSLATE_NOOP("Path_Copy", "Copy"),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_Copy", "Creates a linked copy of another path"
),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
for o in FreeCAD.ActiveDocument.Objects:
if o.Name[:3] == "Job":
return True
return False
def Activated(self):
FreeCAD.ActiveDocument.openTransaction("Create Copy")
FreeCADGui.addModule("PathScripts.PathCopy")
consolecode = """
import Path
import PathScripts
from PathScripts import PathCopy
selGood = True
# check that the selection contains exactly what we want
selection = FreeCADGui.Selection.getSelection()
proj = selection[0].InList[0] #get the group that the selectied object is inside
if len(selection) != 1:
FreeCAD.Console.PrintError(translate("Path_Copy", "Please select one path object")+"\n")
selGood = False
if not selection[0].isDerivedFrom("Path::Feature"):
FreeCAD.Console.PrintError(translate("Path_Copy", "The selected object is not a path")+"\n")
selGood = False
if selGood:
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", str(selection[0].Name)+'_Copy')
PathScripts.PathCopy.ObjectPathCopy(obj)
PathScripts.PathCopy.ViewProviderPathCopy(obj.ViewObject)
obj.Base = FreeCAD.ActiveDocument.getObject(selection[0].Name)
if hasattr(obj.Base, 'ToolController'):
obj.ToolController = obj.Base.ToolController
g = proj.Group
g.append(obj)
proj.Group = g
FreeCAD.ActiveDocument.recompute()
"""
FreeCADGui.doCommand(consolecode)
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand("Path_Copy", CommandPathCopy())
FreeCAD.Console.PrintLog("Loading PathCopy... done\n")

View File

@@ -1,638 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import Part
import Path
import math
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
PathUtils = LazyLoader("PathScripts.PathUtils", globals(), "PathScripts.PathUtils")
__title__ = "Path Features Extensions"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Class and implementation of face extensions features."
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
translate = FreeCAD.Qt.translate
def endPoints(edgeOrWire):
"""endPoints(edgeOrWire) ... return the first and last point of the wire or the edge, assuming the argument is not a closed wire."""
if Part.Wire == type(edgeOrWire):
# edges = edgeOrWire.Edges
pts = [e.valueAt(e.FirstParameter) for e in edgeOrWire.Edges]
pts.extend([e.valueAt(e.LastParameter) for e in edgeOrWire.Edges])
unique = []
for p in pts:
cnt = len([p2 for p2 in pts if Path.Geom.pointsCoincide(p, p2)])
if 1 == cnt:
unique.append(p)
return unique
pfirst = edgeOrWire.valueAt(edgeOrWire.FirstParameter)
plast = edgeOrWire.valueAt(edgeOrWire.LastParameter)
if Path.Geom.pointsCoincide(pfirst, plast):
return None
return [pfirst, plast]
def includesPoint(p, pts):
"""includesPoint(p, pts) ... answer True if the collection of pts includes the point p"""
for pt in pts:
if Path.Geom.pointsCoincide(p, pt):
return True
return False
def selectOffsetWire(feature, wires):
"""selectOffsetWire(feature, wires) ... returns the Wire in wires which is does not intersect with feature"""
closest = None
for w in wires:
dist = feature.distToShape(w)[0]
if closest is None or dist > closest[0]:
closest = (dist, w)
if closest is not None:
return closest[1]
return None
def extendWire(feature, wire, length):
"""extendWire(wire, length) ... return a closed Wire which extends wire by length"""
Path.Log.track(length)
if not length or length == 0:
return None
try:
off2D = wire.makeOffset2D(length)
except FreeCAD.Base.FreeCADError as ee:
Path.Log.debug(ee)
return None
endPts = endPoints(wire) # Assumes wire is NOT closed
if endPts:
edges = [
e
for e in off2D.Edges
if Part.Circle != type(e.Curve) or not includesPoint(e.Curve.Center, endPts)
]
wires = [Part.Wire(e) for e in Part.sortEdges(edges)]
offset = selectOffsetWire(feature, wires)
ePts = endPoints(offset)
if ePts and len(ePts) > 1:
l0 = (ePts[0] - endPts[0]).Length
l1 = (ePts[1] - endPts[0]).Length
edges = wire.Edges
if l0 < l1:
edges.append(Part.Edge(Part.LineSegment(endPts[0], ePts[0])))
edges.extend(offset.Edges)
edges.append(Part.Edge(Part.LineSegment(endPts[1], ePts[1])))
else:
edges.append(Part.Edge(Part.LineSegment(endPts[1], ePts[0])))
edges.extend(offset.Edges)
edges.append(Part.Edge(Part.LineSegment(endPts[0], ePts[1])))
return Part.Wire(edges)
return None
def createExtension(obj, extObj, extFeature, extSub):
return Extension(
obj,
extObj,
extFeature,
extSub,
obj.ExtensionLengthDefault,
Extension.DirectionNormal,
)
def readObjExtensionFeature(obj):
"""readObjExtensionFeature(obj)...
Return three item string tuples (base name, feature, subfeature) extracted from obj.ExtensionFeature"""
extensions = []
for extObj, features in obj.ExtensionFeature:
for sub in features:
extFeature, extSub = sub.split(":")
extensions.append((extObj.Name, extFeature, extSub))
return extensions
def getExtensions(obj):
Path.Log.debug("getExtenstions()")
extensions = []
i = 0
for extObj, features in obj.ExtensionFeature:
for sub in features:
extFeature, extSub = sub.split(":")
extensions.append(createExtension(obj, extObj, extFeature, extSub))
i = i + 1
return extensions
def setExtensions(obj, extensions):
Path.Log.track(obj.Label, len(extensions))
obj.ExtensionFeature = [(ext.obj, ext.getSubLink()) for ext in extensions]
def getStandardAngle(x, y):
"""getStandardAngle(x, y)...
Return standard degree angle given x and y values of vector."""
angle = math.degrees(math.atan2(y, x))
if angle < 0.0:
return angle + 360.0
return angle
def arcAdjustmentAngle(arc1, arc2):
"""arcAdjustmentAngle(arc1, arc2)...
Return adjustment angle to apply to arc2 in order to align it with arc1.
Arcs must have same center point."""
center = arc1.Curve.Center
cntr2 = arc2.Curve.Center
# Verify centers of arcs are same
if center.sub(cntr2).Length > 0.0000001:
return None
# Calculate midpoint of arc1, and standard angle from center to that midpoint
midPntArc1 = arc1.valueAt(
arc1.FirstParameter + (arc1.LastParameter - arc1.FirstParameter) / 2.0
)
midPntVect1 = midPntArc1.sub(center)
ang1 = getStandardAngle(midPntVect1.x, midPntVect1.y)
# Calculate midpoint of arc2, and standard angle from center to that midpoint
midPntArc2 = arc2.valueAt(
arc2.FirstParameter + (arc2.LastParameter - arc2.FirstParameter) / 2.0
)
midPntVect2 = midPntArc2.sub(center)
ang2 = getStandardAngle(midPntVect2.x, midPntVect2.y)
# Return adjustment angle to apply to arc2 in order to align with arc1
return ang1 - ang2
class Extension(object):
DirectionNormal = 0
DirectionX = 1
DirectionY = 2
def __init__(self, op, obj, feature, sub, length, direction):
Path.Log.debug(
"Extension(%s, %s, %s, %.2f, %s"
% (obj.Label, feature, sub, length, direction)
)
self.op = op
self.obj = obj
self.feature = feature
self.sub = sub
self.length = length
self.direction = direction
self.extFaces = None
self.isDebug = True if Path.Log.getLevel(Path.Log.thisModule()) == 4 else False
self.avoid = False
if sub.startswith("Avoid_"):
self.avoid = True
self.wire = None
def getSubLink(self):
return "%s:%s" % (self.feature, self.sub)
def _extendEdge(self, feature, e0, direction):
Path.Log.track(feature, e0, direction)
if isinstance(e0.Curve, Part.Line) or isinstance(e0.Curve, Part.LineSegment):
e2 = e0.copy()
off = self.length.Value * direction
e2.translate(off)
e2 = Path.Geom.flipEdge(e2)
e1 = Part.Edge(
Part.LineSegment(
e0.valueAt(e0.LastParameter), e2.valueAt(e2.FirstParameter)
)
)
e3 = Part.Edge(
Part.LineSegment(
e2.valueAt(e2.LastParameter), e0.valueAt(e0.FirstParameter)
)
)
wire = Part.Wire([e0, e1, e2, e3])
self.wire = wire
return wire
return extendWire(feature, Part.Wire([e0]), self.length.Value)
def _getEdgeNumbers(self):
if "Wire" in self.sub:
numbers = [nr for nr in self.sub[5:-1].split(",")]
else:
numbers = [self.sub[4:]]
Path.Log.debug("_getEdgeNumbers() -> %s" % numbers)
return numbers
def _getEdgeNames(self):
return ["Edge%s" % nr for nr in self._getEdgeNumbers()]
def _getEdges(self):
return [self.obj.Shape.getElement(sub) for sub in self._getEdgeNames()]
def _getDirectedNormal(self, p0, normal):
poffPlus = p0 + 0.01 * normal
poffMinus = p0 - 0.01 * normal
if not self.obj.Shape.isInside(poffPlus, 0.005, True):
return normal
if not self.obj.Shape.isInside(poffMinus, 0.005, True):
return normal.negative()
return None
def _getDirection(self, wire):
e0 = wire.Edges[0]
midparam = e0.FirstParameter + 0.5 * (e0.LastParameter - e0.FirstParameter)
tangent = e0.tangentAt(midparam)
Path.Log.track("tangent", tangent, self.feature, self.sub)
normal = tangent.cross(FreeCAD.Vector(0, 0, 1))
if Path.Geom.pointsCoincide(normal, FreeCAD.Vector(0, 0, 0)):
return None
return self._getDirectedNormal(e0.valueAt(midparam), normal.normalize())
def getExtensionFaces(self, extensionWire):
"""getExtensionFace(extensionWire)...
A public helper method to retrieve the requested extension as a face,
rather than a wire because some extensions require a face shape
for definition that allows for two wires for boundary definition.
"""
if self.extFaces:
return self.extFaces
return [Part.Face(extensionWire)]
def getWire(self):
"""getWire()... Public method to retrieve the extension area, pertaining to the feature
and sub element provided at class instantiation, as a closed wire. If no closed wire
is possible, a `None` value is returned."""
return self._getRegularWire()
def _getRegularWire(self):
"""_getRegularWire()... Private method to retrieve the extension area, pertaining to the feature
and sub element provided at class instantiation, as a closed wire. If no closed wire
is possible, a `None` value is returned."""
Path.Log.track()
length = self.length.Value
if Path.Geom.isRoughly(0, length) or not self.sub:
Path.Log.debug("no extension, length=%.2f, sub=%s" % (length, self.sub))
return None
feature = self.obj.Shape.getElement(self.feature)
edges = self._getEdges()
sub = Part.Wire(Part.sortEdges(edges)[0])
if 1 == len(edges):
Path.Log.debug("Extending single edge wire")
edge = edges[0]
if Part.Circle == type(edge.Curve):
Path.Log.debug("is Part.Circle")
circle = edge.Curve
# for a circle we have to figure out if it's a hole or a cylinder
p0 = edge.valueAt(edge.FirstParameter)
normal = (edge.Curve.Center - p0).normalize()
direction = self._getDirectedNormal(p0, normal)
if direction is None:
return None
if Path.Geom.pointsCoincide(normal, direction):
r = circle.Radius - length
else:
r = circle.Radius + length
# assuming the offset produces a valid circle - go for it
if r > 0:
Path.Log.debug("radius > 0 - extend outward")
e3 = Part.makeCircle(
r,
circle.Center,
circle.Axis,
edge.FirstParameter * 180 / math.pi,
edge.LastParameter * 180 / math.pi,
)
# Determine if rotational alignment is necessary for new arc
rotationAdjustment = arcAdjustmentAngle(edge, e3)
if not Path.Geom.isRoughly(rotationAdjustment, 0.0):
e3.rotate(
edge.Curve.Center,
FreeCAD.Vector(0.0, 0.0, 1.0),
rotationAdjustment,
)
if endPoints(edge):
Path.Log.debug("Make section of donut")
# need to construct the arc slice
e0 = Part.makeLine(
edge.valueAt(edge.FirstParameter),
e3.valueAt(e3.FirstParameter),
)
e2 = Part.makeLine(
edge.valueAt(edge.LastParameter),
e3.valueAt(e3.LastParameter),
)
wire = Part.Wire([e0, edge, e2, e3])
# Determine if calculated extension collides with model (wrong direction)
face = Part.Face(wire)
if face.common(feature).Area < face.Area * 0.10:
return wire # Calculated extension is correct
else:
return None # Extension collides with model
extWire = Part.Wire([e3])
self.extFaces = [self._makeCircularExtFace(edge, extWire)]
return extWire
Path.Log.debug("radius < 0 - extend inward")
# the extension is bigger than the hole - so let's just cover the whole hole
if endPoints(edge):
# if the resulting arc is smaller than the radius, create a pie slice
Path.Log.track()
center = circle.Center
e0 = Part.makeLine(center, edge.valueAt(edge.FirstParameter))
e2 = Part.makeLine(edge.valueAt(edge.LastParameter), center)
return Part.Wire([e0, edge, e2])
Path.Log.track()
return Part.Wire([edge])
else:
Path.Log.debug("else is NOT Part.Circle")
Path.Log.track(self.feature, self.sub, type(edge.Curve), endPoints(edge))
direction = self._getDirection(sub)
if direction is None:
return None
return self._extendEdge(feature, edges[0], direction)
elif sub.isClosed():
Path.Log.debug("Extending multi-edge closed wire")
subFace = Part.Face(sub)
featFace = Part.Face(feature.Wires[0])
isOutside = True
if not Path.Geom.isRoughly(featFace.Area, subFace.Area):
length = -1.0 * length
isOutside = False
try:
off2D = sub.makeOffset2D(length)
except FreeCAD.Base.FreeCADError as ee:
Path.Log.debug(ee)
return None
if isOutside:
self.extFaces = [Part.Face(off2D).cut(featFace)]
else:
self.extFaces = [subFace.cut(Part.Face(off2D))]
return off2D
Path.Log.debug("Extending multi-edge open wire")
extendedWire = extendWire(feature, sub, length)
if extendedWire is None:
return extendedWire
# Trim wire face using model
extFace = Part.Face(extendedWire)
trimmedWire = extFace.cut(self.obj.Shape).Wires[0]
return trimmedWire.copy()
def _makeCircularExtFace(self, edge, extWire):
"""_makeCircularExtensionFace(edge, extWire)...
Create proper circular extension face shape. Incoming edge is expected to be a circle.
"""
# Add original outer wire to cut faces if necessary
edgeFace = Part.Face(Part.Wire([edge]))
edgeFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - edgeFace.BoundBox.ZMin))
extWireFace = Part.Face(extWire)
extWireFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - extWireFace.BoundBox.ZMin))
if extWireFace.Area >= edgeFace.Area:
extensionFace = extWireFace.cut(edgeFace)
else:
extensionFace = edgeFace.cut(extWireFace)
extensionFace.translate(FreeCAD.Vector(0.0, 0.0, edge.BoundBox.ZMin))
return extensionFace
# Eclass
def initialize_properties(obj):
"""initialize_properties(obj)... Adds feature properties to object argument"""
if not hasattr(obj, "ExtensionLengthDefault"):
obj.addProperty(
"App::PropertyDistance",
"ExtensionLengthDefault",
"Extension",
QT_TRANSLATE_NOOP("App::Property", "Default length of extensions."),
)
if not hasattr(obj, "ExtensionFeature"):
obj.addProperty(
"App::PropertyLinkSubListGlobal",
"ExtensionFeature",
"Extension",
QT_TRANSLATE_NOOP("App::Property", "List of features to extend."),
)
if not hasattr(obj, "ExtensionCorners"):
obj.addProperty(
"App::PropertyBool",
"ExtensionCorners",
"Extension",
QT_TRANSLATE_NOOP(
"App::Property",
"When enabled connected extension edges are combined to wires.",
),
)
obj.ExtensionCorners = True
obj.setEditorMode("ExtensionFeature", 2)
def set_default_property_values(obj, job):
"""set_default_property_values(obj, job) ... set default values for feature properties"""
obj.ExtensionCorners = True
obj.setExpression("ExtensionLengthDefault", "OpToolDiameter / 2.0")
def SetupProperties():
"""SetupProperties()... Returns list of feature property names"""
setup = ["ExtensionLengthDefault", "ExtensionFeature", "ExtensionCorners"]
return setup
# Extend outline face generation function
def getExtendOutlineFace(
base_shape, face, extension, remHoles=False, offset_tolerance=1e-4
):
"""getExtendOutlineFace(obj, base_shape, face, extension, remHoles) ...
Creates an extended face for the pocket, taking into consideration lateral
collision with the greater base shape.
Arguments are:
parent base shape of face,
target face,
extension magnitude,
remove holes boolean,
offset tolerance = 1e-4 default
The default value of 1e-4 for offset tolerance is the same default value
at getOffsetArea() function definition.
Return is an all access face extending the specified extension value from the source face.
"""
# Make offset face per user-specified extension distance so as to allow full clearing of face where possible.
offset_face = PathUtils.getOffsetArea(
face, extension, removeHoles=remHoles, plane=face, tolerance=offset_tolerance
)
if not offset_face:
Path.Log.error("Failed to offset a selected face.")
return None
# Apply collision detection by limiting extended face using base shape
depth = 0.2
offset_ext = offset_face.extrude(FreeCAD.Vector(0.0, 0.0, depth))
face_del = offset_face.extrude(FreeCAD.Vector(0.0, 0.0, -1.0 * depth))
clear = base_shape.cut(face_del)
available = offset_ext.cut(clear)
available.removeSplitter()
# Debug
# Part.show(available)
# FreeCAD.ActiveDocument.ActiveObject.Label = "available"
# Identify bottom face of available volume
zmin = available.BoundBox.ZMax
bottom_faces = list()
for f in available.Faces:
bbx = f.BoundBox
zNorm = abs(f.normalAt(0.0, 0.0).z)
if (
Path.Geom.isRoughly(zNorm, 1.0)
and Path.Geom.isRoughly(bbx.ZMax - bbx.ZMin, 0.0)
and Path.Geom.isRoughly(bbx.ZMin, face.BoundBox.ZMin)
):
if bbx.ZMin < zmin:
bottom_faces.append(f)
if bottom_faces:
extended = None
for bf in bottom_faces:
# Drop travel face to same height as source face
diff = face.BoundBox.ZMax - bf.BoundBox.ZMax
bf.translate(FreeCAD.Vector(0.0, 0.0, diff))
cmn = bf.common(face)
if hasattr(cmn, "Area") and cmn.Area > 0.0:
extended = bf
return extended
Path.Log.error("No bottom face for extend outline.")
return None
# Waterline extension face generation function
def getWaterlineFace(base_shape, face):
"""getWaterlineFace(base_shape, face) ...
Creates a waterline extension face for the target face,
taking into consideration the greater base shape.
Arguments are: parent base shape and target face.
Return is a waterline face at height of the target face.
"""
faceHeight = face.BoundBox.ZMin
# Get envelope of model to height of face, then fuse with model and refine the shape
baseBB = base_shape.BoundBox
depthparams = PathUtils.depth_params(
clearance_height=faceHeight,
safe_height=faceHeight,
start_depth=faceHeight,
step_down=math.floor(faceHeight - baseBB.ZMin + 2.0),
z_finish_step=0.0,
final_depth=baseBB.ZMin,
user_depths=None,
)
env = PathUtils.getEnvelope(
partshape=base_shape, subshape=None, depthparams=depthparams
)
# Get top face(s) of envelope at face height
rawList = list()
for f in env.Faces:
if Path.Geom.isRoughly(f.BoundBox.ZMin, faceHeight):
rawList.append(f)
# make compound and extrude downward
rawComp = Part.makeCompound(rawList)
rawCompExtNeg = rawComp.extrude(
FreeCAD.Vector(0.0, 0.0, baseBB.ZMin - faceHeight - 1.0)
)
# Cut off bottom of base shape at face height
topSolid = base_shape.cut(rawCompExtNeg)
# Get intersection with base shape
# The commented version returns waterlines that only intersects horizontal faces at same height as target face
# cmn = base_shape.common(rawComp)
# waterlineShape = cmn.cut(topSolid)
# return waterlineShape
# This version returns more of a true waterline flowing from target face
waterlineShape = rawComp.cut(topSolid)
faces = list()
for f in waterlineShape.Faces:
cmn = face.common(f)
if hasattr(cmn, "Area") and cmn.Area > 0.0:
faces.append(f)
if faces:
return Part.makeCompound(faces)
return None

View File

@@ -1,744 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from PySide import QtCore, QtGui
from pivy import coin
import FreeCAD
import FreeCADGui
import Path
import Path.Base.Gui.Util as PathGuiUtil
import Path.Op.Gui.Base as PathOpGui
import PathScripts.PathFeatureExtensions as FeatureExtensions
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
Part = LazyLoader("Part", globals(), "Part")
__title__ = "Path Feature Extensions UI"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Extensions feature page controller."
translate = FreeCAD.Qt.translate
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
class _Extension(object):
ColourEnabled = (1.0, 0.5, 1.0)
ColourDisabled = (1.0, 1.0, 0.5)
TransparencySelected = 0.0
TransparencyDeselected = 0.7
def __init__(self, obj, base, face, edge):
self.obj = obj
self.base = base
self.face = face
self.edge = edge
self.ext = None
if edge:
self.ext = FeatureExtensions.createExtension(obj, base, face, edge)
self.switch = self.createExtensionSoSwitch(self.ext)
self.root = self.switch
def createExtensionSoSwitch(self, ext):
if not ext:
return None
sep = coin.SoSeparator()
pos = coin.SoTranslation()
mat = coin.SoMaterial()
crd = coin.SoCoordinate3()
fce = coin.SoFaceSet()
hnt = coin.SoShapeHints()
numVert = list() # track number of vertices in each polygon face
try:
wire = ext.getWire()
except FreeCAD.Base.FreeCADError:
wire = None
if not wire:
return None
if isinstance(wire, (list, tuple)):
p0 = [p for p in wire[0].discretize(Deflection=0.02)]
p1 = [p for p in wire[1].discretize(Deflection=0.02)]
p2 = list(reversed(p1))
polygon = [(p.x, p.y, p.z) for p in (p0 + p2)]
else:
if ext.extFaces:
# Create polygon for each extension face in compound extensions
allPolys = list()
extFaces = ext.getExtensionFaces(wire)
for f in extFaces:
pCnt = 0
wCnt = 0
for w in f.Wires:
if wCnt == 0:
poly = [p for p in w.discretize(Deflection=0.01)]
else:
poly = [p for p in w.discretize(Deflection=0.01)][:-1]
pCnt += len(poly)
allPolys.extend(poly)
numVert.append(pCnt)
polygon = [(p.x, p.y, p.z) for p in allPolys]
else:
# poly = [p for p in wire.discretize(Deflection=0.02)][:-1]
poly = [p for p in wire.discretize(Deflection=0.02)]
polygon = [(p.x, p.y, p.z) for p in poly]
crd.point.setValues(polygon)
mat.diffuseColor = self.ColourDisabled
mat.transparency = self.TransparencyDeselected
hnt.faceType = coin.SoShapeHints.UNKNOWN_FACE_TYPE
hnt.vertexOrdering = coin.SoShapeHints.CLOCKWISE
if numVert:
# Transfer vertex counts for polygon faces
fce.numVertices.setValues(tuple(numVert))
sep.addChild(pos)
sep.addChild(mat)
sep.addChild(hnt)
sep.addChild(crd)
sep.addChild(fce)
# Finalize SoSwitch
switch = coin.SoSwitch()
switch.addChild(sep)
switch.whichChild = coin.SO_SWITCH_NONE
self.material = mat
return switch
def _setColour(self, r, g, b):
self.material.diffuseColor = (r, g, b)
def isValid(self):
return not self.root is None
def show(self):
if self.switch:
self.switch.whichChild = coin.SO_SWITCH_ALL
def hide(self):
if self.switch:
self.switch.whichChild = coin.SO_SWITCH_NONE
def enable(self, ena=True):
if ena:
self.material.diffuseColor = self.ColourEnabled
else:
self.disable()
def disable(self):
self.material.diffuseColor = self.ColourDisabled
def select(self):
self.material.transparency = self.TransparencySelected
def deselect(self):
self.material.transparency = self.TransparencyDeselected
class TaskPanelExtensionPage(PathOpGui.TaskPanelPage):
DataObject = QtCore.Qt.ItemDataRole.UserRole
DataSwitch = QtCore.Qt.ItemDataRole.UserRole + 2
Direction = {
FeatureExtensions.Extension.DirectionNormal: translate("PathPocket", "Normal"),
FeatureExtensions.Extension.DirectionX: translate("PathPocket", "X"),
FeatureExtensions.Extension.DirectionY: translate("PathPocket", "Y"),
}
def initPage(self, obj):
self.setTitle("Extensions")
self.OpIcon = ":/icons/view-axonometric.svg"
self.setIcon(self.OpIcon)
self.initialEdgeCount = -1
self.edgeCountThreshold = 30
self.fieldsSet = False
self.useOutlineCheckbox = None
self.useOutline = -1
self.extensionsCache = dict()
self.extensionsReady = False
self.enabled = True
self.lastDefaultLength = ""
self.extensions = list()
self.defaultLength = PathGuiUtil.QuantitySpinBox(
self.form.defaultLength, obj, "ExtensionLengthDefault"
)
self.form.extensionTree.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.form.extensionTree.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.switch = coin.SoSwitch()
self.obj.ViewObject.RootNode.addChild(self.switch)
self.switch.whichChild = coin.SO_SWITCH_ALL
self.model = QtGui.QStandardItemModel(self.form.extensionTree)
self.model.setHorizontalHeaderLabels(["Base", "Extension"])
"""
# russ4262: This `if` block shows all available extensions upon edit of operation with any extension enabled.
# This can cause the model(s) to overly obscured due to previews of extensions.
# Would be great if only enabled extensions were shown.
if 0 < len(obj.ExtensionFeature):
self.form.showExtensions.setCheckState(QtCore.Qt.Checked)
else:
self.form.showExtensions.setCheckState(QtCore.Qt.Unchecked)
"""
self.form.showExtensions.setCheckState(QtCore.Qt.Unchecked)
self.blockUpdateData = False
def cleanupPage(self, obj):
try:
self.obj.ViewObject.RootNode.removeChild(self.switch)
except ReferenceError:
Path.Log.debug("obj already destroyed - no cleanup required")
def getForm(self):
form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpPocketExtEdit.ui")
return form
def forAllItemsCall(self, cb):
for modelRow in range(self.model.rowCount()):
model = self.model.item(modelRow, 0)
for featureRow in range(model.rowCount()):
feature = model.child(featureRow, 0)
for edgeRow in range(feature.rowCount()):
item = feature.child(edgeRow, 0)
ext = item.data(self.DataObject)
cb(item, ext)
def currentExtensions(self):
Path.Log.debug("currentExtensions()")
extensions = []
def extractExtension(item, ext):
if ext and ext.edge and item.checkState() == QtCore.Qt.Checked:
extensions.append(ext.ext)
if self.form.enableExtensions.isChecked():
self.forAllItemsCall(extractExtension)
Path.Log.track("extensions", extensions)
return extensions
def updateProxyExtensions(self, obj):
Path.Log.debug("updateProxyExtensions()")
self.extensions = self.currentExtensions()
FeatureExtensions.setExtensions(obj, self.extensions)
def getFields(self, obj):
Path.Log.track(obj.Label, self.model.rowCount(), self.model.columnCount())
self.blockUpdateData = True
if obj.ExtensionCorners != self.form.extendCorners.isChecked():
obj.ExtensionCorners = self.form.extendCorners.isChecked()
self.defaultLength.updateProperty()
self.updateProxyExtensions(obj)
self.blockUpdateData = False
def setFields(self, obj):
Path.Log.track(obj.Label)
# Path.Log.debug("setFields()")
if obj.ExtensionCorners != self.form.extendCorners.isChecked():
self.form.extendCorners.toggle()
self._autoEnableExtensions() # Check edge count for auto-disable Extensions on initial Task Panel loading
self._initializeExtensions(obj) # Efficiently initialize Extensions
self.defaultLength.updateSpinBox()
self._getUseOutlineState() # Find `useOutline` checkbox and get its boolean value
self.lastDefaultLength = self.form.defaultLength.text() # set last DL value
self.fieldsSet = True # flag to identify initial values set
def _initializeExtensions(self, obj):
"""_initializeExtensions()...
Subroutine called inside `setFields()` to initialize Extensions efficiently."""
if self.enabled:
self.extensions = FeatureExtensions.getExtensions(obj)
elif len(obj.ExtensionFeature) > 0:
self.extensions = FeatureExtensions.getExtensions(obj)
self.form.enableExtensions.setChecked(True)
self._includeEdgesAndWires()
else:
self.form.extensionEdit.setDisabled(True)
self.setExtensions(self.extensions)
def _applyDefaultLengthChange(self, index=None):
"""_applyDefaultLengthChange(index=None)...
Helper method to update Default Length spinbox,
and update extensions due to change in Default Length."""
self.defaultLength.updateSpinBox()
if self.form.defaultLength.text() != self.lastDefaultLength:
self.lastDefaultLength = self.form.defaultLength.text()
self._resetCachedExtensions() # Reset extension cache because extension dimensions likely changed
self._enableExtensions() # Recalculate extensions
def createItemForBaseModel(self, base, sub, edges, extensions):
Path.Log.track(
base.Label, sub, "+", len(edges), len(base.Shape.getElement(sub).Edges)
)
# Path.Log.debug("createItemForBaseModel() label: {}, sub: {}, {}, edgeCnt: {}, subEdges: {}".format(base.Label, sub, '+', len(edges), len(base.Shape.getElement(sub).Edges)))
extendCorners = self.form.extendCorners.isChecked()
subShape = base.Shape.getElement(sub)
def createSubItem(label, ext0):
if ext0.root:
self.switch.addChild(ext0.root)
item0 = QtGui.QStandardItem()
item0.setData(label, QtCore.Qt.EditRole)
item0.setData(ext0, self.DataObject)
item0.setCheckable(True)
for e in extensions:
if e.obj == base and e.sub == label:
item0.setCheckState(QtCore.Qt.Checked)
ext0.enable()
break
item.appendRow([item0])
# ext = self._cachedExtension(self.obj, base, sub, None)
ext = None
item = QtGui.QStandardItem()
item.setData(sub, QtCore.Qt.EditRole)
item.setData(ext, self.DataObject)
item.setSelectable(False)
extensionEdges = {}
if self.useOutline == 1 and sub.startswith("Face"):
# Only show exterior extensions if `Use Outline` is True
subEdges = subShape.Wires[0].Edges
else:
# Show all exterior and interior extensions if `Use Outline` is False
subEdges = subShape.Edges
for edge in subEdges:
for (e, label) in edges:
if edge.isSame(e):
ext1 = self._cachedExtension(self.obj, base, sub, label)
if ext1.isValid():
extensionEdges[e] = label[4:] # isolate edge number
if not extendCorners:
createSubItem(label, ext1)
if extendCorners:
def edgesMatchShape(e0, e1):
flipped = Path.Geom.flipEdge(e1)
if flipped:
return Path.Geom.edgesMatch(e0, e1) or Path.Geom.edgesMatch(
e0, flipped
)
else:
return Path.Geom.edgesMatch(e0, e1)
self.extensionEdges = extensionEdges
Path.Log.debug("extensionEdges.values(): {}".format(extensionEdges.values()))
for edgeList in Part.sortEdges(
list(extensionEdges.keys())
): # Identify connected edges that form wires
self.edgeList = edgeList
if len(edgeList) == 1:
label = (
"Edge%s"
% [
extensionEdges[keyEdge]
for keyEdge in extensionEdges.keys()
if edgesMatchShape(keyEdge, edgeList[0])
][0]
)
else:
label = "Wire(%s)" % ",".join(
sorted(
[
extensionEdges[keyEdge]
for e in edgeList
for keyEdge in extensionEdges.keys()
if edgesMatchShape(e, keyEdge)
],
key=lambda s: int(s),
)
)
ext2 = self._cachedExtension(self.obj, base, sub, label)
createSubItem(label, ext2)
return item
def setExtensions(self, extensions):
Path.Log.track(len(extensions))
Path.Log.debug("setExtensions()")
if self.extensionsReady:
Path.Log.debug("setExtensions() returning per `extensionsReady` flag")
return
self.form.extensionTree.blockSignals(True)
# remember current visual state
if hasattr(self, "selectionModel"):
selectedExtensions = [
self.model.itemFromIndex(index).data(self.DataObject).ext
for index in self.selectionModel.selectedIndexes()
]
else:
selectedExtensions = []
collapsedModels = []
collapsedFeatures = []
for modelRow in range(self.model.rowCount()):
model = self.model.item(modelRow, 0)
modelName = model.data(QtCore.Qt.EditRole)
if not self.form.extensionTree.isExpanded(model.index()):
collapsedModels.append(modelName)
for featureRow in range(model.rowCount()):
feature = model.child(featureRow, 0)
if not self.form.extensionTree.isExpanded(feature.index()):
collapsedFeatures.append(
"%s.%s" % (modelName, feature.data(QtCore.Qt.EditRole))
)
# remove current extensions and all their visuals
def removeItemSwitch(item, ext):
ext.hide()
if ext.root:
self.switch.removeChild(ext.root)
self.forAllItemsCall(removeItemSwitch)
self.model.clear()
# create extensions for model and given argument
if self.enabled:
for base in self.obj.Base:
show = False
edges = [
(edge, "Edge%d" % (i + 1))
for i, edge in enumerate(base[0].Shape.Edges)
]
baseItem = QtGui.QStandardItem()
baseItem.setData(base[0].Label, QtCore.Qt.EditRole)
baseItem.setSelectable(False)
for sub in sorted(base[1]):
if sub.startswith("Face"):
show = True
baseItem.appendRow(
self.createItemForBaseModel(base[0], sub, edges, extensions)
)
if show:
self.model.appendRow(baseItem)
self.form.extensionTree.setModel(self.model)
self.form.extensionTree.expandAll()
self.form.extensionTree.resizeColumnToContents(0)
# restore previous state - at least the parts that are still valid
for modelRow in range(self.model.rowCount()):
model = self.model.item(modelRow, 0)
modelName = model.data(QtCore.Qt.EditRole)
if modelName in collapsedModels:
self.form.extensionTree.setExpanded(model.index(), False)
for featureRow in range(model.rowCount()):
feature = model.child(featureRow, 0)
featureName = "%s.%s" % (modelName, feature.data(QtCore.Qt.EditRole))
if featureName in collapsedFeatures:
self.form.extensionTree.setExpanded(feature.index(), False)
if hasattr(self, "selectionModel") and selectedExtensions:
self.restoreSelection(selectedExtensions)
self.form.extensionTree.blockSignals(False)
self.extensionsReady = True
Path.Log.debug(" setExtensions() finished and setting `extensionsReady=True`")
def updateData(self, obj, prop):
Path.Log.track(obj.Label, prop, self.blockUpdateData)
# Path.Log.debug("updateData({})".format(prop))
if not self.blockUpdateData:
if self.fieldsSet:
if self.form.enableExtensions.isChecked():
if prop == "ExtensionLengthDefault":
self._applyDefaultLengthChange()
elif prop == "Base":
self.extensionsReady = False
self.setExtensions(FeatureExtensions.getExtensions(obj))
elif prop == "UseOutline":
self._getUseOutlineState() # Find `useOutline` checkbox and get its boolean value
self._includeEdgesAndWires()
elif prop == "Base":
self.extensionsReady = False
def restoreSelection(self, selection):
Path.Log.debug("restoreSelection()")
Path.Log.track()
if 0 == self.model.rowCount():
Path.Log.track("-")
self.form.buttonClear.setEnabled(False)
self.form.buttonDisable.setEnabled(False)
self.form.buttonEnable.setEnabled(False)
else:
self.form.buttonClear.setEnabled(True)
if selection or self.selectionModel.selectedIndexes():
self.form.buttonDisable.setEnabled(True)
self.form.buttonEnable.setEnabled(True)
else:
self.form.buttonDisable.setEnabled(False)
self.form.buttonEnable.setEnabled(False)
FreeCADGui.Selection.clearSelection()
def selectItem(item, ext):
for sel in selection:
if ext.base == sel.obj and ext.edge == sel.sub:
return True
return False
def setSelectionVisuals(item, ext):
if selectItem(item, ext):
self.selectionModel.select(
item.index(), QtCore.QItemSelectionModel.Select
)
selected = self.selectionModel.isSelected(item.index())
if selected:
FreeCADGui.Selection.addSelection(ext.base, ext.face)
ext.select()
else:
ext.deselect()
if self.form.showExtensions.isChecked() or selected:
ext.show()
else:
ext.hide()
self.forAllItemsCall(setSelectionVisuals)
def selectionChanged(self):
Path.Log.debug("selectionChanged()")
self.restoreSelection([])
def extensionsClear(self):
Path.Log.debug("extensionsClear()")
def disableItem(item, ext):
item.setCheckState(QtCore.Qt.Unchecked)
ext.disable()
self.forAllItemsCall(disableItem)
self.setDirty()
def _extensionsSetState(self, state):
Path.Log.debug("_extensionsSetState()")
Path.Log.track(state)
for index in self.selectionModel.selectedIndexes():
item = self.model.itemFromIndex(index)
ext = item.data(self.DataObject)
if ext.edge:
item.setCheckState(state)
ext.enable(state == QtCore.Qt.Checked)
self.setDirty()
def extensionsDisable(self):
self._extensionsSetState(QtCore.Qt.Unchecked)
def extensionsEnable(self):
self._extensionsSetState(QtCore.Qt.Checked)
def updateItemEnabled(self, item):
Path.Log.track(item)
ext = item.data(self.DataObject)
if item.checkState() == QtCore.Qt.Checked:
ext.enable()
else:
ext.disable()
self.updateProxyExtensions(self.obj)
self.setDirty()
def showHideExtension(self):
if self.form.showExtensions.isChecked():
def enableExtensionEdit(item, ext):
ext.show()
self.forAllItemsCall(enableExtensionEdit)
else:
def disableExtensionEdit(item, ext):
if not self.selectionModel.isSelected(item.index()):
ext.hide()
self.forAllItemsCall(disableExtensionEdit)
# self.setDirty()
def toggleExtensionCorners(self):
Path.Log.debug("toggleExtensionCorners()")
Path.Log.track()
self.extensionsReady = False
extensions = FeatureExtensions.getExtensions(self.obj)
self.setExtensions(extensions)
self.selectionChanged()
self.setDirty()
def getSignalsForUpdate(self, obj):
Path.Log.track(obj.Label)
signals = []
signals.append(self.form.defaultLength.editingFinished)
signals.append(self.form.enableExtensions.toggled)
return signals
def registerSignalHandlers(self, obj):
self.form.showExtensions.clicked.connect(self.showHideExtension)
self.form.extendCorners.clicked.connect(self.toggleExtensionCorners)
self.form.buttonClear.clicked.connect(self.extensionsClear)
self.form.buttonDisable.clicked.connect(self.extensionsDisable)
self.form.buttonEnable.clicked.connect(self.extensionsEnable)
self.form.enableExtensions.toggled.connect(self._enableExtensions)
self.form.defaultLength.editingFinished.connect(self._applyDefaultLengthChange)
self.model.itemChanged.connect(self.updateItemEnabled)
self.selectionModel = self.form.extensionTree.selectionModel()
self.selectionModel.selectionChanged.connect(self.selectionChanged)
self.selectionChanged()
# Support methods
def _getUseOutlineState(self):
"""_getUseOutlineState() ...
This method locates the `useOutline` form checkbox in the `Operation` tab,
and saves that reference to self.useOutlineInput. If found, then the boolean
value of the checkbox is saved to self.useOutline.
"""
if self.useOutlineCheckbox:
self.useOutline = self.useOutlineCheckbox.isChecked()
if hasattr(self, "parent"):
parent = getattr(self, "parent")
if parent and hasattr(parent, "featurePages"):
for page in parent.featurePages:
if hasattr(page, "panelTitle"):
if page.panelTitle == "Operation" and hasattr(
page.form, "useOutline"
):
Path.Log.debug("Found useOutline checkbox")
self.useOutlineCheckbox = page.form.useOutline
if page.form.useOutline.isChecked():
self.useOutline = 1
return
else:
self.useOutline = 0
return
self.useOutline = -1
# Methods for enable and disablement of Extensions feature
def _autoEnableExtensions(self):
"""_autoEnableExtensions() ...
This method is called to determine if the Extensions feature should be enabled,
or auto disabled due to total edge count of selected faces.
The auto enable/disable feature is designed to allow quicker access
to operations that implement the Extensions feature when selected faces contain
large numbers of edges, which require long computation times for preparation.
The return value is a simple boolean to communicate whether or not Extensions
are be enabled.
"""
enabled = False
if self.form.enableExtensions.isChecked():
enabled = True
Path.Log.debug("_autoEnableExtensions() is {}".format(enabled))
self.enabled = enabled
def _enableExtensions(self):
"""_enableExtensions() ...
This method is called when the enableExtensions push button is toggled.
This method manages the enabled or disabled state of the extensionsEdit
Task Panel input group.
"""
Path.Log.debug("_enableExtensions()")
if self.form.enableExtensions.isChecked():
self.enabled = True
self.extensionsReady = False
self.form.extensionEdit.setEnabled(True)
self.extensions = FeatureExtensions.getExtensions(self.obj)
self.setExtensions(self.extensions)
else:
self.form.extensionEdit.setDisabled(True)
self.enabled = False
def _includeEdgesAndWires(self):
"""_includeEdgesAndWires() ...
This method is called when the includeEdges push button is toggled.
This method manages the state of the button and the message thereof.
"""
self._getUseOutlineState() # Find `useOutline` checkbox and get its boolean value
Path.Log.debug("_includeEdgesAndWires()")
self.extensionsReady = False
self._enableExtensions()
# Methods for creating and managing cached extensions
def _cachedExtension(self, obj, base, sub, label):
"""_cachedExtension(obj, base, sub, label)...
This method creates a new _Extension object if none is found within
the extensionCache dictionary."""
if label:
cacheLabel = base.Name + "_" + sub + "_" + label
else:
cacheLabel = base.Name + "_" + sub + "_None"
if cacheLabel in self.extensionsCache.keys():
# Path.Log.debug("return _cachedExtension({})".format(cacheLabel))
return self.extensionsCache[cacheLabel]
else:
# Path.Log.debug("_cachedExtension({}) created".format(cacheLabel))
_ext = _Extension(obj, base, sub, label)
self.extensionsCache[cacheLabel] = _ext # cache the extension
return _ext
def _resetCachedExtensions(self):
Path.Log.debug("_resetCachedExtensions()")
reset = dict()
self.extensionsCache = reset
self.extensionsReady = False
# Eclass
FreeCAD.Console.PrintLog("Loading PathFeatureExtensionsGui... done\n")

View File

@@ -1,147 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * *
# * 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 FreeCADGui
import Path
from PySide.QtCore import QT_TRANSLATE_NOOP
__doc__ = """Path Hop object and FreeCAD command"""
translate = FreeCAD.Qt.translate
class ObjectHop:
def __init__(self, obj):
obj.addProperty(
"App::PropertyLink",
"NextObject",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The object to be reached by this hop"),
)
obj.addProperty(
"App::PropertyDistance",
"HopHeight",
"Path",
QT_TRANSLATE_NOOP("App::Property", "The Z height of the hop"),
)
obj.Proxy = self
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def execute(self, obj):
nextpoint = FreeCAD.Vector()
if obj.NextObject:
if obj.NextObject.isDerivedFrom("Path::Feature"):
# look for the first position of the next path
for c in obj.NextObject.Path.Commands:
if c.Name in ["G0", "G00", "G1", "G01", "G2", "G02", "G3", "G03"]:
nextpoint = c.Placement.Base
break
# absolute coords, millimeters, cancel offsets
output = "G90\nG21\nG40\n"
# go up to the given height
output += "G0 Z" + str(obj.HopHeight.Value) + "\n"
# go horizontally to the position of nextpoint
output += "G0 X" + str(nextpoint.x) + " Y" + str(nextpoint.y) + "\n"
# print output
path = Path.Path(output)
obj.Path = path
class ViewProviderPathHop:
def __init__(self, vobj):
self.Object = vobj.Object
vobj.Proxy = self
def attach(self, vobj):
self.Object = vobj.Object
def getIcon(self):
return ":/icons/Path_Hop.svg"
def __getstate__(self):
return None
def __setstate__(self, state):
return None
class CommandPathHop:
def GetResources(self):
return {
"Pixmap": "Path_Hop",
"MenuText": QT_TRANSLATE_NOOP("Path_Hop", "Hop"),
"ToolTip": QT_TRANSLATE_NOOP("Path_Hop", "Creates a Path Hop object"),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
for o in FreeCAD.ActiveDocument.Objects:
if o.Name[:3] == "Job":
return True
return False
def Activated(self):
# check that the selection contains exactly what we want
selection = FreeCADGui.Selection.getSelection()
if len(selection) != 1:
FreeCAD.Console.PrintError(
translate("Path_Hop", "Please select one path object") + "\n"
)
return
if not selection[0].isDerivedFrom("Path::Feature"):
FreeCAD.Console.PrintError(
translate("Path_Hop", "The selected object is not a path") + "\n"
)
return
FreeCAD.ActiveDocument.openTransaction("Create Hop")
FreeCADGui.addModule("PathScripts.PathHop")
FreeCADGui.addModule("PathScripts.PathUtils")
FreeCADGui.doCommand(
'obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","Hop")'
)
FreeCADGui.doCommand("PathScripts.PathHop.ObjectHop(obj)")
FreeCADGui.doCommand("PathScripts.PathHop.ViewProviderPathHop(obj.ViewObject)")
FreeCADGui.doCommand(
"obj.NextObject = FreeCAD.ActiveDocument." + selection[0].Name
)
FreeCADGui.doCommand("PathScripts.PathUtils.addToJob(obj)")
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand("Path_Hop", CommandPathHop())
FreeCAD.Console.PrintLog("Loading PathHop... done\n")

View File

@@ -1,380 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2015 Dan Falck <ddfalck@gmail.com> *
# * Copyright (c) 2021 Schildkroet *
# * *
# * 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 *
# * *
# ***************************************************************************
"""Selection gates and observers to control selectability while building Path operations """
import FreeCAD
import FreeCADGui
import Path
import PathScripts.drillableLib as drillableLib
import math
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
# Path.Log.trackModule(Path.Log.thisModule())
class PathBaseGate(object):
pass
class EGate(PathBaseGate):
def allow(self, doc, obj, sub):
return sub and sub[0:4] == "Edge"
class MESHGate(PathBaseGate):
def allow(self, doc, obj, sub):
return obj.TypeId[0:4] == "Mesh"
class VCARVEGate:
def allow(self, doc, obj, sub):
try:
shape = obj.Shape
except Exception:
return False
if math.fabs(shape.Volume) < 1e-9 and len(shape.Wires) > 0:
return True
if shape.ShapeType == "Face":
return True
elif shape.ShapeType == "Solid":
if sub and sub[0:4] == "Face":
return True
elif shape.ShapeType == "Compound":
if sub and sub[0:4] == "Face":
return True
if sub:
subShape = shape.getElement(sub)
if subShape.ShapeType == "Edge":
return False
return False
class ENGRAVEGate(PathBaseGate):
def allow(self, doc, obj, sub):
try:
shape = obj.Shape
except Exception:
return False
if math.fabs(shape.Volume) < 1e-9 and len(shape.Wires) > 0:
return True
if shape.ShapeType == "Edge":
return True
if sub:
subShape = shape.getElement(sub)
if subShape.ShapeType == "Edge":
return True
return False
class CHAMFERGate(PathBaseGate):
def allow(self, doc, obj, sub):
try:
shape = obj.Shape
except Exception:
return False
if math.fabs(shape.Volume) < 1e-9 and len(shape.Wires) > 0:
return True
if "Edge" == shape.ShapeType or "Face" == shape.ShapeType:
return True
if sub:
subShape = shape.getElement(sub)
if subShape.ShapeType == "Edge":
return True
elif subShape.ShapeType == "Face":
return True
return False
class DRILLGate(PathBaseGate):
def allow(self, doc, obj, sub):
Path.Log.debug("obj: {} sub: {}".format(obj, sub))
if not hasattr(obj, "Shape"):
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):
def allow(self, doc, obj, sub):
isFace = False
try:
obj = obj.Shape
except Exception:
return False
if obj.ShapeType == "Compound":
if sub and sub[0:4] == "Face":
isFace = True
elif obj.ShapeType == "Face": # 3D Face, not flat, planar?
isFace = True
elif obj.ShapeType == "Solid":
if sub and sub[0:4] == "Face":
isFace = True
return isFace
class PROFILEGate(PathBaseGate):
def allow(self, doc, obj, sub):
if sub and sub[0:4] == "Edge":
return True
try:
obj = obj.Shape
except Exception:
return False
if obj.ShapeType == "Compound":
if sub and sub[0:4] == "Face":
return True
elif obj.ShapeType == "Face":
return True
elif obj.ShapeType == "Solid":
if sub and sub[0:4] == "Face":
return True
elif obj.ShapeType == "Wire":
return True
return False
class POCKETGate(PathBaseGate):
def allow(self, doc, obj, sub):
pocketable = False
try:
obj = obj.Shape
except Exception:
return False
if obj.ShapeType == "Edge":
pocketable = False
elif obj.ShapeType == "Face":
pocketable = True
elif obj.ShapeType == "Solid":
if sub and sub[0:4] == "Face":
pocketable = True
elif obj.ShapeType == "Compound":
if sub and sub[0:4] == "Face":
pocketable = True
return pocketable
class ADAPTIVEGate(PathBaseGate):
def allow(self, doc, obj, sub):
adaptive = True
try:
obj = obj.Shape
except Exception:
return False
return adaptive
class CONTOURGate(PathBaseGate):
def allow(self, doc, obj, sub):
pass
class PROBEGate:
def allow(self, doc, obj, sub):
pass
class TURNGate(PathBaseGate):
def allow(self, doc, obj, sub):
Path.Log.debug("obj: {} sub: {}".format(obj, sub))
if hasattr(obj, "Shape") and sub:
shape = obj.Shape
subobj = shape.getElement(sub)
return drillableLib.isDrillable(shape, subobj, vector=None)
else:
return False
class ALLGate(PathBaseGate):
def allow(self, doc, obj, sub):
if sub and sub[0:6] == "Vertex":
return True
if sub and sub[0:4] == "Edge":
return True
if sub and sub[0:4] == "Face":
return True
return False
def contourselect():
FreeCADGui.Selection.addSelectionGate(CONTOURGate())
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Contour Select Mode\n")
def eselect():
FreeCADGui.Selection.addSelectionGate(EGate())
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Edge Select Mode\n")
def drillselect():
FreeCADGui.Selection.addSelectionGate(DRILLGate())
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Drilling Select Mode\n")
def engraveselect():
FreeCADGui.Selection.addSelectionGate(ENGRAVEGate())
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Engraving Select Mode\n")
def fselect():
FreeCADGui.Selection.addSelectionGate(FACEGate()) # Was PROFILEGate()
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Profiling Select Mode\n")
def chamferselect():
FreeCADGui.Selection.addSelectionGate(CHAMFERGate())
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Deburr Select Mode\n")
def profileselect():
FreeCADGui.Selection.addSelectionGate(PROFILEGate())
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Profiling Select Mode\n")
def pocketselect():
FreeCADGui.Selection.addSelectionGate(POCKETGate())
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Pocketing Select Mode\n")
def adaptiveselect():
FreeCADGui.Selection.addSelectionGate(ADAPTIVEGate())
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Adaptive Select Mode\n")
def slotselect():
FreeCADGui.Selection.addSelectionGate(ALLGate())
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Slot Cutter Select Mode\n")
def surfaceselect():
gate = False
if MESHGate() or FACEGate():
gate = True
FreeCADGui.Selection.addSelectionGate(gate)
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Surfacing Select Mode\n")
def vcarveselect():
FreeCADGui.Selection.addSelectionGate(VCARVEGate())
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Vcarve Select Mode\n")
def probeselect():
FreeCADGui.Selection.addSelectionGate(PROBEGate())
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Probe Select Mode\n")
def customselect():
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Custom Select Mode\n")
def turnselect():
FreeCADGui.Selection.addSelectionGate(TURNGate())
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Turning Select Mode\n")
def select(op):
opsel = {}
opsel["Contour"] = contourselect # deprecated
opsel["Deburr"] = chamferselect
opsel["Drilling"] = drillselect
opsel["Engrave"] = engraveselect
opsel["Helix"] = drillselect
opsel["MillFace"] = pocketselect
opsel["Pocket"] = pocketselect
opsel["Pocket 3D"] = pocketselect
opsel["Pocket3D"] = pocketselect # deprecated
opsel["Pocket Shape"] = pocketselect
opsel["Profile Edges"] = eselect # deprecated
opsel["Profile Faces"] = fselect # deprecated
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["ThreadMilling"] = drillselect
opsel["TurnFace"] = turnselect
opsel["TurnProfile"] = turnselect
opsel["TurnPartoff"] = turnselect
opsel["TurnRough"] = turnselect
return opsel[op]
def clear():
FreeCADGui.Selection.removeSelectionGate()
if not Path.Preferences.suppressSelectionModeWarning():
FreeCAD.Console.PrintWarning("Free Select\n")

View File

@@ -1,90 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2015 Yorik van Havre <yorik@uncreated.net> *
# * *
# * 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 FreeCADGui
import Path
import PathScripts
from PySide.QtCore import QT_TRANSLATE_NOOP
__doc__ = """Path SimpleCopy command"""
translate = FreeCAD.Qt.translate
class CommandPathSimpleCopy:
def GetResources(self):
return {
"Pixmap": "Path_SimpleCopy",
"MenuText": QT_TRANSLATE_NOOP("Path_SimpleCopy", "Simple Copy"),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_SimpleCopy", "Creates a non-parametric copy of another path"
),
}
def IsActive(self):
if bool(FreeCADGui.Selection.getSelection()) is False:
return False
try:
obj = FreeCADGui.Selection.getSelectionEx()[0].Object
return isinstance(obj.Proxy, Path.Op.Base.ObjectOp)
except Exception:
return False
def Activated(self):
# check that the selection contains exactly what we want
selection = FreeCADGui.Selection.getSelection()
if len(selection) != 1:
FreeCAD.Console.PrintError(
translate("Path_SimpleCopy", "Please select exactly one path object")
+ "\n"
)
return
if not (selection[0].isDerivedFrom("Path::Feature")):
FreeCAD.Console.PrintError(
translate("Path_SimpleCopy", "Please select exactly one path object")
+ "\n"
)
return
FreeCAD.ActiveDocument.openTransaction("Simple Copy")
FreeCADGui.doCommand(
"srcpath = FreeCADGui.Selection.getSelectionEx()[0].Object.Path\n"
)
FreeCADGui.addModule("PathScripts.PathUtils")
FreeCADGui.addModule("Path.Op.Custom")
FreeCADGui.doCommand(
'obj = Path.Op.Custom.Create("'
+ selection[0].Name
+ '_SimpleCopy")'
)
FreeCADGui.doCommand("obj.ViewObject.Proxy = 0")
FreeCADGui.doCommand("obj.Gcode = [c.toGCode() for c in srcpath.Commands]")
FreeCADGui.doCommand("PathScripts.PathUtils.addToJob(obj)")
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand("Path_SimpleCopy", CommandPathSimpleCopy())

View File

@@ -1,149 +0,0 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2015 Dan Falck <ddfalck@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 *
# * *
# ***************************************************************************
"""Used for CNC machine Stops for Path module. Create an Optional or Mandatory Stop."""
import FreeCAD
import FreeCADGui
import Path
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
translate = FreeCAD.Qt.translate
class Stop:
def __init__(self, obj):
obj.addProperty(
"App::PropertyEnumeration",
"Stop",
"Path",
QT_TRANSLATE_NOOP(
"App::Property", "Add Optional or Mandatory Stop to the program"
),
)
obj.Stop = ["Optional", "Mandatory"]
obj.Proxy = self
mode = 2
obj.setEditorMode("Placement", mode)
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def onChanged(self, obj, prop):
pass
def execute(self, obj):
if obj.Stop == "Optional":
word = "M1"
else:
word = "M0"
output = ""
output = word + "\n"
path = Path.Path(output)
obj.Path = path
class _ViewProviderStop:
def __init__(self, vobj): # mandatory
vobj.Proxy = self
mode = 2
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
def __getstate__(self): # mandatory
return None
def __setstate__(self, state): # mandatory
return None
def getIcon(self): # optional
return ":/icons/Path_Stop.svg"
def onChanged(self, vobj, prop): # optional
mode = 2
vobj.setEditorMode("LineWidth", mode)
vobj.setEditorMode("MarkerColor", mode)
vobj.setEditorMode("NormalColor", mode)
vobj.setEditorMode("DisplayMode", mode)
vobj.setEditorMode("BoundingBox", mode)
vobj.setEditorMode("Selectable", mode)
vobj.setEditorMode("ShapeColor", mode)
vobj.setEditorMode("Transparency", mode)
vobj.setEditorMode("Visibility", mode)
class CommandPathStop:
def GetResources(self):
return {
"Pixmap": "Path_Stop",
"MenuText": QT_TRANSLATE_NOOP("Path_Stop", "Stop"),
"ToolTip": QT_TRANSLATE_NOOP(
"Path_Stop", "Add Optional or Mandatory Stop to the program"
),
}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
for o in FreeCAD.ActiveDocument.Objects:
if o.Name[:3] == "Job":
return True
return False
def Activated(self):
FreeCAD.ActiveDocument.openTransaction(
"Add Optional or Mandatory Stop to the program"
)
FreeCADGui.addModule("PathScripts.PathStop")
snippet = """
import Path
import PathScripts
from PathScripts import PathUtils
prjexists = False
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","Stop")
PathScripts.PathStop.Stop(obj)
PathScripts.PathStop._ViewProviderStop(obj.ViewObject)
PathUtils.addToJob(obj)
"""
FreeCADGui.doCommand(snippet)
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand("Path_Stop", CommandPathStop())
FreeCAD.Console.PrintLog("Loading PathStop... done\n")

View File

@@ -1,306 +0,0 @@
import FreeCAD as App
import Part
import Path
import numpy
import math
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
def checkForBlindHole(baseshape, selectedFace):
"""
check for blind holes, returns the bottom face if found, none
if the hole is a thru-hole
"""
circularFaces = [
f
for f in baseshape.Faces
if len(f.OuterWire.Edges) == 1
and type(f.OuterWire.Edges[0].Curve) == Part.Circle
]
circularFaceEdges = [f.OuterWire.Edges[0] for f in circularFaces]
commonedges = [
i for i in selectedFace.Edges for x in circularFaceEdges if i.isSame(x)
]
bottomface = None
for f in circularFaces:
for e in f.Edges:
for i in commonedges:
if e.isSame(i):
bottomface = f
break
return bottomface
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
Path.Log.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):
Path.Log.debug("The cylindrical face is a raised feature")
return False
if not matchToolDiameter and not matchVector:
return True
if matchToolDiameter and tooldiameter / 2 > candidate.Surface.Radius:
Path.Log.debug("The tool is larger than the target")
return False
bottomface = checkForBlindHole(obj, candidate)
Path.Log.track("candidate is a blind hole")
if (
bottomface is not None and matchVector
): # blind holes only drillable at exact vector
result = compareVecs(bottomface.normalAt(0, 0), vector, exact=True)
Path.Log.track(result)
return result
elif matchVector and not (compareVecs(getSeam(candidate).Curve.Direction, vector)):
Path.Log.debug("The feature is not aligned with the given vector")
return False
else:
return True
def isDrillableFace(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
Path.Log.debug(
"\n match tool diameter {} \n match vector {}".format(
matchToolDiameter, matchVector
)
)
if not type(candidate.Surface) == Part.Plane:
Path.Log.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
Path.Log.debug("Face is circular - 1 edge")
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
Path.Log.debug("Face is a donut - 2 edges")
e1 = candidate.Edges[0]
e2 = candidate.Edges[1]
edge = e1 if e1.Curve.Radius < e2.Curve.Radius else e2
else:
Path.Log.debug(
"expected a Face with one or two circular edges got a face with {} edges".format(
len(candidate.Edges)
)
)
return False
if vector is not None: # Check for blind hole alignment
if not compareVecs(candidate.normalAt(0, 0), vector, exact=True):
Path.Log.debug("Vector not aligned")
return False
if matchToolDiameter and edge.Curve.Radius < tooldiameter / 2:
Path.Log.debug("Failed diameter check")
return False
else:
Path.Log.debug("Face is drillable")
return True
def isDrillableEdge(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1)):
"""
checks if an edge is drillable
"""
matchToolDiameter = tooldiameter is not None
matchVector = vector is not None
Path.Log.debug(
"\n match tool diameter {} \n match vector {}".format(
matchToolDiameter, matchVector
)
)
edge = candidate
if not (isinstance(edge.Curve, Part.Circle) and edge.isClosed()):
Path.Log.debug("expected a closed circular edge")
return False
if not hasattr(edge.Curve, "Radius"):
Path.Log.debug("The Feature edge has no radius - Ellipse.")
return False
if not matchToolDiameter and not matchVector:
return True
if matchToolDiameter and tooldiameter / 2 > edge.Curve.Radius:
Path.Log.debug("The tool is larger than the target")
return False
if matchVector and not (compareVecs(edge.Curve.Axis, vector)):
Path.Log.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
"""
Path.Log.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":
if isinstance(candidate.Surface, Part.Cylinder):
return isDrillableCylinder(obj, candidate, tooldiameter, vector)
else:
return isDrillableFace(obj, candidate, tooldiameter, vector)
if candidate.ShapeType == "Edge":
return isDrillableEdge(obj, candidate, tooldiameter, vector)
else:
return False
except TypeError as e:
Path.Log.debug(e)
return False
# raise TypeError("{}".format(e))
def compareVecs(vec1, vec2, exact=False):
"""
compare the two vectors to see if they are aligned for drilling.
if exact is True, vectors must match direction. Otherwise,
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)
Path.Log.debug("vector angle: {}".format(angle))
if exact:
return numpy.isclose(angle, 0, rtol=1e-05, atol=1e-06)
else:
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)
Path.Log.debug(fname)
candidate = obj.getSubObject(fname)
if not isinstance(candidate.Surface, Part.Cylinder):
continue
try:
drillable = isDrillable(
shp, candidate, tooldiameter=ToolDiameter, vector=vector
)
Path.Log.debug("fname: {} : drillable {}".format(fname, drillable))
except Exception as e:
Path.Log.debug(e)
continue
if drillable:
results.append((obj, fname))
return results