Moved the rest of the operations into Path.Op (.Gui) module
This commit is contained in:
@@ -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())
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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())
|
||||
@@ -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")
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user