Moved all Path operations with model and gui into Path.Op module
This commit is contained in:
@@ -1,496 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
|
||||
|
||||
# lazily loaded modules
|
||||
from lazy_loader.lazy_loader import LazyLoader
|
||||
|
||||
Draft = LazyLoader("Draft", globals(), "Draft")
|
||||
Part = LazyLoader("Part", globals(), "Part")
|
||||
PathGeom = LazyLoader("PathScripts.PathGeom", globals(), "PathScripts.PathGeom")
|
||||
|
||||
|
||||
__title__ = "Base class for PathArea based operations."
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Base class and properties for Path.Area based operations."
|
||||
__contributors__ = "russ4262 (Russell Johnson)"
|
||||
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class ObjectOp(PathOp.ObjectOp):
|
||||
"""Base class for all Path.Area based operations.
|
||||
Provides standard features including debugging properties AreaParams,
|
||||
PathParams and removalshape, all hidden.
|
||||
The main reason for existence is to implement the standard interface
|
||||
to Path.Area so subclasses only have to provide the shapes for the
|
||||
operations."""
|
||||
|
||||
def opFeatures(self, obj):
|
||||
"""opFeatures(obj) ... returns the base features supported by all Path.Area based operations.
|
||||
The standard feature list is OR'ed with the return value of areaOpFeatures().
|
||||
Do not overwrite, implement areaOpFeatures(obj) instead."""
|
||||
return (
|
||||
PathOp.FeatureTool
|
||||
| PathOp.FeatureDepths
|
||||
| PathOp.FeatureStepDown
|
||||
| PathOp.FeatureHeights
|
||||
| PathOp.FeatureStartPoint
|
||||
| self.areaOpFeatures(obj)
|
||||
| PathOp.FeatureCoolant
|
||||
)
|
||||
|
||||
def areaOpFeatures(self, obj):
|
||||
"""areaOpFeatures(obj) ... overwrite to add operation specific features.
|
||||
Can safely be overwritten by subclasses."""
|
||||
return 0
|
||||
|
||||
def initOperation(self, obj):
|
||||
"""initOperation(obj) ... sets up standard Path.Area properties and calls initAreaOp().
|
||||
Do not overwrite, overwrite initAreaOp(obj) instead."""
|
||||
Path.Log.track()
|
||||
|
||||
# Debugging
|
||||
obj.addProperty("App::PropertyString", "AreaParams", "Path")
|
||||
obj.setEditorMode("AreaParams", 2) # hide
|
||||
obj.addProperty("App::PropertyString", "PathParams", "Path")
|
||||
obj.setEditorMode("PathParams", 2) # hide
|
||||
obj.addProperty("Part::PropertyPartShape", "removalshape", "Path")
|
||||
obj.setEditorMode("removalshape", 2) # hide
|
||||
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"SplitArcs",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Split Arcs into discrete segments"),
|
||||
)
|
||||
|
||||
# obj.Proxy = self
|
||||
|
||||
self.initAreaOp(obj)
|
||||
|
||||
def initAreaOp(self, obj):
|
||||
"""initAreaOp(obj) ... overwrite if the receiver class needs initialisation.
|
||||
Can safely be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def areaOpShapeForDepths(self, obj, job):
|
||||
"""areaOpShapeForDepths(obj) ... returns the shape used to make an initial calculation for the depths being used.
|
||||
The default implementation returns the job's Base.Shape"""
|
||||
if job:
|
||||
if job.Stock:
|
||||
Path.Log.debug(
|
||||
"job=%s base=%s shape=%s" % (job, job.Stock, job.Stock.Shape)
|
||||
)
|
||||
return job.Stock.Shape
|
||||
else:
|
||||
Path.Log.warning(
|
||||
translate("PathAreaOp", "job %s has no Base.") % job.Label
|
||||
)
|
||||
else:
|
||||
Path.Log.warning(
|
||||
translate("PathAreaOp", "no job for op %s found.") % obj.Label
|
||||
)
|
||||
return None
|
||||
|
||||
def areaOpOnChanged(self, obj, prop):
|
||||
"""areaOpOnChanged(obj, porp) ... overwrite to process operation specific changes to properties.
|
||||
Can safely be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def opOnChanged(self, obj, prop):
|
||||
"""opOnChanged(obj, prop) ... base implementation of the notification framework - do not overwrite.
|
||||
The base implementation takes a stab at determining Heights and Depths if the operations's Base
|
||||
changes.
|
||||
Do not overwrite, overwrite areaOpOnChanged(obj, prop) instead."""
|
||||
# Path.Log.track(obj.Label, prop)
|
||||
if prop in ["AreaParams", "PathParams", "removalshape"]:
|
||||
obj.setEditorMode(prop, 2)
|
||||
|
||||
if prop == "Base" and len(obj.Base) == 1:
|
||||
(base, sub) = obj.Base[0]
|
||||
bb = base.Shape.BoundBox # parent boundbox
|
||||
subobj = base.Shape.getElement(sub[0])
|
||||
fbb = subobj.BoundBox # feature boundbox
|
||||
|
||||
if hasattr(obj, "Side"):
|
||||
if bb.XLength == fbb.XLength and bb.YLength == fbb.YLength:
|
||||
obj.Side = "Outside"
|
||||
else:
|
||||
obj.Side = "Inside"
|
||||
|
||||
self.areaOpOnChanged(obj, prop)
|
||||
|
||||
def opOnDocumentRestored(self, obj):
|
||||
Path.Log.track()
|
||||
for prop in ["AreaParams", "PathParams", "removalshape"]:
|
||||
if hasattr(obj, prop):
|
||||
obj.setEditorMode(prop, 2)
|
||||
if not hasattr(obj, "SplitArcs"):
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"SplitArcs",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Split Arcs into discrete segments"),
|
||||
)
|
||||
|
||||
self.areaOpOnDocumentRestored(obj)
|
||||
|
||||
def areaOpOnDocumentRestored(self, obj):
|
||||
"""areaOpOnDocumentRestored(obj) ... overwrite to fully restore receiver"""
|
||||
pass
|
||||
|
||||
def opSetDefaultValues(self, obj, job):
|
||||
"""opSetDefaultValues(obj) ... base implementation, do not overwrite.
|
||||
The base implementation sets the depths and heights based on the
|
||||
areaOpShapeForDepths() return value.
|
||||
Do not overwrite, overwrite areaOpSetDefaultValues(obj, job) instead."""
|
||||
Path.Log.debug("opSetDefaultValues(%s, %s)" % (obj.Label, job.Label))
|
||||
|
||||
if PathOp.FeatureDepths & self.opFeatures(obj):
|
||||
try:
|
||||
shape = self.areaOpShapeForDepths(obj, job)
|
||||
except Exception as ee:
|
||||
Path.Log.error(ee)
|
||||
shape = None
|
||||
|
||||
# Set initial start and final depths
|
||||
if shape is None:
|
||||
Path.Log.debug("shape is None")
|
||||
startDepth = 1.0
|
||||
finalDepth = 0.0
|
||||
else:
|
||||
bb = job.Stock.Shape.BoundBox
|
||||
startDepth = bb.ZMax
|
||||
finalDepth = bb.ZMin
|
||||
|
||||
# obj.StartDepth.Value = startDepth
|
||||
# obj.FinalDepth.Value = finalDepth
|
||||
obj.OpStartDepth.Value = startDepth
|
||||
obj.OpFinalDepth.Value = finalDepth
|
||||
|
||||
Path.Log.debug(
|
||||
"Default OpDepths are Start: {}, and Final: {}".format(
|
||||
obj.OpStartDepth.Value, obj.OpFinalDepth.Value
|
||||
)
|
||||
)
|
||||
Path.Log.debug(
|
||||
"Default Depths are Start: {}, and Final: {}".format(
|
||||
startDepth, finalDepth
|
||||
)
|
||||
)
|
||||
|
||||
self.areaOpSetDefaultValues(obj, job)
|
||||
|
||||
def areaOpSetDefaultValues(self, obj, job):
|
||||
"""areaOpSetDefaultValues(obj, job) ... overwrite to set initial values of operation specific properties.
|
||||
Can safely be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def _buildPathArea(self, obj, baseobject, isHole, start, getsim):
|
||||
"""_buildPathArea(obj, baseobject, isHole, start, getsim) ... internal function."""
|
||||
Path.Log.track()
|
||||
area = Path.Area()
|
||||
area.setPlane(PathUtils.makeWorkplane(baseobject))
|
||||
area.add(baseobject)
|
||||
|
||||
areaParams = self.areaOpAreaParams(obj, isHole)
|
||||
areaParams['SectionTolerance'] = 1e-07
|
||||
|
||||
heights = [i for i in self.depthparams]
|
||||
Path.Log.debug("depths: {}".format(heights))
|
||||
area.setParams(**areaParams)
|
||||
obj.AreaParams = str(area.getParams())
|
||||
|
||||
Path.Log.debug("Area with params: {}".format(area.getParams()))
|
||||
|
||||
sections = area.makeSections(
|
||||
mode=0, project=self.areaOpUseProjection(obj), heights=heights
|
||||
)
|
||||
Path.Log.debug("sections = %s" % sections)
|
||||
shapelist = [sec.getShape() for sec in sections]
|
||||
Path.Log.debug("shapelist = %s" % shapelist)
|
||||
|
||||
pathParams = self.areaOpPathParams(obj, isHole)
|
||||
pathParams["shapes"] = shapelist
|
||||
pathParams["feedrate"] = self.horizFeed
|
||||
pathParams["feedrate_v"] = self.vertFeed
|
||||
pathParams["verbose"] = True
|
||||
pathParams["resume_height"] = obj.SafeHeight.Value
|
||||
pathParams["retraction"] = obj.ClearanceHeight.Value
|
||||
pathParams["return_end"] = True
|
||||
# Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers
|
||||
pathParams["preamble"] = False
|
||||
|
||||
if not self.areaOpRetractTool(obj):
|
||||
pathParams["threshold"] = 2.001 * self.radius
|
||||
|
||||
if self.endVector is not None:
|
||||
pathParams["start"] = self.endVector
|
||||
elif PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint:
|
||||
pathParams["start"] = obj.StartPoint
|
||||
|
||||
obj.PathParams = str(
|
||||
{key: value for key, value in pathParams.items() if key != "shapes"}
|
||||
)
|
||||
Path.Log.debug("Path with params: {}".format(obj.PathParams))
|
||||
|
||||
(pp, end_vector) = Path.fromShapes(**pathParams)
|
||||
Path.Log.debug("pp: {}, end vector: {}".format(pp, end_vector))
|
||||
self.endVector = end_vector
|
||||
|
||||
simobj = None
|
||||
if getsim:
|
||||
areaParams["Thicken"] = True
|
||||
areaParams["ToolRadius"] = self.radius - self.radius * 0.005
|
||||
area.setParams(**areaParams)
|
||||
sec = area.makeSections(mode=0, project=False, heights=heights)[
|
||||
-1
|
||||
].getShape()
|
||||
simobj = sec.extrude(FreeCAD.Vector(0, 0, baseobject.BoundBox.ZMax))
|
||||
|
||||
return pp, simobj
|
||||
|
||||
def _buildProfileOpenEdges(self, obj, edgeList, isHole, start, getsim):
|
||||
"""_buildPathArea(obj, edgeList, isHole, start, getsim) ... internal function."""
|
||||
Path.Log.track()
|
||||
|
||||
paths = []
|
||||
heights = [i for i in self.depthparams]
|
||||
Path.Log.debug("depths: {}".format(heights))
|
||||
for i in range(0, len(heights)):
|
||||
for baseShape in edgeList:
|
||||
hWire = Part.Wire(Part.__sortEdges__(baseShape.Edges))
|
||||
hWire.translate(FreeCAD.Vector(0, 0, heights[i] - hWire.BoundBox.ZMin))
|
||||
|
||||
pathParams = {}
|
||||
pathParams["shapes"] = [hWire]
|
||||
pathParams["feedrate"] = self.horizFeed
|
||||
pathParams["feedrate_v"] = self.vertFeed
|
||||
pathParams["verbose"] = True
|
||||
pathParams["resume_height"] = obj.SafeHeight.Value
|
||||
pathParams["retraction"] = obj.ClearanceHeight.Value
|
||||
pathParams["return_end"] = True
|
||||
# Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers
|
||||
pathParams["preamble"] = False
|
||||
|
||||
if self.endVector is None:
|
||||
verts = hWire.Wires[0].Vertexes
|
||||
idx = 0
|
||||
if obj.Direction == "CCW":
|
||||
idx = len(verts) - 1
|
||||
x = verts[idx].X
|
||||
y = verts[idx].Y
|
||||
# Zero start value adjustments for Path.fromShapes() bug
|
||||
if PathGeom.isRoughly(x, 0.0):
|
||||
x = 0.00001
|
||||
if PathGeom.isRoughly(y, 0.0):
|
||||
y = 0.00001
|
||||
pathParams["start"] = FreeCAD.Vector(x, y, verts[0].Z)
|
||||
else:
|
||||
pathParams["start"] = self.endVector
|
||||
|
||||
obj.PathParams = str(
|
||||
{key: value for key, value in pathParams.items() if key != "shapes"}
|
||||
)
|
||||
Path.Log.debug("Path with params: {}".format(obj.PathParams))
|
||||
|
||||
(pp, end_vector) = Path.fromShapes(**pathParams)
|
||||
paths.extend(pp.Commands)
|
||||
Path.Log.debug("pp: {}, end vector: {}".format(pp, end_vector))
|
||||
|
||||
self.endVector = end_vector
|
||||
simobj = None
|
||||
|
||||
return paths, simobj
|
||||
|
||||
def opExecute(self, obj, getsim=False):
|
||||
"""opExecute(obj, getsim=False) ... implementation of Path.Area ops.
|
||||
determines the parameters for _buildPathArea().
|
||||
Do not overwrite, implement
|
||||
areaOpAreaParams(obj, isHole) ... op specific area param dictionary
|
||||
areaOpPathParams(obj, isHole) ... op specific path param dictionary
|
||||
areaOpShapes(obj) ... the shape for path area to process
|
||||
areaOpUseProjection(obj) ... return true if operation can use projection
|
||||
instead."""
|
||||
Path.Log.track()
|
||||
|
||||
# Instantiate class variables for operation reference
|
||||
self.endVector = None
|
||||
self.leadIn = 2.0
|
||||
|
||||
# Initiate depthparams and calculate operation heights for operation
|
||||
self.depthparams = self._customDepthParams(
|
||||
obj, obj.StartDepth.Value, obj.FinalDepth.Value
|
||||
)
|
||||
|
||||
# Set start point
|
||||
if PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint:
|
||||
start = obj.StartPoint
|
||||
else:
|
||||
start = None
|
||||
|
||||
aOS = self.areaOpShapes(obj)
|
||||
|
||||
# Adjust tuples length received from other PathWB tools/operations
|
||||
shapes = []
|
||||
for shp in aOS:
|
||||
if len(shp) == 2:
|
||||
(fc, iH) = shp
|
||||
# fc, iH, sub or description
|
||||
tup = fc, iH, "otherOp"
|
||||
shapes.append(tup)
|
||||
else:
|
||||
shapes.append(shp)
|
||||
|
||||
if len(shapes) > 1:
|
||||
locations = []
|
||||
for s in shapes:
|
||||
if s[2] == "OpenEdge":
|
||||
shp = Part.makeCompound(s[0])
|
||||
else:
|
||||
shp = s[0]
|
||||
locations.append(
|
||||
{"x": shp.BoundBox.XMax, "y": shp.BoundBox.YMax, "shape": s}
|
||||
)
|
||||
|
||||
locations = PathUtils.sort_locations(locations, ["x", "y"])
|
||||
|
||||
shapes = [j["shape"] for j in locations]
|
||||
|
||||
sims = []
|
||||
for shape, isHole, sub in shapes:
|
||||
profileEdgesIsOpen = False
|
||||
|
||||
if sub == "OpenEdge":
|
||||
profileEdgesIsOpen = True
|
||||
if (
|
||||
PathOp.FeatureStartPoint & self.opFeatures(obj)
|
||||
and obj.UseStartPoint
|
||||
):
|
||||
osp = obj.StartPoint
|
||||
self.commandlist.append(
|
||||
Path.Command(
|
||||
"G0", {"X": osp.x, "Y": osp.y, "F": self.horizRapid}
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
if profileEdgesIsOpen:
|
||||
(pp, sim) = self._buildProfileOpenEdges(
|
||||
obj, shape, isHole, start, getsim
|
||||
)
|
||||
else:
|
||||
(pp, sim) = self._buildPathArea(obj, shape, isHole, start, getsim)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintError(e)
|
||||
FreeCAD.Console.PrintError(
|
||||
"Something unexpected happened. Check project and tool config."
|
||||
)
|
||||
raise e
|
||||
else:
|
||||
if profileEdgesIsOpen:
|
||||
ppCmds = pp
|
||||
else:
|
||||
ppCmds = pp.Commands
|
||||
|
||||
# Save gcode commands to object command list
|
||||
self.commandlist.extend(ppCmds)
|
||||
sims.append(sim)
|
||||
# Eif
|
||||
|
||||
if (
|
||||
self.areaOpRetractTool(obj)
|
||||
and self.endVector is not None
|
||||
and len(self.commandlist) > 1
|
||||
):
|
||||
self.endVector[2] = obj.ClearanceHeight.Value
|
||||
self.commandlist.append(
|
||||
Path.Command(
|
||||
"G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}
|
||||
)
|
||||
)
|
||||
|
||||
Path.Log.debug("obj.Name: " + str(obj.Name) + "\n\n")
|
||||
return sims
|
||||
|
||||
def areaOpRetractTool(self, obj):
|
||||
"""areaOpRetractTool(obj) ... return False to keep the tool at current level between shapes. Default is True."""
|
||||
return True
|
||||
|
||||
def areaOpAreaParams(self, obj, isHole):
|
||||
"""areaOpAreaParams(obj, isHole) ... return operation specific area parameters in a dictionary.
|
||||
Note that the resulting parameters are stored in the property AreaParams.
|
||||
Must be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def areaOpPathParams(self, obj, isHole):
|
||||
"""areaOpPathParams(obj, isHole) ... return operation specific path parameters in a dictionary.
|
||||
Note that the resulting parameters are stored in the property PathParams.
|
||||
Must be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def areaOpShapes(self, obj):
|
||||
"""areaOpShapes(obj) ... return all shapes to be processed by Path.Area for this op.
|
||||
Must be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def areaOpUseProjection(self, obj):
|
||||
"""areaOpUseProcjection(obj) ... return True if the operation can use procjection, defaults to False.
|
||||
Can safely be overwritten by subclasses."""
|
||||
return False
|
||||
|
||||
# Support methods
|
||||
def _customDepthParams(self, obj, strDep, finDep):
|
||||
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
||||
cdp = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=strDep,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=finDep,
|
||||
user_depths=None,
|
||||
)
|
||||
return cdp
|
||||
|
||||
|
||||
# Eclass
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
setup = []
|
||||
return setup
|
||||
@@ -1,221 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathScripts.drillableLib as drillableLib
|
||||
|
||||
# lazily loaded modules
|
||||
from lazy_loader.lazy_loader import LazyLoader
|
||||
|
||||
Draft = LazyLoader("Draft", globals(), "Draft")
|
||||
Part = LazyLoader("Part", globals(), "Part")
|
||||
DraftGeomUtils = LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils")
|
||||
|
||||
|
||||
__title__ = "Path Circular Holes Base Operation"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Base class an implementation for operations on circular holes."
|
||||
|
||||
|
||||
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 ObjectOp(PathOp.ObjectOp):
|
||||
"""Base class for proxy objects of all operations on circular holes."""
|
||||
|
||||
def opFeatures(self, obj):
|
||||
"""opFeatures(obj) ... calls circularHoleFeatures(obj) and ORs in the standard features required for processing circular holes.
|
||||
Do not overwrite, implement circularHoleFeatures(obj) instead"""
|
||||
return (
|
||||
PathOp.FeatureTool
|
||||
| PathOp.FeatureDepths
|
||||
| PathOp.FeatureHeights
|
||||
| PathOp.FeatureBaseFaces
|
||||
| self.circularHoleFeatures(obj)
|
||||
| PathOp.FeatureCoolant
|
||||
)
|
||||
|
||||
def circularHoleFeatures(self, obj):
|
||||
"""circularHoleFeatures(obj) ... overwrite to add operations specific features.
|
||||
Can safely be overwritten by subclasses."""
|
||||
return 0
|
||||
|
||||
def initOperation(self, obj):
|
||||
"""initOperation(obj) ... adds Disabled properties and calls initCircularHoleOperation(obj).
|
||||
Do not overwrite, implement initCircularHoleOperation(obj) instead."""
|
||||
obj.addProperty(
|
||||
"App::PropertyStringList",
|
||||
"Disabled",
|
||||
"Base",
|
||||
QT_TRANSLATE_NOOP("App::Property", "List of disabled features"),
|
||||
)
|
||||
self.initCircularHoleOperation(obj)
|
||||
|
||||
def initCircularHoleOperation(self, obj):
|
||||
"""initCircularHoleOperation(obj) ... overwrite if the subclass needs initialisation.
|
||||
Can safely be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def holeDiameter(self, obj, base, sub):
|
||||
"""holeDiameter(obj, base, sub) ... returns the diameter of the specified hole."""
|
||||
try:
|
||||
shape = base.Shape.getElement(sub)
|
||||
if shape.ShapeType == "Vertex":
|
||||
return 0
|
||||
|
||||
if shape.ShapeType == "Edge" and type(shape.Curve) == Part.Circle:
|
||||
return shape.Curve.Radius * 2
|
||||
|
||||
if shape.ShapeType == "Face":
|
||||
for i in range(len(shape.Edges)):
|
||||
if (
|
||||
type(shape.Edges[i].Curve) == Part.Circle
|
||||
and shape.Edges[i].Curve.Radius * 2
|
||||
< shape.BoundBox.XLength * 1.1
|
||||
and shape.Edges[i].Curve.Radius * 2
|
||||
> shape.BoundBox.XLength * 0.9
|
||||
):
|
||||
return shape.Edges[i].Curve.Radius * 2
|
||||
|
||||
# for all other shapes the diameter is just the dimension in X.
|
||||
# This may be inaccurate as the BoundBox is calculated on the tessellated geometry
|
||||
Path.Log.warning(
|
||||
translate(
|
||||
"Path",
|
||||
"Hole diameter may be inaccurate due to tessellation on face. Consider selecting hole edge.",
|
||||
)
|
||||
)
|
||||
return shape.BoundBox.XLength
|
||||
except Part.OCCError as e:
|
||||
Path.Log.error(e)
|
||||
|
||||
return 0
|
||||
|
||||
def holePosition(self, obj, base, sub):
|
||||
"""holePosition(obj, base, sub) ... returns a Vector for the position defined by the given features.
|
||||
Note that the value for Z is set to 0."""
|
||||
|
||||
try:
|
||||
shape = base.Shape.getElement(sub)
|
||||
if shape.ShapeType == "Vertex":
|
||||
return FreeCAD.Vector(shape.X, shape.Y, 0)
|
||||
|
||||
if shape.ShapeType == "Edge" and hasattr(shape.Curve, "Center"):
|
||||
return FreeCAD.Vector(shape.Curve.Center.x, shape.Curve.Center.y, 0)
|
||||
|
||||
if shape.ShapeType == "Face":
|
||||
if hasattr(shape.Surface, "Center"):
|
||||
return FreeCAD.Vector(
|
||||
shape.Surface.Center.x, shape.Surface.Center.y, 0
|
||||
)
|
||||
if len(shape.Edges) == 1 and type(shape.Edges[0].Curve) == Part.Circle:
|
||||
return shape.Edges[0].Curve.Center
|
||||
except Part.OCCError as e:
|
||||
Path.Log.error(e)
|
||||
|
||||
Path.Log.error(
|
||||
translate(
|
||||
"Path",
|
||||
"Feature %s.%s cannot be processed as a circular hole - please remove from Base geometry list.",
|
||||
)
|
||||
% (base.Label, sub)
|
||||
)
|
||||
return None
|
||||
|
||||
def isHoleEnabled(self, obj, base, sub):
|
||||
"""isHoleEnabled(obj, base, sub) ... return true if hole is enabled."""
|
||||
name = "%s.%s" % (base.Name, sub)
|
||||
return name not in obj.Disabled
|
||||
|
||||
def opExecute(self, obj):
|
||||
"""opExecute(obj) ... processes all Base features and Locations and collects
|
||||
them in a list of positions and radii which is then passed to circularHoleExecute(obj, holes).
|
||||
If no Base geometries and no Locations are present, the job's Base is inspected and all
|
||||
drillable features are added to Base. In this case appropriate values for depths are also
|
||||
calculated and assigned.
|
||||
Do not overwrite, implement circularHoleExecute(obj, holes) instead."""
|
||||
Path.Log.track()
|
||||
|
||||
def haveLocations(self, obj):
|
||||
if PathOp.FeatureLocations & self.opFeatures(obj):
|
||||
return len(obj.Locations) != 0
|
||||
return False
|
||||
|
||||
holes = []
|
||||
for base, subs in obj.Base:
|
||||
for sub in subs:
|
||||
Path.Log.debug("processing {} in {}".format(sub, base.Name))
|
||||
if self.isHoleEnabled(obj, base, sub):
|
||||
pos = self.holePosition(obj, base, sub)
|
||||
if pos:
|
||||
holes.append(
|
||||
{
|
||||
"x": pos.x,
|
||||
"y": pos.y,
|
||||
"r": self.holeDiameter(obj, base, sub),
|
||||
}
|
||||
)
|
||||
|
||||
if haveLocations(self, obj):
|
||||
for location in obj.Locations:
|
||||
holes.append({"x": location.x, "y": location.y, "r": 0})
|
||||
|
||||
if len(holes) > 0:
|
||||
self.circularHoleExecute(obj, holes)
|
||||
|
||||
def circularHoleExecute(self, obj, holes):
|
||||
"""circularHoleExecute(obj, holes) ... implement processing of holes.
|
||||
holes is a list of dictionaries with 'x', 'y' and 'r' specified for each hole.
|
||||
Note that for Vertexes, non-circular Edges and Locations r=0.
|
||||
Must be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def findAllHoles(self, obj):
|
||||
"""findAllHoles(obj) ... find all holes of all base models and assign as features."""
|
||||
Path.Log.track()
|
||||
job = self.getJob(obj)
|
||||
if not job:
|
||||
return
|
||||
|
||||
matchvector = None if job.JobType == "Multiaxis" else FreeCAD.Vector(0, 0, 1)
|
||||
tooldiameter = obj.ToolController.Tool.Diameter
|
||||
|
||||
features = []
|
||||
for base in self.model:
|
||||
features.extend(
|
||||
drillableLib.getDrillableTargets(
|
||||
base, ToolDiameter=tooldiameter, vector=matchvector
|
||||
)
|
||||
)
|
||||
obj.Base = features
|
||||
obj.Disabled = []
|
||||
@@ -1,191 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import PathGui as PGui # ensure Path/Gui/Resources are loaded
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
__title__ = "Base for Circular Hole based operations' UI"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Implementation of circular hole specific base geometry page controller."
|
||||
|
||||
LOGLEVEL = False
|
||||
|
||||
if LOGLEVEL:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.NOTICE, Path.Log.thisModule())
|
||||
|
||||
|
||||
class TaskPanelHoleGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
|
||||
"""Controller class to be used for the BaseGeomtery page.
|
||||
Circular holes don't just display the feature, they also add a column
|
||||
displaying the radius the feature describes. This page provides that
|
||||
UI and functionality for all circular hole based operations."""
|
||||
|
||||
DataFeatureName = QtCore.Qt.ItemDataRole.UserRole
|
||||
DataObject = QtCore.Qt.ItemDataRole.UserRole + 1
|
||||
DataObjectSub = QtCore.Qt.ItemDataRole.UserRole + 2
|
||||
|
||||
InitBase = False
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... load and return page"""
|
||||
return FreeCADGui.PySideUic.loadUi(":/panels/PageBaseHoleGeometryEdit.ui")
|
||||
|
||||
def initPage(self, obj):
|
||||
self.updating = False
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... fill form with values from obj"""
|
||||
Path.Log.track()
|
||||
self.form.baseList.blockSignals(True)
|
||||
self.form.baseList.clearContents()
|
||||
self.form.baseList.setRowCount(0)
|
||||
for (base, subs) in obj.Base:
|
||||
for sub in subs:
|
||||
self.form.baseList.insertRow(self.form.baseList.rowCount())
|
||||
|
||||
item = QtGui.QTableWidgetItem("%s.%s" % (base.Label, sub))
|
||||
item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
|
||||
if obj.Proxy.isHoleEnabled(obj, base, sub):
|
||||
item.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
name = "%s.%s" % (base.Name, sub)
|
||||
item.setData(self.DataFeatureName, name)
|
||||
item.setData(self.DataObject, base)
|
||||
item.setData(self.DataObjectSub, sub)
|
||||
self.form.baseList.setItem(self.form.baseList.rowCount() - 1, 0, item)
|
||||
|
||||
dia = obj.Proxy.holeDiameter(obj, base, sub)
|
||||
item = QtGui.QTableWidgetItem("{:.3f}".format(dia))
|
||||
item.setData(self.DataFeatureName, name)
|
||||
item.setData(self.DataObject, base)
|
||||
item.setData(self.DataObjectSub, sub)
|
||||
item.setTextAlignment(QtCore.Qt.AlignHCenter)
|
||||
self.form.baseList.setItem(self.form.baseList.rowCount() - 1, 1, item)
|
||||
|
||||
self.form.baseList.resizeColumnToContents(0)
|
||||
self.form.baseList.blockSignals(False)
|
||||
self.form.baseList.setSortingEnabled(True)
|
||||
self.itemActivated()
|
||||
|
||||
def itemActivated(self):
|
||||
"""itemActivated() ... callback when item in table is selected"""
|
||||
Path.Log.track()
|
||||
if self.form.baseList.selectedItems():
|
||||
self.form.deleteBase.setEnabled(True)
|
||||
FreeCADGui.Selection.clearSelection()
|
||||
activatedRows = []
|
||||
for item in self.form.baseList.selectedItems():
|
||||
row = item.row()
|
||||
if not row in activatedRows:
|
||||
activatedRows.append(row)
|
||||
obj = item.data(self.DataObject)
|
||||
sub = str(item.data(self.DataObjectSub))
|
||||
Path.Log.debug("itemActivated() -> %s.%s" % (obj.Label, sub))
|
||||
if sub:
|
||||
FreeCADGui.Selection.addSelection(obj, sub)
|
||||
else:
|
||||
FreeCADGui.Selection.addSelection(obj)
|
||||
else:
|
||||
self.form.deleteBase.setEnabled(False)
|
||||
|
||||
def deleteBase(self):
|
||||
"""deleteBase() ... callback for push button"""
|
||||
Path.Log.track()
|
||||
selected = [
|
||||
self.form.baseList.row(item) for item in self.form.baseList.selectedItems()
|
||||
]
|
||||
self.form.baseList.blockSignals(True)
|
||||
for row in sorted(list(set(selected)), key=lambda row: -row):
|
||||
self.form.baseList.removeRow(row)
|
||||
self.updateBase()
|
||||
self.form.baseList.resizeColumnToContents(0)
|
||||
self.form.baseList.blockSignals(False)
|
||||
# self.obj.Proxy.execute(self.obj)
|
||||
FreeCAD.ActiveDocument.recompute()
|
||||
self.setFields(self.obj)
|
||||
|
||||
def updateBase(self):
|
||||
"""updateBase() ... helper function to transfer current table to obj"""
|
||||
Path.Log.track()
|
||||
newlist = []
|
||||
for i in range(self.form.baseList.rowCount()):
|
||||
item = self.form.baseList.item(i, 0)
|
||||
obj = item.data(self.DataObject)
|
||||
sub = str(item.data(self.DataObjectSub))
|
||||
base = (obj, sub)
|
||||
Path.Log.debug("keeping (%s.%s)" % (obj.Label, sub))
|
||||
newlist.append(base)
|
||||
Path.Log.debug("obj.Base=%s newlist=%s" % (self.obj.Base, newlist))
|
||||
self.updating = True
|
||||
self.obj.Base = newlist
|
||||
self.updating = False
|
||||
|
||||
def checkedChanged(self):
|
||||
"""checkeChanged() ... callback when checked status of a base feature changed"""
|
||||
Path.Log.track()
|
||||
disabled = []
|
||||
for i in range(0, self.form.baseList.rowCount()):
|
||||
item = self.form.baseList.item(i, 0)
|
||||
if item.checkState() != QtCore.Qt.Checked:
|
||||
disabled.append(item.data(self.DataFeatureName))
|
||||
self.obj.Disabled = disabled
|
||||
FreeCAD.ActiveDocument.recompute()
|
||||
|
||||
def registerSignalHandlers(self, obj):
|
||||
"""registerSignalHandlers(obj) ... setup signal handlers"""
|
||||
self.form.baseList.itemSelectionChanged.connect(self.itemActivated)
|
||||
self.form.addBase.clicked.connect(self.addBase)
|
||||
self.form.deleteBase.clicked.connect(self.deleteBase)
|
||||
self.form.resetBase.clicked.connect(self.resetBase)
|
||||
self.form.baseList.itemChanged.connect(self.checkedChanged)
|
||||
|
||||
def resetBase(self):
|
||||
"""resetBase() ... push button callback"""
|
||||
self.obj.Base = []
|
||||
self.obj.Disabled = []
|
||||
self.obj.Proxy.findAllHoles(self.obj)
|
||||
|
||||
self.obj.Proxy.execute(self.obj)
|
||||
FreeCAD.ActiveDocument.recompute()
|
||||
|
||||
def updateData(self, obj, prop):
|
||||
"""updateData(obj, prop) ... callback whenever a property of the model changed"""
|
||||
if not self.updating and prop in ["Base", "Disabled"]:
|
||||
self.setFields(obj)
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""Base class for circular hole based operation's page controller."""
|
||||
|
||||
def taskPanelBaseGeometryPage(self, obj, features):
|
||||
"""taskPanelBaseGeometryPage(obj, features) ... Return circular hole specific page controller for Base Geometry."""
|
||||
return TaskPanelHoleGeometryPage(obj, features)
|
||||
@@ -1,80 +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 Path
|
||||
|
||||
import PathScripts.PathOp as PathOp
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
__title__ = "Path Custom Operation"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Path Custom object and FreeCAD command"
|
||||
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class ObjectCustom(PathOp.ObjectOp):
|
||||
def opFeatures(self, obj):
|
||||
return PathOp.FeatureTool | PathOp.FeatureCoolant
|
||||
|
||||
def initOperation(self, obj):
|
||||
obj.addProperty(
|
||||
"App::PropertyStringList",
|
||||
"Gcode",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The gcode to be inserted"),
|
||||
)
|
||||
|
||||
obj.Proxy = self
|
||||
|
||||
def opExecute(self, obj):
|
||||
self.commandlist.append(Path.Command("(Begin Custom)"))
|
||||
if obj.Gcode:
|
||||
for l in obj.Gcode:
|
||||
newcommand = Path.Command(str(l))
|
||||
self.commandlist.append(newcommand)
|
||||
|
||||
self.commandlist.append(Path.Command("(End Custom)"))
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
setup = []
|
||||
return setup
|
||||
|
||||
|
||||
def Create(name, obj=None, parentJob=None):
|
||||
"""Create(name) ... Creates and returns a Custom operation."""
|
||||
if obj is None:
|
||||
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
||||
obj.Proxy = ObjectCustom(obj, name, parentJob)
|
||||
return obj
|
||||
@@ -1,77 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import PathScripts.PathCustom as PathCustom
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
|
||||
__title__ = "Path Custom Operation UI"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Custom operation page controller and command implementation."
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""Page controller class for the Custom operation."""
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... returns UI"""
|
||||
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpCustomEdit.ui")
|
||||
|
||||
def getFields(self, obj):
|
||||
"""getFields(obj) ... transfers values from UI to obj's properties"""
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
self.updateCoolant(obj, self.form.coolantController)
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... transfers obj's property values to UI"""
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.form.txtGCode.setText("\n".join(obj.Gcode))
|
||||
self.setupCoolant(obj, self.form.coolantController)
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
|
||||
signals = []
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
self.form.txtGCode.textChanged.connect(self.setGCode)
|
||||
return signals
|
||||
|
||||
def setGCode(self):
|
||||
self.obj.Gcode = self.form.txtGCode.toPlainText().splitlines()
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Custom",
|
||||
PathCustom.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_Custom",
|
||||
QT_TRANSLATE_NOOP("Path_Custom", "Custom"),
|
||||
QT_TRANSLATE_NOOP("Path_Custom", "Create custom gcode snippet"),
|
||||
PathCustom.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathCustomGui... done\n")
|
||||
@@ -1,470 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2018 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * Copyright (c) 2020-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 *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathEngraveBase as PathEngraveBase
|
||||
import PathScripts.PathGeom as PathGeom
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathScripts.PathOpTools as PathOpTools
|
||||
import math
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
# lazily loaded modules
|
||||
from lazy_loader.lazy_loader import LazyLoader
|
||||
|
||||
Part = LazyLoader("Part", globals(), "Part")
|
||||
|
||||
__title__ = "Path Deburr Operation"
|
||||
__author__ = "sliptonic (Brad Collette), Schildkroet"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Deburr operation."
|
||||
|
||||
|
||||
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 toolDepthAndOffset(width, extraDepth, tool, printInfo):
|
||||
"""toolDepthAndOffset(width, extraDepth, tool) ... return tuple for given\n
|
||||
parameters."""
|
||||
|
||||
if not hasattr(tool, "Diameter"):
|
||||
raise ValueError("Deburr requires tool with diameter\n")
|
||||
|
||||
suppressInfo = False
|
||||
if hasattr(tool, "CuttingEdgeAngle"):
|
||||
angle = float(tool.CuttingEdgeAngle)
|
||||
if PathGeom.isRoughly(angle, 180) or PathGeom.isRoughly(angle, 0):
|
||||
angle = 180
|
||||
toolOffset = float(tool.Diameter) / 2
|
||||
else:
|
||||
if hasattr(tool, "TipDiameter"):
|
||||
toolOffset = float(tool.TipDiameter) / 2
|
||||
elif hasattr(tool, "FlatRadius"):
|
||||
toolOffset = float(tool.FlatRadius)
|
||||
else:
|
||||
toolOffset = 0.0
|
||||
if printInfo and not suppressInfo:
|
||||
FreeCAD.Console.PrintMessage(
|
||||
translate(
|
||||
"PathDeburr",
|
||||
"The selected tool has no FlatRadius and no TipDiameter property. Assuming {}\n".format(
|
||||
"Endmill" if angle == 180 else "V-Bit"
|
||||
),
|
||||
)
|
||||
)
|
||||
suppressInfo = True
|
||||
else:
|
||||
angle = 180
|
||||
toolOffset = float(tool.Diameter) / 2
|
||||
if printInfo:
|
||||
FreeCAD.Console.PrintMessage(
|
||||
translate(
|
||||
"PathDeburr",
|
||||
"The selected tool has no CuttingEdgeAngle property. Assuming Endmill\n",
|
||||
)
|
||||
)
|
||||
suppressInfo = True
|
||||
|
||||
tan = math.tan(math.radians(angle / 2))
|
||||
|
||||
toolDepth = 0 if PathGeom.isRoughly(tan, 0) else width / tan
|
||||
depth = toolDepth + extraDepth
|
||||
extraOffset = -width if angle == 180 else (extraDepth / tan)
|
||||
offset = toolOffset + extraOffset
|
||||
|
||||
return (depth, offset, extraOffset, suppressInfo)
|
||||
|
||||
|
||||
class ObjectDeburr(PathEngraveBase.ObjectOp):
|
||||
"""Proxy class for Deburr operation."""
|
||||
|
||||
def opFeatures(self, obj):
|
||||
return (
|
||||
PathOp.FeatureTool
|
||||
| PathOp.FeatureHeights
|
||||
| PathOp.FeatureStepDown
|
||||
| PathOp.FeatureBaseEdges
|
||||
| PathOp.FeatureBaseFaces
|
||||
| PathOp.FeatureCoolant
|
||||
| PathOp.FeatureBaseGeometry
|
||||
)
|
||||
|
||||
def initOperation(self, obj):
|
||||
Path.Log.track(obj.Label)
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"Width",
|
||||
"Deburr",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The desired width of the chamfer"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"ExtraDepth",
|
||||
"Deburr",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The additional depth of the tool path"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"Join",
|
||||
"Deburr",
|
||||
QT_TRANSLATE_NOOP("App::Property", "How to join chamfer segments"),
|
||||
)
|
||||
# obj.Join = ["Round", "Miter"]
|
||||
obj.setEditorMode("Join", 2) # hide for now
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"Direction",
|
||||
"Deburr",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Direction of Operation"),
|
||||
)
|
||||
# obj.Direction = ["CW", "CCW"]
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"Side",
|
||||
"Deburr",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Side of Operation"),
|
||||
)
|
||||
obj.Side = ["Outside", "Inside"]
|
||||
obj.setEditorMode("Side", 2) # Hide property, it's calculated by op
|
||||
obj.addProperty(
|
||||
"App::PropertyInteger",
|
||||
"EntryPoint",
|
||||
"Deburr",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Select the segment, there the operations starts"
|
||||
),
|
||||
)
|
||||
|
||||
ENUMS = self.propertyEnumerations()
|
||||
for n in ENUMS:
|
||||
setattr(obj, n[0], n[1])
|
||||
|
||||
@classmethod
|
||||
def propertyEnumerations(self, dataType="data"):
|
||||
|
||||
"""opPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
|
||||
Args:
|
||||
dataType = 'data', 'raw', 'translated'
|
||||
Notes:
|
||||
'data' is list of internal string literals used in code
|
||||
'raw' is list of (translated_text, data_string) tuples
|
||||
'translated' is list of translated string literals
|
||||
"""
|
||||
|
||||
# Enumeration lists for App::PropertyEnumeration properties
|
||||
enums = {
|
||||
"Direction": [
|
||||
(translate("Path", "CW"), "CW"),
|
||||
(translate("Path", "CCW"), "CCW"),
|
||||
], # this is the direction that the profile runs
|
||||
"Join": [
|
||||
(translate("PathDeburr", "Round"), "Round"),
|
||||
(translate("PathDeburr", "Miter"), "Miter"),
|
||||
], # this is the direction that the profile runs
|
||||
}
|
||||
|
||||
if dataType == "raw":
|
||||
return enums
|
||||
|
||||
data = list()
|
||||
idx = 0 if dataType == "translated" else 1
|
||||
|
||||
Path.Log.debug(enums)
|
||||
|
||||
for k, v in enumerate(enums):
|
||||
# data[k] = [tup[idx] for tup in v]
|
||||
data.append((v, [tup[idx] for tup in enums[v]]))
|
||||
Path.Log.debug(data)
|
||||
|
||||
return data
|
||||
|
||||
def opOnDocumentRestored(self, obj):
|
||||
obj.setEditorMode("Join", 2) # hide for now
|
||||
|
||||
def opExecute(self, obj):
|
||||
Path.Log.track(obj.Label)
|
||||
if not hasattr(self, "printInfo"):
|
||||
self.printInfo = True
|
||||
try:
|
||||
(depth, offset, extraOffset, suppressInfo) = toolDepthAndOffset(
|
||||
obj.Width.Value, obj.ExtraDepth.Value, self.tool, self.printInfo
|
||||
)
|
||||
self.printInfo = not suppressInfo
|
||||
except ValueError as e:
|
||||
msg = "{} \n No path will be generated".format(e)
|
||||
raise ValueError(msg)
|
||||
# QtGui.QMessageBox.information(None, "Tool Error", msg)
|
||||
# return
|
||||
|
||||
Path.Log.track(obj.Label, depth, offset)
|
||||
|
||||
self.basewires = []
|
||||
self.adjusted_basewires = []
|
||||
wires = []
|
||||
|
||||
for base, subs in obj.Base:
|
||||
edges = []
|
||||
basewires = []
|
||||
max_h = -99999
|
||||
radius_top = 0
|
||||
radius_bottom = 0
|
||||
|
||||
for f in subs:
|
||||
sub = base.Shape.getElement(f)
|
||||
|
||||
if type(sub) == Part.Edge: # Edge
|
||||
edges.append(sub)
|
||||
|
||||
elif type(sub) == Part.Face and sub.normalAt(0, 0) != FreeCAD.Vector(
|
||||
0, 0, 1
|
||||
): # Angled face
|
||||
# If an angled face is selected, the lower edge is projected to the height of the upper edge,
|
||||
# to simulate an edge
|
||||
|
||||
# Find z value of upper edge
|
||||
for edge in sub.Edges:
|
||||
for p0 in edge.Vertexes:
|
||||
if p0.Point.z > max_h:
|
||||
max_h = p0.Point.z
|
||||
|
||||
# Find biggest radius for top/bottom
|
||||
for edge in sub.Edges:
|
||||
if Part.Circle == type(edge.Curve):
|
||||
if edge.Vertexes[0].Point.z == max_h:
|
||||
if edge.Curve.Radius > radius_top:
|
||||
radius_top = edge.Curve.Radius
|
||||
else:
|
||||
if edge.Curve.Radius > radius_bottom:
|
||||
radius_bottom = edge.Curve.Radius
|
||||
|
||||
# Search for lower edge and raise it to height of upper edge
|
||||
for edge in sub.Edges:
|
||||
if Part.Circle == type(edge.Curve): # Edge is a circle
|
||||
if edge.Vertexes[0].Point.z < max_h:
|
||||
|
||||
if edge.Closed: # Circle
|
||||
# New center
|
||||
center = FreeCAD.Vector(
|
||||
edge.Curve.Center.x, edge.Curve.Center.y, max_h
|
||||
)
|
||||
new_edge = Part.makeCircle(
|
||||
edge.Curve.Radius,
|
||||
center,
|
||||
FreeCAD.Vector(0, 0, 1),
|
||||
)
|
||||
edges.append(new_edge)
|
||||
|
||||
# Modify offset for inner angled faces
|
||||
if radius_bottom < radius_top:
|
||||
offset -= 2 * extraOffset
|
||||
|
||||
break
|
||||
|
||||
else: # Arc
|
||||
if (
|
||||
edge.Vertexes[0].Point.z
|
||||
== edge.Vertexes[1].Point.z
|
||||
):
|
||||
# Arc vertexes are on same layer
|
||||
l1 = math.sqrt(
|
||||
(
|
||||
edge.Vertexes[0].Point.x
|
||||
- edge.Curve.Center.x
|
||||
)
|
||||
** 2
|
||||
+ (
|
||||
edge.Vertexes[0].Point.y
|
||||
- edge.Curve.Center.y
|
||||
)
|
||||
** 2
|
||||
)
|
||||
l2 = math.sqrt(
|
||||
(
|
||||
edge.Vertexes[1].Point.x
|
||||
- edge.Curve.Center.x
|
||||
)
|
||||
** 2
|
||||
+ (
|
||||
edge.Vertexes[1].Point.y
|
||||
- edge.Curve.Center.y
|
||||
)
|
||||
** 2
|
||||
)
|
||||
|
||||
# New center
|
||||
center = FreeCAD.Vector(
|
||||
edge.Curve.Center.x,
|
||||
edge.Curve.Center.y,
|
||||
max_h,
|
||||
)
|
||||
|
||||
# Calculate angles based on x-axis (0 - PI/2)
|
||||
start_angle = math.acos(
|
||||
(
|
||||
edge.Vertexes[0].Point.x
|
||||
- edge.Curve.Center.x
|
||||
)
|
||||
/ l1
|
||||
)
|
||||
end_angle = math.acos(
|
||||
(
|
||||
edge.Vertexes[1].Point.x
|
||||
- edge.Curve.Center.x
|
||||
)
|
||||
/ l2
|
||||
)
|
||||
|
||||
# Angles are based on x-axis (Mirrored on x-axis) -> negative y value means negative angle
|
||||
if (
|
||||
edge.Vertexes[0].Point.y
|
||||
< edge.Curve.Center.y
|
||||
):
|
||||
start_angle *= -1
|
||||
if (
|
||||
edge.Vertexes[1].Point.y
|
||||
< edge.Curve.Center.y
|
||||
):
|
||||
end_angle *= -1
|
||||
|
||||
# Create new arc
|
||||
new_edge = Part.ArcOfCircle(
|
||||
Part.Circle(
|
||||
center,
|
||||
FreeCAD.Vector(0, 0, 1),
|
||||
edge.Curve.Radius,
|
||||
),
|
||||
start_angle,
|
||||
end_angle,
|
||||
).toShape()
|
||||
edges.append(new_edge)
|
||||
|
||||
# Modify offset for inner angled faces
|
||||
if radius_bottom < radius_top:
|
||||
offset -= 2 * extraOffset
|
||||
|
||||
break
|
||||
|
||||
else: # Line
|
||||
if (
|
||||
edge.Vertexes[0].Point.z == edge.Vertexes[1].Point.z
|
||||
and edge.Vertexes[0].Point.z < max_h
|
||||
):
|
||||
new_edge = Part.Edge(
|
||||
Part.LineSegment(
|
||||
FreeCAD.Vector(
|
||||
edge.Vertexes[0].Point.x,
|
||||
edge.Vertexes[0].Point.y,
|
||||
max_h,
|
||||
),
|
||||
FreeCAD.Vector(
|
||||
edge.Vertexes[1].Point.x,
|
||||
edge.Vertexes[1].Point.y,
|
||||
max_h,
|
||||
),
|
||||
)
|
||||
)
|
||||
edges.append(new_edge)
|
||||
|
||||
elif sub.Wires:
|
||||
basewires.extend(sub.Wires)
|
||||
|
||||
else: # Flat face
|
||||
basewires.append(Part.Wire(sub.Edges))
|
||||
|
||||
self.edges = edges
|
||||
for edgelist in Part.sortEdges(edges):
|
||||
basewires.append(Part.Wire(edgelist))
|
||||
|
||||
self.basewires.extend(basewires)
|
||||
|
||||
# Set default side
|
||||
side = ["Outside"]
|
||||
|
||||
for w in basewires:
|
||||
self.adjusted_basewires.append(w)
|
||||
wire = PathOpTools.offsetWire(w, base.Shape, offset, True, side)
|
||||
if wire:
|
||||
wires.append(wire)
|
||||
|
||||
# Set direction of op
|
||||
forward = obj.Direction == "CW"
|
||||
|
||||
# Set value of side
|
||||
obj.Side = side[0]
|
||||
# Check side extra for angled faces
|
||||
if radius_top > radius_bottom:
|
||||
obj.Side = "Inside"
|
||||
|
||||
zValues = []
|
||||
z = 0
|
||||
if obj.StepDown.Value != 0:
|
||||
while z + obj.StepDown.Value < depth:
|
||||
z = z + obj.StepDown.Value
|
||||
zValues.append(z)
|
||||
|
||||
zValues.append(depth)
|
||||
Path.Log.track(obj.Label, depth, zValues)
|
||||
|
||||
if obj.EntryPoint < 0:
|
||||
obj.EntryPoint = 0
|
||||
|
||||
self.wires = wires
|
||||
self.buildpathocc(obj, wires, zValues, True, forward, obj.EntryPoint)
|
||||
|
||||
def opRejectAddBase(self, obj, base, sub):
|
||||
"""The chamfer op can only deal with features of the base model, all others are rejected."""
|
||||
return base not in self.model
|
||||
|
||||
def opSetDefaultValues(self, obj, job):
|
||||
Path.Log.track(obj.Label, job.Label)
|
||||
obj.Width = "1 mm"
|
||||
obj.ExtraDepth = "0.5 mm"
|
||||
obj.Join = "Round"
|
||||
obj.setExpression("StepDown", "0 mm")
|
||||
obj.StepDown = "0 mm"
|
||||
obj.Direction = "CW"
|
||||
obj.Side = "Outside"
|
||||
obj.EntryPoint = 0
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
setup = []
|
||||
setup.append("Width")
|
||||
setup.append("ExtraDepth")
|
||||
return setup
|
||||
|
||||
|
||||
def Create(name, obj=None, parentJob=None):
|
||||
"""Create(name) ... Creates and returns a Deburr operation."""
|
||||
if obj is None:
|
||||
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
||||
obj.Proxy = ObjectDeburr(obj, name, parentJob)
|
||||
return obj
|
||||
@@ -1,152 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2018 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import PathScripts.PathDeburr as PathDeburr
|
||||
import PathScripts.PathGui as PathGui
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
from PySide import QtCore, QtGui
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
__title__ = "Path Deburr Operation UI"
|
||||
__author__ = "sliptonic (Brad Collette), Schildkroet"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Deburr operation page controller and command implementation."
|
||||
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
|
||||
"""Enhanced base geometry page to also allow special base objects."""
|
||||
|
||||
def super(self):
|
||||
return super(TaskPanelBaseGeometryPage, self)
|
||||
|
||||
def addBaseGeometry(self, selection):
|
||||
self.super().addBaseGeometry(selection)
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""Page controller class for the Deburr operation."""
|
||||
|
||||
_ui_form = ":/panels/PageOpDeburrEdit.ui"
|
||||
|
||||
def getForm(self):
|
||||
form = FreeCADGui.PySideUic.loadUi(self._ui_form)
|
||||
comboToPropertyMap = [("direction", "Direction")]
|
||||
enumTups = PathDeburr.ObjectDeburr.propertyEnumerations(dataType="raw")
|
||||
|
||||
self.populateCombobox(form, enumTups, comboToPropertyMap)
|
||||
|
||||
return form
|
||||
|
||||
def initPage(self, obj):
|
||||
self.opImagePath = "{}Mod/Path/Images/Ops/{}".format(
|
||||
FreeCAD.getHomePath(), "chamfer.svg"
|
||||
)
|
||||
self.opImage = QtGui.QPixmap(self.opImagePath)
|
||||
self.form.opImage.setPixmap(self.opImage)
|
||||
iconMiter = QtGui.QIcon(":/icons/edge-join-miter-not.svg")
|
||||
iconMiter.addFile(":/icons/edge-join-miter.svg", state=QtGui.QIcon.On)
|
||||
iconRound = QtGui.QIcon(":/icons/edge-join-round-not.svg")
|
||||
iconRound.addFile(":/icons/edge-join-round.svg", state=QtGui.QIcon.On)
|
||||
self.form.joinMiter.setIcon(iconMiter)
|
||||
self.form.joinRound.setIcon(iconRound)
|
||||
|
||||
def getFields(self, obj):
|
||||
PathGui.updateInputField(obj, "Width", self.form.value_W)
|
||||
PathGui.updateInputField(obj, "ExtraDepth", self.form.value_h)
|
||||
if self.form.joinRound.isChecked():
|
||||
obj.Join = "Round"
|
||||
elif self.form.joinMiter.isChecked():
|
||||
obj.Join = "Miter"
|
||||
|
||||
if obj.Direction != str(self.form.direction.currentData()):
|
||||
obj.Direction = str(self.form.direction.currentData())
|
||||
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
self.updateCoolant(obj, self.form.coolantController)
|
||||
|
||||
def setFields(self, obj):
|
||||
self.form.value_W.setText(
|
||||
FreeCAD.Units.Quantity(obj.Width.Value, FreeCAD.Units.Length).UserString
|
||||
)
|
||||
self.form.value_h.setText(
|
||||
FreeCAD.Units.Quantity(
|
||||
obj.ExtraDepth.Value, FreeCAD.Units.Length
|
||||
).UserString
|
||||
)
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.setupCoolant(obj, self.form.coolantController)
|
||||
self.form.joinRound.setChecked("Round" == obj.Join)
|
||||
self.form.joinMiter.setChecked("Miter" == obj.Join)
|
||||
self.form.joinFrame.hide()
|
||||
self.selectInComboBox(obj.Direction, self.form.direction)
|
||||
|
||||
def updateWidth(self):
|
||||
PathGui.updateInputField(self.obj, "Width", self.form.value_W)
|
||||
|
||||
def updateExtraDepth(self):
|
||||
PathGui.updateInputField(self.obj, "ExtraDepth", self.form.value_h)
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
signals = []
|
||||
signals.append(self.form.joinMiter.clicked)
|
||||
signals.append(self.form.joinRound.clicked)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
signals.append(self.form.direction.currentIndexChanged)
|
||||
signals.append(self.form.value_W.valueChanged)
|
||||
signals.append(self.form.value_h.valueChanged)
|
||||
return signals
|
||||
|
||||
def registerSignalHandlers(self, obj):
|
||||
self.form.value_W.editingFinished.connect(self.updateWidth)
|
||||
self.form.value_h.editingFinished.connect(self.updateExtraDepth)
|
||||
|
||||
def taskPanelBaseGeometryPage(self, obj, features):
|
||||
"""taskPanelBaseGeometryPage(obj, features) ... return page for adding base geometries."""
|
||||
return TaskPanelBaseGeometryPage(obj, features)
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Deburr",
|
||||
PathDeburr.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_Deburr",
|
||||
QT_TRANSLATE_NOOP("Path_Deburr", "Deburr"),
|
||||
QT_TRANSLATE_NOOP(
|
||||
"Path_Deburr", "Creates a Deburr Path along Edges or around Faces"
|
||||
),
|
||||
PathDeburr.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathDeburrGui... done\n")
|
||||
@@ -1,324 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
|
||||
# * Copyright (c) 2020 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 *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
|
||||
from Generators import drill_generator as generator
|
||||
import FreeCAD
|
||||
import Part
|
||||
import Path
|
||||
import PathFeedRate
|
||||
import PathMachineState
|
||||
import PathScripts.PathCircularHoleBase as PathCircularHoleBase
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
__title__ = "Path Drilling Operation"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Path Drilling operation."
|
||||
__contributors__ = "IMBack!"
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class ObjectDrilling(PathCircularHoleBase.ObjectOp):
|
||||
"""Proxy object for Drilling operation."""
|
||||
|
||||
@classmethod
|
||||
def propertyEnumerations(self, dataType="data"):
|
||||
"""helixOpPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
|
||||
Args:
|
||||
dataType = 'data', 'raw', 'translated'
|
||||
Notes:
|
||||
'data' is list of internal string literals used in code
|
||||
'raw' is list of (translated_text, data_string) tuples
|
||||
'translated' is list of translated string literals
|
||||
"""
|
||||
|
||||
# Enumeration lists for App::PropertyEnumeration properties
|
||||
enums = {
|
||||
"ReturnLevel": [
|
||||
(translate("Path_Drilling", "G98"), "G98"),
|
||||
(translate("Path_Drilling", "G99"), "G99"),
|
||||
], # How high to retract after a drilling move
|
||||
"ExtraOffset": [
|
||||
(translate("Path_Drilling", "None"), "None"),
|
||||
(translate("Path_Drilling", "Drill Tip"), "Drill Tip"),
|
||||
(translate("Path_Drilling", "2x Drill Tip"), "2x Drill Tip"),
|
||||
], # extra drilling depth to clear drill taper
|
||||
}
|
||||
|
||||
if dataType == "raw":
|
||||
return enums
|
||||
|
||||
data = list()
|
||||
idx = 0 if dataType == "translated" else 1
|
||||
|
||||
Path.Log.debug(enums)
|
||||
|
||||
for k, v in enumerate(enums):
|
||||
data.append((v, [tup[idx] for tup in enums[v]]))
|
||||
Path.Log.debug(data)
|
||||
|
||||
return data
|
||||
|
||||
def circularHoleFeatures(self, obj):
|
||||
"""circularHoleFeatures(obj) ... drilling works on anything, turn on all Base geometries and Locations."""
|
||||
return (
|
||||
PathOp.FeatureBaseGeometry | PathOp.FeatureLocations | PathOp.FeatureCoolant
|
||||
)
|
||||
|
||||
def onDocumentRestored(self, obj):
|
||||
if not hasattr(obj, "chipBreakEnabled"):
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"chipBreakEnabled",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Use chipbreaking"),
|
||||
)
|
||||
|
||||
def initCircularHoleOperation(self, obj):
|
||||
"""initCircularHoleOperation(obj) ... add drilling specific properties to obj."""
|
||||
obj.addProperty(
|
||||
"App::PropertyLength",
|
||||
"PeckDepth",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Incremental Drill depth before retracting to clear chips",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"PeckEnabled",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Enable pecking"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"chipBreakEnabled",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Use chipbreaking"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyFloat",
|
||||
"DwellTime",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The time to dwell between peck cycles"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"DwellEnabled",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Enable dwell"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"AddTipLength",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Calculate the tip length and subtract from final depth",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"ReturnLevel",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Controls how tool retracts Default=G98"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"RetractHeight",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"The height where feed starts and height during retract tool when path is finished while in a peck operation",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"ExtraOffset",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP("App::Property", "How far the drill depth is extended"),
|
||||
)
|
||||
|
||||
for n in self.propertyEnumerations():
|
||||
setattr(obj, n[0], n[1])
|
||||
|
||||
def circularHoleExecute(self, obj, holes):
|
||||
"""circularHoleExecute(obj, holes) ... generate drill operation for each hole in holes."""
|
||||
Path.Log.track()
|
||||
machine = PathMachineState.MachineState()
|
||||
|
||||
self.commandlist.append(Path.Command("(Begin Drilling)"))
|
||||
|
||||
# rapid to clearance height
|
||||
command = Path.Command("G0", {"Z": obj.ClearanceHeight.Value})
|
||||
machine.addCommand(command)
|
||||
self.commandlist.append(command)
|
||||
|
||||
self.commandlist.append(Path.Command("G90")) # Absolute distance mode
|
||||
|
||||
# Calculate offsets to add to target edge
|
||||
endoffset = 0.0
|
||||
if obj.ExtraOffset == "Drill Tip":
|
||||
endoffset = PathUtils.drillTipLength(self.tool)
|
||||
elif obj.ExtraOffset == "2x Drill Tip":
|
||||
endoffset = PathUtils.drillTipLength(self.tool) * 2
|
||||
|
||||
# http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g98-g99
|
||||
self.commandlist.append(Path.Command(obj.ReturnLevel))
|
||||
|
||||
holes = PathUtils.sort_locations(holes, ["x", "y"])
|
||||
|
||||
# This section is technical debt. The computation of the
|
||||
# target shapes should be factored out for re-use.
|
||||
# This will likely mean refactoring upstream CircularHoleBase to pass
|
||||
# spotshapes instead of holes.
|
||||
|
||||
startHeight = obj.StartDepth.Value + self.job.SetupSheet.SafeHeightOffset.Value
|
||||
|
||||
edgelist = []
|
||||
for hole in holes:
|
||||
v1 = FreeCAD.Vector(hole["x"], hole["y"], obj.StartDepth.Value)
|
||||
v2 = FreeCAD.Vector(hole["x"], hole["y"], obj.FinalDepth.Value - endoffset)
|
||||
edgelist.append(Part.makeLine(v1, v2))
|
||||
|
||||
# iterate the edgelist and generate gcode
|
||||
for edge in edgelist:
|
||||
|
||||
Path.Log.debug(edge)
|
||||
|
||||
# move to hole location
|
||||
|
||||
startPoint = edge.Vertexes[0].Point
|
||||
|
||||
command = Path.Command("G0", {"X": startPoint.x, "Y": startPoint.y})
|
||||
self.commandlist.append(command)
|
||||
machine.addCommand(command)
|
||||
|
||||
command = Path.Command("G0", {"Z": startHeight})
|
||||
self.commandlist.append(command)
|
||||
machine.addCommand(command)
|
||||
|
||||
# command = Path.Command("G1", {"Z": obj.StartDepth.Value})
|
||||
# self.commandlist.append(command)
|
||||
# machine.addCommand(command)
|
||||
|
||||
# Technical Debt: We are assuming the edges are aligned.
|
||||
# This assumption should be corrected and the necessary rotations
|
||||
# performed to align the edge with the Z axis for drilling
|
||||
|
||||
# Perform drilling
|
||||
dwelltime = obj.DwellTime if obj.DwellEnabled else 0.0
|
||||
peckdepth = obj.PeckDepth.Value if obj.PeckEnabled else 0.0
|
||||
repeat = 1 # technical debt: Add a repeat property for user control
|
||||
chipBreak = (obj.chipBreakEnabled and obj.PeckEnabled)
|
||||
|
||||
try:
|
||||
drillcommands = generator.generate(
|
||||
edge,
|
||||
dwelltime,
|
||||
peckdepth,
|
||||
repeat,
|
||||
obj.RetractHeight.Value,
|
||||
chipBreak=chipBreak
|
||||
)
|
||||
|
||||
except ValueError as e: # any targets that fail the generator are ignored
|
||||
Path.Log.info(e)
|
||||
continue
|
||||
|
||||
for command in drillcommands:
|
||||
self.commandlist.append(command)
|
||||
machine.addCommand(command)
|
||||
|
||||
# Cancel canned drilling cycle
|
||||
self.commandlist.append(Path.Command("G80"))
|
||||
command = Path.Command("G0", {"Z": obj.SafeHeight.Value})
|
||||
self.commandlist.append(command)
|
||||
machine.addCommand(command)
|
||||
|
||||
# Apply feedrates to commands
|
||||
PathFeedRate.setFeedRate(self.commandlist, obj.ToolController)
|
||||
|
||||
def opSetDefaultValues(self, obj, job):
|
||||
"""opSetDefaultValues(obj, job) ... set default value for RetractHeight"""
|
||||
obj.ExtraOffset = "None"
|
||||
|
||||
if hasattr(job.SetupSheet, "RetractHeight"):
|
||||
obj.RetractHeight = job.SetupSheet.RetractHeight
|
||||
elif self.applyExpression(
|
||||
obj, "RetractHeight", "StartDepth+SetupSheet.SafeHeightOffset"
|
||||
):
|
||||
if not job:
|
||||
obj.RetractHeight = 10
|
||||
else:
|
||||
obj.RetractHeight.Value = obj.StartDepth.Value + 1.0
|
||||
|
||||
if hasattr(job.SetupSheet, "PeckDepth"):
|
||||
obj.PeckDepth = job.SetupSheet.PeckDepth
|
||||
elif self.applyExpression(obj, "PeckDepth", "OpToolDiameter*0.75"):
|
||||
obj.PeckDepth = 1
|
||||
|
||||
if hasattr(job.SetupSheet, "DwellTime"):
|
||||
obj.DwellTime = job.SetupSheet.DwellTime
|
||||
else:
|
||||
obj.DwellTime = 1
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
setup = []
|
||||
setup.append("PeckDepth")
|
||||
setup.append("PeckEnabled")
|
||||
setup.append("DwellTime")
|
||||
setup.append("DwellEnabled")
|
||||
setup.append("AddTipLength")
|
||||
setup.append("ReturnLevel")
|
||||
setup.append("ExtraOffset")
|
||||
setup.append("RetractHeight")
|
||||
return setup
|
||||
|
||||
|
||||
def Create(name, obj=None, parentJob=None):
|
||||
"""Create(name) ... Creates and returns a Drilling operation."""
|
||||
if obj is None:
|
||||
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
||||
|
||||
obj.Proxy = ObjectDrilling(obj, name, parentJob)
|
||||
if obj.Proxy:
|
||||
obj.Proxy.findAllHoles(obj)
|
||||
|
||||
return obj
|
||||
@@ -1,185 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import PathGui as PGui # ensure Path/Gui/Resources are loaded
|
||||
import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui
|
||||
import PathScripts.PathDrilling as PathDrilling
|
||||
import PathScripts.PathGui as PathGui
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
|
||||
from PySide import QtCore
|
||||
|
||||
__title__ = "Path Drilling Operation UI."
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "UI and Command for Path Drilling Operation."
|
||||
__contributors__ = "IMBack!"
|
||||
|
||||
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 TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
|
||||
"""Controller for the drilling operation's page"""
|
||||
|
||||
def initPage(self, obj):
|
||||
self.peckDepthSpinBox = PathGui.QuantitySpinBox(
|
||||
self.form.peckDepth, obj, "PeckDepth"
|
||||
)
|
||||
self.peckRetractSpinBox = PathGui.QuantitySpinBox(
|
||||
self.form.peckRetractHeight, obj, "RetractHeight"
|
||||
)
|
||||
self.dwellTimeSpinBox = PathGui.QuantitySpinBox(
|
||||
self.form.dwellTime, obj, "DwellTime"
|
||||
)
|
||||
self.form.chipBreakEnabled.setEnabled(False)
|
||||
|
||||
def registerSignalHandlers(self, obj):
|
||||
self.form.peckEnabled.toggled.connect(self.form.peckDepth.setEnabled)
|
||||
self.form.peckEnabled.toggled.connect(self.form.dwellEnabled.setDisabled)
|
||||
self.form.peckEnabled.toggled.connect(self.setChipBreakControl)
|
||||
|
||||
self.form.dwellEnabled.toggled.connect(self.form.dwellTime.setEnabled)
|
||||
self.form.dwellEnabled.toggled.connect(self.form.dwellTimelabel.setEnabled)
|
||||
self.form.dwellEnabled.toggled.connect(self.form.peckEnabled.setDisabled)
|
||||
self.form.dwellEnabled.toggled.connect(self.setChipBreakControl)
|
||||
|
||||
self.form.peckRetractHeight.setEnabled(True)
|
||||
self.form.retractLabel.setEnabled(True)
|
||||
|
||||
if self.form.peckEnabled.isChecked():
|
||||
self.form.dwellEnabled.setEnabled(False)
|
||||
self.form.peckDepth.setEnabled(True)
|
||||
self.form.peckDepthLabel.setEnabled(True)
|
||||
self.form.chipBreakEnabled.setEnabled(True)
|
||||
elif self.form.dwellEnabled.isChecked():
|
||||
self.form.peckEnabled.setEnabled(False)
|
||||
self.form.dwellTime.setEnabled(True)
|
||||
self.form.dwellTimelabel.setEnabled(True)
|
||||
self.form.chipBreakEnabled.setEnabled(False)
|
||||
else:
|
||||
self.form.chipBreakEnabled.setEnabled(False)
|
||||
|
||||
def setChipBreakControl(self):
|
||||
self.form.chipBreakEnabled.setEnabled(self.form.peckEnabled.isChecked())
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... return UI"""
|
||||
form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpDrillingEdit.ui")
|
||||
|
||||
comboToPropertyMap = [("ExtraOffset", "ExtraOffset")]
|
||||
enumTups = PathDrilling.ObjectDrilling.propertyEnumerations(dataType="raw")
|
||||
self.populateCombobox(form, enumTups, comboToPropertyMap)
|
||||
|
||||
return form
|
||||
|
||||
def updateQuantitySpinBoxes(self, index=None):
|
||||
self.peckDepthSpinBox.updateSpinBox()
|
||||
self.peckRetractSpinBox.updateSpinBox()
|
||||
self.dwellTimeSpinBox.updateSpinBox()
|
||||
|
||||
def getFields(self, obj):
|
||||
"""setFields(obj) ... update obj's properties with values from the UI"""
|
||||
Path.Log.track()
|
||||
self.peckDepthSpinBox.updateProperty()
|
||||
self.peckRetractSpinBox.updateProperty()
|
||||
self.dwellTimeSpinBox.updateProperty()
|
||||
|
||||
if obj.DwellEnabled != self.form.dwellEnabled.isChecked():
|
||||
obj.DwellEnabled = self.form.dwellEnabled.isChecked()
|
||||
if obj.PeckEnabled != self.form.peckEnabled.isChecked():
|
||||
obj.PeckEnabled = self.form.peckEnabled.isChecked()
|
||||
if obj.chipBreakEnabled != self.form.chipBreakEnabled.isChecked():
|
||||
obj.chipBreakEnabled = self.form.chipBreakEnabled.isChecked()
|
||||
if obj.ExtraOffset != str(self.form.ExtraOffset.currentData()):
|
||||
obj.ExtraOffset = str(self.form.ExtraOffset.currentData())
|
||||
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
self.updateCoolant(obj, self.form.coolantController)
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... update UI with obj properties' values"""
|
||||
Path.Log.track()
|
||||
self.updateQuantitySpinBoxes()
|
||||
|
||||
if obj.DwellEnabled:
|
||||
self.form.dwellEnabled.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
self.form.dwellEnabled.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
if obj.PeckEnabled:
|
||||
self.form.peckEnabled.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
self.form.peckEnabled.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.form.chipBreakEnabled.setEnabled(False)
|
||||
|
||||
if obj.chipBreakEnabled:
|
||||
self.form.chipBreakEnabled.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
self.form.chipBreakEnabled.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
self.selectInComboBox(obj.ExtraOffset, self.form.ExtraOffset)
|
||||
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.setupCoolant(obj, self.form.coolantController)
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals which cause the receiver to update the model"""
|
||||
signals = []
|
||||
|
||||
signals.append(self.form.peckRetractHeight.editingFinished)
|
||||
signals.append(self.form.peckDepth.editingFinished)
|
||||
signals.append(self.form.dwellTime.editingFinished)
|
||||
signals.append(self.form.dwellEnabled.stateChanged)
|
||||
signals.append(self.form.peckEnabled.stateChanged)
|
||||
signals.append(self.form.chipBreakEnabled.stateChanged)
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
signals.append(self.form.ExtraOffset.currentIndexChanged)
|
||||
|
||||
return signals
|
||||
|
||||
def updateData(self, obj, prop):
|
||||
if prop in ["PeckDepth", "RetractHeight"] and not prop in ["Base", "Disabled"]:
|
||||
self.updateQuantitySpinBoxes()
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Drilling",
|
||||
PathDrilling.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_Drilling",
|
||||
QtCore.QT_TRANSLATE_NOOP("Path_Drilling", "Drilling"),
|
||||
QtCore.QT_TRANSLATE_NOOP(
|
||||
"Path_Drilling",
|
||||
"Creates a Path Drilling object from a features of a base object",
|
||||
),
|
||||
PathDrilling.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathDrillingGui... done\n")
|
||||
@@ -1,177 +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 Path
|
||||
import PathScripts.PathEngraveBase as PathEngraveBase
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
__doc__ = "Class and implementation of Path Engrave operation"
|
||||
|
||||
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())
|
||||
|
||||
# lazily loaded modules
|
||||
from lazy_loader.lazy_loader import LazyLoader
|
||||
|
||||
Part = LazyLoader("Part", globals(), "Part")
|
||||
|
||||
|
||||
class ObjectEngrave(PathEngraveBase.ObjectOp):
|
||||
"""Proxy class for Engrave operation."""
|
||||
|
||||
def __init__(self, obj, name, parentJob):
|
||||
super(ObjectEngrave, self).__init__(obj, name, parentJob)
|
||||
self.wires = []
|
||||
|
||||
def opFeatures(self, obj):
|
||||
"""opFeatures(obj) ... return all standard features and edges based geometries"""
|
||||
return (
|
||||
PathOp.FeatureTool
|
||||
| PathOp.FeatureDepths
|
||||
| PathOp.FeatureHeights
|
||||
| PathOp.FeatureStepDown
|
||||
| PathOp.FeatureBaseEdges
|
||||
| PathOp.FeatureCoolant
|
||||
)
|
||||
|
||||
def setupAdditionalProperties(self, obj):
|
||||
if not hasattr(obj, "BaseShapes"):
|
||||
obj.addProperty(
|
||||
"App::PropertyLinkList",
|
||||
"BaseShapes",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Additional base objects to be engraved"
|
||||
),
|
||||
)
|
||||
obj.setEditorMode("BaseShapes", 2) # hide
|
||||
if not hasattr(obj, "BaseObject"):
|
||||
obj.addProperty(
|
||||
"App::PropertyLink",
|
||||
"BaseObject",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Additional base objects to be engraved"
|
||||
),
|
||||
)
|
||||
obj.setEditorMode("BaseObject", 2) # hide
|
||||
|
||||
def initOperation(self, obj):
|
||||
"""initOperation(obj) ... create engraving specific properties."""
|
||||
obj.addProperty(
|
||||
"App::PropertyInteger",
|
||||
"StartVertex",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "The vertex index to start the path from"
|
||||
),
|
||||
)
|
||||
self.setupAdditionalProperties(obj)
|
||||
|
||||
def opOnDocumentRestored(self, obj):
|
||||
# upgrade ...
|
||||
self.setupAdditionalProperties(obj)
|
||||
|
||||
def opExecute(self, obj):
|
||||
"""opExecute(obj) ... process engraving operation"""
|
||||
Path.Log.track()
|
||||
|
||||
jobshapes = []
|
||||
|
||||
if len(obj.Base) >= 1: # user has selected specific subelements
|
||||
Path.Log.track(len(obj.Base))
|
||||
wires = []
|
||||
for base, subs in obj.Base:
|
||||
edges = []
|
||||
basewires = []
|
||||
for feature in subs:
|
||||
sub = base.Shape.getElement(feature)
|
||||
if type(sub) == Part.Edge:
|
||||
edges.append(sub)
|
||||
elif sub.Wires:
|
||||
basewires.extend(sub.Wires)
|
||||
else:
|
||||
basewires.append(Part.Wire(sub.Edges))
|
||||
|
||||
for edgelist in Part.sortEdges(edges):
|
||||
basewires.append(Part.Wire(edgelist))
|
||||
|
||||
wires.extend(basewires)
|
||||
jobshapes.append(Part.makeCompound(wires))
|
||||
|
||||
elif len(obj.BaseShapes) > 0: # user added specific shapes
|
||||
jobshapes.extend([base.Shape for base in obj.BaseShapes])
|
||||
else:
|
||||
Path.Log.track(self.model)
|
||||
for base in self.model:
|
||||
Path.Log.track(base.Label)
|
||||
if base.isDerivedFrom("Part::Part2DObject"):
|
||||
jobshapes.append(base.Shape)
|
||||
elif base.isDerivedFrom("Sketcher::SketchObject"):
|
||||
jobshapes.append(base.Shape)
|
||||
elif hasattr(base, "ArrayType"):
|
||||
jobshapes.append(base.Shape)
|
||||
|
||||
if len(jobshapes) > 0:
|
||||
Path.Log.debug("processing {} jobshapes".format(len(jobshapes)))
|
||||
wires = []
|
||||
for shape in jobshapes:
|
||||
shapeWires = shape.Wires
|
||||
Path.Log.debug("jobshape has {} edges".format(len(shape.Edges)))
|
||||
self.commandlist.append(
|
||||
Path.Command(
|
||||
"G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}
|
||||
)
|
||||
)
|
||||
self.buildpathocc(obj, shapeWires, self.getZValues(obj))
|
||||
wires.extend(shapeWires)
|
||||
self.wires = wires
|
||||
Path.Log.debug(
|
||||
"processing {} jobshapes -> {} wires".format(len(jobshapes), len(wires))
|
||||
)
|
||||
# the last command is a move to clearance, which is automatically added by PathOp
|
||||
if self.commandlist:
|
||||
self.commandlist.pop()
|
||||
|
||||
def opUpdateDepths(self, obj):
|
||||
"""updateDepths(obj) ... engraving is always done at the top most z-value"""
|
||||
job = PathUtils.findParentJob(obj)
|
||||
self.opSetDefaultValues(obj, job)
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
return ["StartVertex"]
|
||||
|
||||
|
||||
def Create(name, obj=None, parentJob=None):
|
||||
"""Create(name) ... Creates and returns an Engrave operation."""
|
||||
if obj is None:
|
||||
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
||||
obj.Proxy = ObjectEngrave(obj, name, parentJob)
|
||||
return obj
|
||||
@@ -1,171 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2018 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 lazy_loader.lazy_loader import LazyLoader
|
||||
import Path
|
||||
import PathScripts.PathGeom as PathGeom
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathScripts.PathOpTools as PathOpTools
|
||||
import copy
|
||||
|
||||
__doc__ = "Base class for all ops in the engrave family."
|
||||
|
||||
# lazily loaded modules
|
||||
DraftGeomUtils = LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils")
|
||||
Part = LazyLoader("Part", globals(), "Part")
|
||||
|
||||
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 ObjectOp(PathOp.ObjectOp):
|
||||
"""Proxy base class for engrave operations."""
|
||||
|
||||
def getZValues(self, obj):
|
||||
zValues = []
|
||||
if obj.StepDown.Value != 0:
|
||||
z = obj.StartDepth.Value - obj.StepDown.Value
|
||||
stepdown = obj.StepDown.Value
|
||||
if stepdown < 0:
|
||||
stepdown = -stepdown
|
||||
|
||||
while z > obj.FinalDepth.Value:
|
||||
zValues.append(z)
|
||||
z -= stepdown
|
||||
|
||||
zValues.append(obj.FinalDepth.Value)
|
||||
return zValues
|
||||
|
||||
def buildpathocc(self, obj, wires, zValues, relZ=False, forward=True, start_idx=0):
|
||||
"""buildpathocc(obj, wires, zValues, relZ=False) ... internal helper function to generate engraving commands."""
|
||||
Path.Log.track(obj.Label, len(wires), zValues)
|
||||
|
||||
decomposewires = []
|
||||
for wire in wires:
|
||||
decomposewires.extend(PathOpTools.makeWires(wire.Edges))
|
||||
|
||||
wires = decomposewires
|
||||
for wire in wires:
|
||||
# offset = wire
|
||||
|
||||
# reorder the wire
|
||||
if hasattr(obj, "StartVertex"):
|
||||
start_idx = obj.StartVertex
|
||||
edges = wire.Edges
|
||||
|
||||
# edges = copy.copy(PathOpTools.orientWire(offset, forward).Edges)
|
||||
# Path.Log.track("wire: {} offset: {}".format(len(wire.Edges), len(edges)))
|
||||
# edges = Part.sortEdges(edges)[0]
|
||||
# Path.Log.track("edges: {}".format(len(edges)))
|
||||
|
||||
last = None
|
||||
|
||||
for z in zValues:
|
||||
Path.Log.debug(z)
|
||||
if last:
|
||||
self.appendCommand(
|
||||
Path.Command("G1", {"X": last.x, "Y": last.y, "Z": last.z}),
|
||||
z,
|
||||
relZ,
|
||||
self.vertFeed,
|
||||
)
|
||||
|
||||
first = True
|
||||
if start_idx > len(edges) - 1:
|
||||
start_idx = len(edges) - 1
|
||||
|
||||
edges = edges[start_idx:] + edges[:start_idx]
|
||||
for edge in edges:
|
||||
Path.Log.debug(
|
||||
"points: {} -> {}".format(
|
||||
edge.Vertexes[0].Point, edge.Vertexes[-1].Point
|
||||
)
|
||||
)
|
||||
Path.Log.debug(
|
||||
"valueat {} -> {}".format(
|
||||
edge.valueAt(edge.FirstParameter),
|
||||
edge.valueAt(edge.LastParameter),
|
||||
)
|
||||
)
|
||||
if first and (not last or not wire.isClosed()):
|
||||
Path.Log.debug("processing first edge entry")
|
||||
# we set the first move to our first point
|
||||
last = edge.Vertexes[0].Point
|
||||
|
||||
self.commandlist.append(
|
||||
Path.Command(
|
||||
"G0",
|
||||
{"Z": obj.ClearanceHeight.Value, "F": self.vertRapid},
|
||||
)
|
||||
)
|
||||
self.commandlist.append(
|
||||
Path.Command(
|
||||
"G0", {"X": last.x, "Y": last.y, "F": self.horizRapid}
|
||||
)
|
||||
)
|
||||
self.commandlist.append(
|
||||
Path.Command(
|
||||
"G0", {"Z": obj.SafeHeight.Value, "F": self.vertRapid}
|
||||
)
|
||||
)
|
||||
self.appendCommand(
|
||||
Path.Command("G1", {"X": last.x, "Y": last.y, "Z": last.z}),
|
||||
z,
|
||||
relZ,
|
||||
self.vertFeed,
|
||||
)
|
||||
first = False
|
||||
|
||||
if PathGeom.pointsCoincide(last, edge.valueAt(edge.FirstParameter)):
|
||||
# if PathGeom.pointsCoincide(last, edge.Vertexes[0].Point):
|
||||
for cmd in PathGeom.cmdsForEdge(edge):
|
||||
self.appendCommand(cmd, z, relZ, self.horizFeed)
|
||||
last = edge.Vertexes[-1].Point
|
||||
else:
|
||||
for cmd in PathGeom.cmdsForEdge(edge, True):
|
||||
self.appendCommand(cmd, z, relZ, self.horizFeed)
|
||||
last = edge.Vertexes[0].Point
|
||||
self.commandlist.append(
|
||||
Path.Command(
|
||||
"G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}
|
||||
)
|
||||
)
|
||||
|
||||
def appendCommand(self, cmd, z, relZ, feed):
|
||||
params = cmd.Parameters
|
||||
if relZ:
|
||||
z = params["Z"] - z
|
||||
params.update({"Z": z, "F": feed})
|
||||
self.commandlist.append(Path.Command(cmd.Name, params))
|
||||
|
||||
def opSetDefaultValues(self, obj, job):
|
||||
"""opSetDefaultValues(obj) ... set depths for engraving"""
|
||||
if PathOp.FeatureDepths & self.opFeatures(obj):
|
||||
if job and len(job.Model.Group) > 0:
|
||||
bb = job.Proxy.modelBoundBox(job)
|
||||
obj.OpStartDepth = bb.ZMax
|
||||
obj.OpFinalDepth = bb.ZMax - max(obj.StepDown.Value, 0.1)
|
||||
else:
|
||||
obj.OpFinalDepth = -0.1
|
||||
@@ -1,179 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import PathGui as PGui # ensure Path/Gui/Resources are loaded
|
||||
import PathScripts.PathEngrave as PathEngrave
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
|
||||
__title__ = "Path Engrave Operation UI"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Engrave operation page controller and command implementation."
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
|
||||
"""Enhanced base geometry page to also allow special base objects."""
|
||||
|
||||
def super(self):
|
||||
return super(TaskPanelBaseGeometryPage, self)
|
||||
|
||||
def selectionSupportedAsBaseGeometry(self, selection, ignoreErrors):
|
||||
# allow selection of an entire 2D object, which is generally not the case
|
||||
if (
|
||||
len(selection) == 1
|
||||
and not selection[0].HasSubObjects
|
||||
and selection[0].Object.isDerivedFrom("Part::Part2DObject")
|
||||
):
|
||||
return True
|
||||
# Let general logic handle all other cases.
|
||||
return self.super().selectionSupportedAsBaseGeometry(selection, ignoreErrors)
|
||||
|
||||
def addBaseGeometry(self, selection):
|
||||
added = False
|
||||
shapes = self.obj.BaseShapes
|
||||
for sel in selection:
|
||||
job = PathUtils.findParentJob(self.obj)
|
||||
base = job.Proxy.resourceClone(job, sel.Object)
|
||||
if not base:
|
||||
Path.Log.notice(
|
||||
(
|
||||
translate("Path", "%s is not a Base Model object of the job %s")
|
||||
+ "\n"
|
||||
)
|
||||
% (sel.Object.Label, job.Label)
|
||||
)
|
||||
continue
|
||||
if base in shapes:
|
||||
Path.Log.notice(
|
||||
(translate("Path", "Base shape %s already in the list") + "\n")
|
||||
% (sel.Object.Label)
|
||||
)
|
||||
continue
|
||||
if base.isDerivedFrom("Part::Part2DObject"):
|
||||
if sel.HasSubObjects:
|
||||
# selectively add some elements of the drawing to the Base
|
||||
for sub in sel.SubElementNames:
|
||||
if "Vertex" in sub:
|
||||
Path.Log.info("Ignoring vertex")
|
||||
else:
|
||||
self.obj.Proxy.addBase(self.obj, base, sub)
|
||||
else:
|
||||
# when adding an entire shape to BaseShapes we can take its sub shapes out of Base
|
||||
self.obj.Base = [(p, el) for p, el in self.obj.Base if p != base]
|
||||
shapes.append(base)
|
||||
self.obj.BaseShapes = shapes
|
||||
added = True
|
||||
else:
|
||||
# user wants us to engrave an edge of face of a base model
|
||||
base = self.super().addBaseGeometry(selection)
|
||||
added = added or base
|
||||
|
||||
return added
|
||||
|
||||
def setFields(self, obj):
|
||||
self.super().setFields(obj)
|
||||
self.form.baseList.blockSignals(True)
|
||||
for shape in self.obj.BaseShapes:
|
||||
item = QtGui.QListWidgetItem(shape.Label)
|
||||
item.setData(self.super().DataObject, shape)
|
||||
item.setData(self.super().DataObjectSub, None)
|
||||
self.form.baseList.addItem(item)
|
||||
self.form.baseList.blockSignals(False)
|
||||
|
||||
def updateBase(self):
|
||||
Path.Log.track()
|
||||
shapes = []
|
||||
for i in range(self.form.baseList.count()):
|
||||
item = self.form.baseList.item(i)
|
||||
obj = item.data(self.super().DataObject)
|
||||
sub = item.data(self.super().DataObjectSub)
|
||||
if not sub:
|
||||
shapes.append(obj)
|
||||
Path.Log.debug(
|
||||
"Setting new base shapes: %s -> %s" % (self.obj.BaseShapes, shapes)
|
||||
)
|
||||
self.obj.BaseShapes = shapes
|
||||
return self.super().updateBase()
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""Page controller class for the Engrave operation."""
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... returns UI"""
|
||||
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpEngraveEdit.ui")
|
||||
|
||||
def getFields(self, obj):
|
||||
"""getFields(obj) ... transfers values from UI to obj's proprties"""
|
||||
if obj.StartVertex != self.form.startVertex.value():
|
||||
obj.StartVertex = self.form.startVertex.value()
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
self.updateCoolant(obj, self.form.coolantController)
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... transfers obj's property values to UI"""
|
||||
self.form.startVertex.setValue(obj.StartVertex)
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.setupCoolant(obj, self.form.coolantController)
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
|
||||
signals = []
|
||||
signals.append(self.form.startVertex.editingFinished)
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
return signals
|
||||
|
||||
def taskPanelBaseGeometryPage(self, obj, features):
|
||||
"""taskPanelBaseGeometryPage(obj, features) ... return page for adding base geometries."""
|
||||
return TaskPanelBaseGeometryPage(obj, features)
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Engrave",
|
||||
PathEngrave.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_Engrave",
|
||||
QtCore.QT_TRANSLATE_NOOP("Path_Engrave", "Engrave"),
|
||||
QtCore.QT_TRANSLATE_NOOP(
|
||||
"Path_Engrave", "Creates an Engraving Path around a Draft ShapeString"
|
||||
),
|
||||
PathEngrave.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathEngraveGui... done\n")
|
||||
@@ -25,10 +25,10 @@ from pivy import coin
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import Path.Op.Gui.Base as PathOpGui
|
||||
import PathScripts.PathFeatureExtensions as FeatureExtensions
|
||||
import PathScripts.PathGeom as PathGeom
|
||||
import PathScripts.PathGui as PathGui
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
|
||||
# lazily loaded modules
|
||||
from lazy_loader.lazy_loader import LazyLoader
|
||||
|
||||
@@ -47,36 +47,36 @@ def Startup():
|
||||
from Path.Dressup.Gui import RampEntry
|
||||
from Path.Dressup.Gui import Tags
|
||||
from Path.Dressup.Gui import ZCorrect
|
||||
from Path.Op.Gui import Custom
|
||||
from Path.Op.Gui import Deburr
|
||||
from Path.Op.Gui import Drilling
|
||||
from Path.Op.Gui import Engrave
|
||||
from Path.Op.Gui import Helix
|
||||
from Path.Op.Gui import MillFace
|
||||
from Path.Op.Gui import Pocket
|
||||
from Path.Op.Gui import PocketShape
|
||||
from Path.Op.Gui import Probe
|
||||
from Path.Op.Gui import Profile
|
||||
from Path.Op.Gui import Slot
|
||||
from Path.Op.Gui import ThreadMilling
|
||||
from Path.Op.Gui import Vcarve
|
||||
from Path.Post import Command
|
||||
from Path.Tools import Controller
|
||||
from Path.Tools.Gui import Controller
|
||||
from PathScripts import PathArray
|
||||
from PathScripts import PathComment
|
||||
from PathScripts import PathCustomGui
|
||||
from PathScripts import PathDeburrGui
|
||||
from PathScripts import PathDrillingGui
|
||||
from PathScripts import PathEngraveGui
|
||||
from PathScripts import PathFixture
|
||||
from PathScripts import PathHelixGui
|
||||
from PathScripts import PathHop
|
||||
from PathScripts import PathInspect
|
||||
from PathScripts import PathMillFaceGui
|
||||
from PathScripts import PathPocketGui
|
||||
from PathScripts import PathPocketShapeGui
|
||||
from PathScripts import PathProbeGui
|
||||
from PathScripts import PathProfileGui
|
||||
from PathScripts import PathPropertyBagGui
|
||||
from PathScripts import PathSanity
|
||||
from PathScripts import PathSetupSheetGui
|
||||
from PathScripts import PathSimpleCopy
|
||||
from PathScripts import PathSimulatorGui
|
||||
from PathScripts import PathSlotGui
|
||||
from PathScripts import PathStop
|
||||
from PathScripts import PathThreadMillingGui
|
||||
from PathScripts import PathToolLibraryEditor
|
||||
from PathScripts import PathToolLibraryManager
|
||||
from PathScripts import PathUtilsGui
|
||||
from PathScripts import PathVcarveGui
|
||||
|
||||
from packaging.version import Version, parse
|
||||
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2016 Lorenz Hüdepohl <dev@stellardeath.org> *
|
||||
# * *
|
||||
# * 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 Generators import helix_generator
|
||||
from PathScripts.PathUtils import fmt
|
||||
from PathScripts.PathUtils import sort_locations
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
import FreeCAD
|
||||
import Part
|
||||
import Path
|
||||
import PathScripts.PathCircularHoleBase as PathCircularHoleBase
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathFeedRate
|
||||
|
||||
|
||||
__title__ = "Path Helix Drill Operation"
|
||||
__author__ = "Lorenz Hüdepohl"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Class and implementation of Helix Drill operation"
|
||||
__contributors__ = "russ4262 (Russell Johnson)"
|
||||
__created__ = "2016"
|
||||
__scriptVersion__ = "1b testing"
|
||||
__lastModified__ = "2019-07-12 09:50 CST"
|
||||
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class ObjectHelix(PathCircularHoleBase.ObjectOp):
|
||||
"""Proxy class for Helix operations."""
|
||||
|
||||
@classmethod
|
||||
def helixOpPropertyEnumerations(self, dataType="data"):
|
||||
"""helixOpPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
|
||||
Args:
|
||||
dataType = 'data', 'raw', 'translated'
|
||||
Notes:
|
||||
'data' is list of internal string literals used in code
|
||||
'raw' is list of (translated_text, data_string) tuples
|
||||
'translated' is list of translated string literals
|
||||
"""
|
||||
|
||||
# Enumeration lists for App::PropertyEnumeration properties
|
||||
enums = {
|
||||
"Direction": [
|
||||
(translate("Path_Helix", "CW"), "CW"),
|
||||
(translate("Path_Helix", "CCW"), "CCW"),
|
||||
], # this is the direction that the profile runs
|
||||
"StartSide": [
|
||||
(translate("PathProfile", "Outside"), "Outside"),
|
||||
(translate("PathProfile", "Inside"), "Inside"),
|
||||
], # side of profile that cutter is on in relation to direction of profile
|
||||
}
|
||||
|
||||
if dataType == "raw":
|
||||
return enums
|
||||
|
||||
data = list()
|
||||
idx = 0 if dataType == "translated" else 1
|
||||
|
||||
Path.Log.debug(enums)
|
||||
|
||||
for k, v in enumerate(enums):
|
||||
data.append((v, [tup[idx] for tup in enums[v]]))
|
||||
Path.Log.debug(data)
|
||||
|
||||
return data
|
||||
|
||||
def circularHoleFeatures(self, obj):
|
||||
"""circularHoleFeatures(obj) ... enable features supported by Helix."""
|
||||
return (
|
||||
PathOp.FeatureStepDown | PathOp.FeatureBaseEdges | PathOp.FeatureBaseFaces
|
||||
)
|
||||
|
||||
def initCircularHoleOperation(self, obj):
|
||||
"""initCircularHoleOperation(obj) ... create helix specific properties."""
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"Direction",
|
||||
"Helix Drill",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"The direction of the circular cuts, ClockWise (CW), or CounterClockWise (CCW)",
|
||||
),
|
||||
)
|
||||
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"StartSide",
|
||||
"Helix Drill",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Start cutting from the inside or outside"
|
||||
),
|
||||
)
|
||||
|
||||
obj.addProperty(
|
||||
"App::PropertyPercent",
|
||||
"StepOver",
|
||||
"Helix Drill",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Percent of cutter diameter to step over on each pass"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyLength",
|
||||
"StartRadius",
|
||||
"Helix Drill",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Starting Radius"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"OffsetExtra",
|
||||
"Helix Drill",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Extra value to stay away from final profile- good for roughing toolpath",
|
||||
),
|
||||
)
|
||||
|
||||
ENUMS = self.helixOpPropertyEnumerations()
|
||||
for n in ENUMS:
|
||||
setattr(obj, n[0], n[1])
|
||||
obj.StepOver = 50
|
||||
|
||||
def opOnDocumentRestored(self, obj):
|
||||
if not hasattr(obj, "StartRadius"):
|
||||
obj.addProperty(
|
||||
"App::PropertyLength",
|
||||
"StartRadius",
|
||||
"Helix Drill",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Starting Radius"),
|
||||
)
|
||||
|
||||
if not hasattr(obj, "OffsetExtra"):
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"OffsetExtra",
|
||||
"Helix Drill",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Extra value to stay away from final profile- good for roughing toolpath",
|
||||
),
|
||||
)
|
||||
|
||||
def circularHoleExecute(self, obj, holes):
|
||||
"""circularHoleExecute(obj, holes) ... generate helix commands for each hole in holes"""
|
||||
Path.Log.track()
|
||||
self.commandlist.append(Path.Command("(helix cut operation)"))
|
||||
|
||||
self.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value}))
|
||||
|
||||
holes = sort_locations(holes, ["x", "y"])
|
||||
|
||||
tool = obj.ToolController.Tool
|
||||
tooldiamter = (
|
||||
tool.Diameter.Value if hasattr(tool.Diameter, "Value") else tool.Diameter
|
||||
)
|
||||
|
||||
args = {
|
||||
"edge": None,
|
||||
"hole_radius": None,
|
||||
"step_down": obj.StepDown.Value,
|
||||
"step_over": obj.StepOver / 100,
|
||||
"tool_diameter": tooldiamter,
|
||||
"inner_radius": obj.StartRadius.Value + obj.OffsetExtra.Value,
|
||||
"direction": obj.Direction,
|
||||
"startAt": obj.StartSide,
|
||||
}
|
||||
|
||||
for hole in holes:
|
||||
args["hole_radius"] = (hole["r"] / 2) - (obj.OffsetExtra.Value)
|
||||
startPoint = FreeCAD.Vector(hole["x"], hole["y"], obj.StartDepth.Value)
|
||||
endPoint = FreeCAD.Vector(hole["x"], hole["y"], obj.FinalDepth.Value)
|
||||
args["edge"] = Part.makeLine(startPoint, endPoint)
|
||||
|
||||
# move to starting position
|
||||
self.commandlist.append(
|
||||
Path.Command("G0", {"Z": obj.ClearanceHeight.Value})
|
||||
)
|
||||
self.commandlist.append(
|
||||
Path.Command(
|
||||
"G0",
|
||||
{
|
||||
"X": startPoint.x,
|
||||
"Y": startPoint.y,
|
||||
"Z": obj.ClearanceHeight.Value,
|
||||
},
|
||||
)
|
||||
)
|
||||
self.commandlist.append(
|
||||
Path.Command(
|
||||
"G0", {"X": startPoint.x, "Y": startPoint.y, "Z": startPoint.z}
|
||||
)
|
||||
)
|
||||
|
||||
results = helix_generator.generate(**args)
|
||||
|
||||
for command in results:
|
||||
self.commandlist.append(command)
|
||||
|
||||
PathFeedRate.setFeedRate(self.commandlist, obj.ToolController)
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
setup = []
|
||||
setup.append("Direction")
|
||||
setup.append("StartSide")
|
||||
setup.append("StepOver")
|
||||
setup.append("StartRadius")
|
||||
return setup
|
||||
|
||||
|
||||
def Create(name, obj=None, parentJob=None):
|
||||
"""Create(name) ... Creates and returns a Helix operation."""
|
||||
if obj is None:
|
||||
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
||||
obj.Proxy = ObjectHelix(obj, name, parentJob)
|
||||
if obj.Proxy:
|
||||
obj.Proxy.findAllHoles(obj)
|
||||
return obj
|
||||
@@ -1,119 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import PathGui as PGui # ensure Path/Gui/Resources are loaded
|
||||
import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui
|
||||
import PathScripts.PathHelix as PathHelix
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathGui as PathGui
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
from PySide import QtCore
|
||||
|
||||
__doc__ = "Helix operation page controller and command implementation."
|
||||
|
||||
LOGLEVEL = False
|
||||
|
||||
if LOGLEVEL:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.NOTICE, Path.Log.thisModule())
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
|
||||
"""Page controller class for Helix operations."""
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... return UI"""
|
||||
|
||||
form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpHelixEdit.ui")
|
||||
comboToPropertyMap = [("startSide", "StartSide"), ("direction", "Direction")]
|
||||
|
||||
enumTups = PathHelix.ObjectHelix.helixOpPropertyEnumerations(dataType="raw")
|
||||
|
||||
self.populateCombobox(form, enumTups, comboToPropertyMap)
|
||||
return form
|
||||
|
||||
def getFields(self, obj):
|
||||
"""getFields(obj) ... transfers values from UI to obj's proprties"""
|
||||
Path.Log.track()
|
||||
if obj.Direction != str(self.form.direction.currentData()):
|
||||
obj.Direction = str(self.form.direction.currentData())
|
||||
if obj.StartSide != str(self.form.startSide.currentData()):
|
||||
obj.StartSide = str(self.form.startSide.currentData())
|
||||
if obj.StepOver != self.form.stepOverPercent.value():
|
||||
obj.StepOver = self.form.stepOverPercent.value()
|
||||
PathGui.updateInputField(obj, "OffsetExtra", self.form.extraOffset)
|
||||
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
self.updateCoolant(obj, self.form.coolantController)
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... transfers obj's property values to UI"""
|
||||
Path.Log.track()
|
||||
|
||||
self.form.stepOverPercent.setValue(obj.StepOver)
|
||||
self.selectInComboBox(obj.Direction, self.form.direction)
|
||||
self.selectInComboBox(obj.StartSide, self.form.startSide)
|
||||
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.setupCoolant(obj, self.form.coolantController)
|
||||
|
||||
self.form.extraOffset.setText(
|
||||
FreeCAD.Units.Quantity(
|
||||
obj.OffsetExtra.Value, FreeCAD.Units.Length
|
||||
).UserString
|
||||
)
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
|
||||
signals = []
|
||||
|
||||
signals.append(self.form.stepOverPercent.editingFinished)
|
||||
signals.append(self.form.extraOffset.editingFinished)
|
||||
signals.append(self.form.direction.currentIndexChanged)
|
||||
signals.append(self.form.startSide.currentIndexChanged)
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
|
||||
return signals
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Helix",
|
||||
PathHelix.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_Helix",
|
||||
QT_TRANSLATE_NOOP("Path_Helix", "Helix"),
|
||||
QT_TRANSLATE_NOOP(
|
||||
"Path_Helix", "Creates a Path Helix object from a features of a base object"
|
||||
),
|
||||
PathHelix.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathHelixGui... done\n")
|
||||
@@ -1,411 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2016 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 __future__ import print_function
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathPocketBase as PathPocketBase
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
import numpy
|
||||
|
||||
# lazily loaded modules
|
||||
from lazy_loader.lazy_loader import LazyLoader
|
||||
|
||||
Part = LazyLoader("Part", globals(), "Part")
|
||||
|
||||
__title__ = "Path Mill Face Operation"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Class and implementation of Mill Facing operation."
|
||||
__contributors__ = "russ4262 (Russell Johnson)"
|
||||
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class ObjectFace(PathPocketBase.ObjectPocket):
|
||||
"""Proxy object for Mill Facing operation."""
|
||||
|
||||
@classmethod
|
||||
def propertyEnumerations(self, dataType="data"):
|
||||
"""helixOpPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
|
||||
Args:
|
||||
dataType = 'data', 'raw', 'translated'
|
||||
Notes:
|
||||
'data' is list of internal string literals used in code
|
||||
'raw' is list of (translated_text, data_string) tuples
|
||||
'translated' is list of translated string literals
|
||||
"""
|
||||
|
||||
enums = {
|
||||
"BoundaryShape": [
|
||||
(translate("Path_Pocket", "Boundbox"), "Boundbox"),
|
||||
(translate("Path_Pocket", "Face Region"), "Face Region"),
|
||||
(translate("Path_Pocket", "Perimeter"), "Perimeter"),
|
||||
(translate("Path_Pocket", "Stock"), "Stock"),
|
||||
],
|
||||
}
|
||||
|
||||
if dataType == "raw":
|
||||
return enums
|
||||
|
||||
data = list()
|
||||
idx = 0 if dataType == "translated" else 1
|
||||
|
||||
Path.Log.debug(enums)
|
||||
|
||||
for k, v in enumerate(enums):
|
||||
data.append((v, [tup[idx] for tup in enums[v]]))
|
||||
Path.Log.debug(data)
|
||||
|
||||
return data
|
||||
|
||||
def initPocketOp(self, obj):
|
||||
Path.Log.track()
|
||||
"""initPocketOp(obj) ... create facing specific properties"""
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"BoundaryShape",
|
||||
"Face",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Shape to use for calculating Boundary"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"ClearEdges",
|
||||
"Face",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Clear edges of surface (Only applicable to BoundBox)"
|
||||
),
|
||||
)
|
||||
if not hasattr(obj, "ExcludeRaisedAreas"):
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"ExcludeRaisedAreas",
|
||||
"Face",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Exclude milling raised areas inside the face."
|
||||
),
|
||||
)
|
||||
|
||||
for n in self.propertyEnumerations():
|
||||
setattr(obj, n[0], n[1])
|
||||
|
||||
def pocketInvertExtraOffset(self):
|
||||
return True
|
||||
|
||||
def areaOpOnChanged(self, obj, prop):
|
||||
"""areaOpOnChanged(obj, prop) ... facing specific depths calculation."""
|
||||
Path.Log.track(prop)
|
||||
if prop == "StepOver" and obj.StepOver == 0:
|
||||
obj.StepOver = 1
|
||||
|
||||
# default depths calculation not correct for facing
|
||||
if prop == "Base":
|
||||
job = PathUtils.findParentJob(obj)
|
||||
if job:
|
||||
obj.OpStartDepth = job.Stock.Shape.BoundBox.ZMax
|
||||
|
||||
if len(obj.Base) >= 1:
|
||||
Path.Log.debug("processing")
|
||||
sublist = []
|
||||
for i in obj.Base:
|
||||
o = i[0]
|
||||
for s in i[1]:
|
||||
sublist.append(o.Shape.getElement(s))
|
||||
|
||||
# If the operation has a geometry identified the Finaldepth
|
||||
# is the top of the bboundbox which includes all features.
|
||||
# Otherwise, top of part.
|
||||
|
||||
obj.OpFinalDepth = Part.makeCompound(sublist).BoundBox.ZMax
|
||||
elif job:
|
||||
obj.OpFinalDepth = job.Proxy.modelBoundBox(job).ZMax
|
||||
|
||||
def areaOpShapes(self, obj):
|
||||
"""areaOpShapes(obj) ... return top face"""
|
||||
# Facing is done either against base objects
|
||||
self.removalshapes = []
|
||||
holeShape = None
|
||||
|
||||
Path.Log.debug("depthparams: {}".format([i for i in self.depthparams]))
|
||||
|
||||
if obj.Base:
|
||||
Path.Log.debug("obj.Base: {}".format(obj.Base))
|
||||
faces = []
|
||||
holes = []
|
||||
holeEnvs = []
|
||||
oneBase = [obj.Base[0][0], True]
|
||||
sub0 = getattr(obj.Base[0][0].Shape, obj.Base[0][1][0])
|
||||
minHeight = sub0.BoundBox.ZMax
|
||||
|
||||
for b in obj.Base:
|
||||
for sub in b[1]:
|
||||
shape = getattr(b[0].Shape, sub)
|
||||
if isinstance(shape, Part.Face):
|
||||
faces.append(shape)
|
||||
if shape.BoundBox.ZMin < minHeight:
|
||||
minHeight = shape.BoundBox.ZMin
|
||||
# Limit to one model base per operation
|
||||
if oneBase[0] is not b[0]:
|
||||
oneBase[1] = False
|
||||
if numpy.isclose(
|
||||
abs(shape.normalAt(0, 0).z), 1
|
||||
): # horizontal face
|
||||
# Analyze internal closed wires to determine if raised or a recess
|
||||
for wire in shape.Wires[1:]:
|
||||
if obj.ExcludeRaisedAreas:
|
||||
ip = self.isPocket(b[0], shape, wire)
|
||||
if ip is False:
|
||||
holes.append((b[0].Shape, wire))
|
||||
else:
|
||||
holes.append((b[0].Shape, wire))
|
||||
else:
|
||||
Path.Log.warning(
|
||||
'The base subobject, "{0}," is not a face. Ignoring "{0}."'.format(
|
||||
sub
|
||||
)
|
||||
)
|
||||
|
||||
if obj.ExcludeRaisedAreas and len(holes) > 0:
|
||||
for shape, wire in holes:
|
||||
f = Part.makeFace(wire, "Part::FaceMakerSimple")
|
||||
env = PathUtils.getEnvelope(
|
||||
shape, subshape=f, depthparams=self.depthparams
|
||||
)
|
||||
holeEnvs.append(env)
|
||||
holeShape = Part.makeCompound(holeEnvs)
|
||||
|
||||
Path.Log.debug("Working on a collection of faces {}".format(faces))
|
||||
planeshape = Part.makeCompound(faces)
|
||||
|
||||
# If no base object, do planing of top surface of entire model
|
||||
else:
|
||||
planeshape = Part.makeCompound([base.Shape for base in self.model])
|
||||
Path.Log.debug("Working on a shape {}".format(obj.Label))
|
||||
|
||||
# Find the correct shape depending on Boundary shape.
|
||||
Path.Log.debug("Boundary Shape: {}".format(obj.BoundaryShape))
|
||||
bb = planeshape.BoundBox
|
||||
|
||||
# Apply offset for clearing edges
|
||||
offset = 0
|
||||
if obj.ClearEdges:
|
||||
offset = self.radius + 0.1
|
||||
|
||||
bb.XMin = bb.XMin - offset
|
||||
bb.YMin = bb.YMin - offset
|
||||
bb.XMax = bb.XMax + offset
|
||||
bb.YMax = bb.YMax + offset
|
||||
|
||||
if obj.BoundaryShape == "Boundbox":
|
||||
bbperim = Part.makeBox(
|
||||
bb.XLength,
|
||||
bb.YLength,
|
||||
1,
|
||||
FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMin),
|
||||
FreeCAD.Vector(0, 0, 1),
|
||||
)
|
||||
env = PathUtils.getEnvelope(partshape=bbperim, depthparams=self.depthparams)
|
||||
if obj.ExcludeRaisedAreas and oneBase[1]:
|
||||
includedFaces = self.getAllIncludedFaces(
|
||||
oneBase[0], env, faceZ=minHeight
|
||||
)
|
||||
if len(includedFaces) > 0:
|
||||
includedShape = Part.makeCompound(includedFaces)
|
||||
includedEnv = PathUtils.getEnvelope(
|
||||
oneBase[0].Shape,
|
||||
subshape=includedShape,
|
||||
depthparams=self.depthparams,
|
||||
)
|
||||
env = env.cut(includedEnv)
|
||||
elif obj.BoundaryShape == "Stock":
|
||||
stock = PathUtils.findParentJob(obj).Stock.Shape
|
||||
env = stock
|
||||
|
||||
if obj.ExcludeRaisedAreas and oneBase[1]:
|
||||
includedFaces = self.getAllIncludedFaces(
|
||||
oneBase[0], stock, faceZ=minHeight
|
||||
)
|
||||
if len(includedFaces) > 0:
|
||||
stockEnv = PathUtils.getEnvelope(
|
||||
partshape=stock, depthparams=self.depthparams
|
||||
)
|
||||
includedShape = Part.makeCompound(includedFaces)
|
||||
includedEnv = PathUtils.getEnvelope(
|
||||
oneBase[0].Shape,
|
||||
subshape=includedShape,
|
||||
depthparams=self.depthparams,
|
||||
)
|
||||
env = stockEnv.cut(includedEnv)
|
||||
elif obj.BoundaryShape == "Perimeter":
|
||||
if obj.ClearEdges:
|
||||
psZMin = planeshape.BoundBox.ZMin
|
||||
ofstShape = PathUtils.getOffsetArea(
|
||||
planeshape, self.radius * 1.25, plane=planeshape
|
||||
)
|
||||
ofstShape.translate(
|
||||
FreeCAD.Vector(0.0, 0.0, psZMin - ofstShape.BoundBox.ZMin)
|
||||
)
|
||||
env = PathUtils.getEnvelope(
|
||||
partshape=ofstShape, depthparams=self.depthparams
|
||||
)
|
||||
else:
|
||||
env = PathUtils.getEnvelope(
|
||||
partshape=planeshape, depthparams=self.depthparams
|
||||
)
|
||||
elif obj.BoundaryShape == "Face Region":
|
||||
baseShape = planeshape # oneBase[0].Shape
|
||||
psZMin = planeshape.BoundBox.ZMin
|
||||
ofst = 0.0
|
||||
if obj.ClearEdges:
|
||||
ofst = self.tool.Diameter * 0.51
|
||||
ofstShape = PathUtils.getOffsetArea(planeshape, ofst, plane=planeshape)
|
||||
ofstShape.translate(
|
||||
FreeCAD.Vector(0.0, 0.0, psZMin - ofstShape.BoundBox.ZMin)
|
||||
)
|
||||
|
||||
# Calculate custom depth params for removal shape envelope, with start and final depth buffers
|
||||
custDepthparams = self._customDepthParams(
|
||||
obj, obj.StartDepth.Value + 0.2, obj.FinalDepth.Value - 0.1
|
||||
) # only an envelope
|
||||
ofstShapeEnv = PathUtils.getEnvelope(
|
||||
partshape=ofstShape, depthparams=custDepthparams
|
||||
)
|
||||
if obj.ExcludeRaisedAreas:
|
||||
env = ofstShapeEnv.cut(baseShape)
|
||||
env.translate(
|
||||
FreeCAD.Vector(0.0, 0.0, -0.00001)
|
||||
) # lower removal shape into buffer zone
|
||||
else:
|
||||
env = ofstShapeEnv
|
||||
|
||||
if holeShape:
|
||||
Path.Log.debug("Processing holes and face ...")
|
||||
holeEnv = PathUtils.getEnvelope(
|
||||
partshape=holeShape, depthparams=self.depthparams
|
||||
)
|
||||
newEnv = env.cut(holeEnv)
|
||||
tup = newEnv, False, "pathMillFace"
|
||||
else:
|
||||
Path.Log.debug("Processing solid face ...")
|
||||
tup = env, False, "pathMillFace"
|
||||
|
||||
self.removalshapes.append(tup)
|
||||
obj.removalshape = self.removalshapes[0][0] # save removal shape
|
||||
|
||||
return self.removalshapes
|
||||
|
||||
def areaOpSetDefaultValues(self, obj, job):
|
||||
"""areaOpSetDefaultValues(obj, job) ... initialize mill facing properties"""
|
||||
obj.StepOver = 50
|
||||
obj.ZigZagAngle = 45.0
|
||||
obj.ExcludeRaisedAreas = False
|
||||
obj.ClearEdges = False
|
||||
|
||||
# need to overwrite the default depth calculations for facing
|
||||
if job and len(job.Model.Group) > 0:
|
||||
obj.OpStartDepth = job.Stock.Shape.BoundBox.ZMax
|
||||
obj.OpFinalDepth = job.Proxy.modelBoundBox(job).ZMax
|
||||
|
||||
# If the operation has a geometry identified the Finaldepth
|
||||
# is the top of the boundbox which includes all features.
|
||||
if len(obj.Base) >= 1:
|
||||
shapes = []
|
||||
for base, subs in obj.Base:
|
||||
for s in subs:
|
||||
shapes.append(getattr(base.Shape, s))
|
||||
obj.OpFinalDepth = Part.makeCompound(shapes).BoundBox.ZMax
|
||||
|
||||
def isPocket(self, b, f, w):
|
||||
e = w.Edges[0]
|
||||
for fi in range(0, len(b.Shape.Faces)):
|
||||
face = b.Shape.Faces[fi]
|
||||
for ei in range(0, len(face.Edges)):
|
||||
edge = face.Edges[ei]
|
||||
if e.isSame(edge):
|
||||
if f is face:
|
||||
# Alternative: run loop to see if all edges are same
|
||||
pass # same source face, look for another
|
||||
else:
|
||||
if face.CenterOfMass.z < f.CenterOfMass.z:
|
||||
return True
|
||||
return False
|
||||
|
||||
def getAllIncludedFaces(self, base, env, faceZ):
|
||||
"""getAllIncludedFaces(base, env, faceZ)...
|
||||
Return all `base` faces extending above `faceZ` whose boundboxes overlap with the `env` boundbox."""
|
||||
included = []
|
||||
|
||||
eXMin = env.BoundBox.XMin
|
||||
eXMax = env.BoundBox.XMax
|
||||
eYMin = env.BoundBox.YMin
|
||||
eYMax = env.BoundBox.YMax
|
||||
eZMin = faceZ
|
||||
|
||||
def isOverlap(faceMin, faceMax, envMin, envMax):
|
||||
if faceMax > envMin:
|
||||
if faceMin <= envMax or faceMin == envMin:
|
||||
return True
|
||||
return False
|
||||
|
||||
for fi in range(0, len(base.Shape.Faces)):
|
||||
# Check all faces of `base` shape
|
||||
incl = False
|
||||
face = base.Shape.Faces[fi]
|
||||
fXMin = face.BoundBox.XMin
|
||||
fXMax = face.BoundBox.XMax
|
||||
fYMin = face.BoundBox.YMin
|
||||
fYMax = face.BoundBox.YMax
|
||||
fZMax = face.BoundBox.ZMax
|
||||
|
||||
if fZMax > eZMin:
|
||||
# Include face if its boundbox overlaps envelope boundbox
|
||||
if isOverlap(fXMin, fXMax, eXMin, eXMax): # check X values
|
||||
if isOverlap(fYMin, fYMax, eYMin, eYMax): # check Y values
|
||||
incl = True
|
||||
if incl:
|
||||
included.append(face)
|
||||
return included
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
setup = PathPocketBase.SetupProperties()
|
||||
setup.append("BoundaryShape")
|
||||
setup.append("ExcludeRaisedAreas")
|
||||
setup.append("ClearEdges")
|
||||
return setup
|
||||
|
||||
|
||||
def Create(name, obj=None, parentJob=None):
|
||||
"""Create(name) ... Creates and returns a Mill Facing operation."""
|
||||
if obj is None:
|
||||
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
||||
obj.Proxy = ObjectFace(obj, name, parentJob)
|
||||
return obj
|
||||
@@ -1,83 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathMillFace as PathMillFace
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathPocketBaseGui as PathPocketBaseGui
|
||||
import PathScripts.PathPocketShape as PathPocketShape
|
||||
import FreeCADGui
|
||||
|
||||
__title__ = "Path Face Mill Operation UI"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Face Mill operation page controller and command implementation."
|
||||
|
||||
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 TaskPanelOpPage(PathPocketBaseGui.TaskPanelOpPage):
|
||||
"""Page controller class for the face milling operation."""
|
||||
|
||||
def getForm(self):
|
||||
Path.Log.track()
|
||||
"""getForm() ... return UI"""
|
||||
|
||||
form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpPocketFullEdit.ui")
|
||||
comboToPropertyMap = [
|
||||
("cutMode", "CutMode"),
|
||||
("offsetPattern", "OffsetPattern"),
|
||||
("boundaryShape", "BoundaryShape"),
|
||||
]
|
||||
|
||||
enumTups = PathMillFace.ObjectFace.propertyEnumerations(dataType="raw")
|
||||
enumTups.update(
|
||||
PathPocketShape.ObjectPocket.pocketPropertyEnumerations(dataType="raw")
|
||||
)
|
||||
|
||||
self.populateCombobox(form, enumTups, comboToPropertyMap)
|
||||
return form
|
||||
|
||||
def pocketFeatures(self):
|
||||
"""pocketFeatures() ... return FeatureFacing (see PathPocketBaseGui)"""
|
||||
return PathPocketBaseGui.FeatureFacing
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"MillFace",
|
||||
PathMillFace.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_Face",
|
||||
QT_TRANSLATE_NOOP("Path_MillFace", "Face"),
|
||||
QT_TRANSLATE_NOOP(
|
||||
"Path_MillFace", "Create a Facing Operation from a model or face"
|
||||
),
|
||||
PathMillFace.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathMillFaceGui... done\n")
|
||||
@@ -1,919 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
from PathScripts.PathUtils import waiting_effects
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
import Path
|
||||
import PathScripts.PathGeom as PathGeom
|
||||
import PathScripts.PathPreferences as PathPreferences
|
||||
import PathScripts.PathUtil as PathUtil
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
import math
|
||||
import time
|
||||
|
||||
|
||||
# lazily loaded modules
|
||||
from lazy_loader.lazy_loader import LazyLoader
|
||||
|
||||
Part = LazyLoader("Part", globals(), "Part")
|
||||
|
||||
__title__ = "Base class for all operations."
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Base class and properties implementation for all Path operations."
|
||||
|
||||
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
|
||||
|
||||
|
||||
FeatureTool = 0x0001 # ToolController
|
||||
FeatureDepths = 0x0002 # FinalDepth, StartDepth
|
||||
FeatureHeights = 0x0004 # ClearanceHeight, SafeHeight
|
||||
FeatureStartPoint = 0x0008 # StartPoint
|
||||
FeatureFinishDepth = 0x0010 # FinishDepth
|
||||
FeatureStepDown = 0x0020 # StepDown
|
||||
FeatureNoFinalDepth = 0x0040 # edit or not edit FinalDepth
|
||||
FeatureBaseVertexes = 0x0100 # Base
|
||||
FeatureBaseEdges = 0x0200 # Base
|
||||
FeatureBaseFaces = 0x0400 # Base
|
||||
FeatureBasePanels = 0x0800 # Base
|
||||
FeatureLocations = 0x1000 # Locations
|
||||
FeatureCoolant = 0x2000 # Coolant
|
||||
FeatureDiameters = 0x4000 # Turning Diameters
|
||||
|
||||
FeatureBaseGeometry = FeatureBaseVertexes | FeatureBaseFaces | FeatureBaseEdges
|
||||
|
||||
|
||||
class PathNoTCException(Exception):
|
||||
"""PathNoTCException is raised when no TC was selected or matches the input
|
||||
criteria. This can happen intentionally by the user when they cancel the TC
|
||||
selection dialog."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("No Tool Controller found")
|
||||
|
||||
|
||||
class ObjectOp(object):
|
||||
"""
|
||||
Base class for proxy objects of all Path operations.
|
||||
|
||||
Use this class as a base class for new operations. It provides properties
|
||||
and some functionality for the standard properties each operation supports.
|
||||
By OR'ing features from the feature list an operation can select which ones
|
||||
of the standard features it requires and/or supports.
|
||||
|
||||
The currently supported features are:
|
||||
FeatureTool ... Use of a ToolController
|
||||
FeatureDepths ... Depths, for start, final
|
||||
FeatureHeights ... Heights, safe and clearance
|
||||
FeatureStartPoint ... Supports setting a start point
|
||||
FeatureFinishDepth ... Operation supports a finish depth
|
||||
FeatureStepDown ... Support for step down
|
||||
FeatureNoFinalDepth ... Disable support for final depth modifications
|
||||
FeatureBaseVertexes ... Base geometry support for vertexes
|
||||
FeatureBaseEdges ... Base geometry support for edges
|
||||
FeatureBaseFaces ... Base geometry support for faces
|
||||
FeatureLocations ... Base location support
|
||||
FeatureCoolant ... Support for operation coolant
|
||||
FeatureDiameters ... Support for turning operation diameters
|
||||
|
||||
The base class handles all base API and forwards calls to subclasses with
|
||||
an op prefix. For instance, an op is not expected to overwrite onChanged(),
|
||||
but implement the function opOnChanged().
|
||||
If a base class overwrites a base API function it should call the super's
|
||||
implementation - otherwise the base functionality might be broken.
|
||||
"""
|
||||
|
||||
def addBaseProperty(self, obj):
|
||||
obj.addProperty(
|
||||
"App::PropertyLinkSubListGlobal",
|
||||
"Base",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The base geometry for this operation"),
|
||||
)
|
||||
|
||||
def addOpValues(self, obj, values):
|
||||
if "start" in values:
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"OpStartDepth",
|
||||
"Op Values",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Holds the calculated value for the StartDepth"
|
||||
),
|
||||
)
|
||||
obj.setEditorMode("OpStartDepth", 1) # read-only
|
||||
if "final" in values:
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"OpFinalDepth",
|
||||
"Op Values",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Holds the calculated value for the FinalDepth"
|
||||
),
|
||||
)
|
||||
obj.setEditorMode("OpFinalDepth", 1) # read-only
|
||||
if "tooldia" in values:
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"OpToolDiameter",
|
||||
"Op Values",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Holds the diameter of the tool"),
|
||||
)
|
||||
obj.setEditorMode("OpToolDiameter", 1) # read-only
|
||||
if "stockz" in values:
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"OpStockZMax",
|
||||
"Op Values",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Holds the max Z value of Stock"),
|
||||
)
|
||||
obj.setEditorMode("OpStockZMax", 1) # read-only
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"OpStockZMin",
|
||||
"Op Values",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Holds the min Z value of Stock"),
|
||||
)
|
||||
obj.setEditorMode("OpStockZMin", 1) # read-only
|
||||
|
||||
def __init__(self, obj, name, parentJob=None):
|
||||
Path.Log.track()
|
||||
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"Active",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Make False, to prevent operation from generating code"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyString",
|
||||
"Comment",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "An optional comment for this Operation"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyString",
|
||||
"UserLabel",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "User Assigned Label"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyString",
|
||||
"CycleTime",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Operations Cycle Time Estimation"),
|
||||
)
|
||||
obj.setEditorMode("CycleTime", 1) # read-only
|
||||
|
||||
features = self.opFeatures(obj)
|
||||
|
||||
if FeatureBaseGeometry & features:
|
||||
self.addBaseProperty(obj)
|
||||
|
||||
if FeatureLocations & features:
|
||||
obj.addProperty(
|
||||
"App::PropertyVectorList",
|
||||
"Locations",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Base locations for this operation"),
|
||||
)
|
||||
|
||||
if FeatureTool & features:
|
||||
obj.addProperty(
|
||||
"App::PropertyLink",
|
||||
"ToolController",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"The tool controller that will be used to calculate the path",
|
||||
),
|
||||
)
|
||||
self.addOpValues(obj, ["tooldia"])
|
||||
|
||||
if FeatureCoolant & features:
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"CoolantMode",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Coolant mode for this operation"),
|
||||
)
|
||||
|
||||
if FeatureDepths & features:
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"StartDepth",
|
||||
"Depth",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Starting Depth of Tool- first cut depth in Z"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"FinalDepth",
|
||||
"Depth",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Final Depth of Tool- lowest value in Z"
|
||||
),
|
||||
)
|
||||
if FeatureNoFinalDepth & features:
|
||||
obj.setEditorMode("FinalDepth", 2) # hide
|
||||
self.addOpValues(obj, ["start", "final"])
|
||||
else:
|
||||
# StartDepth has become necessary for expressions on other properties
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"StartDepth",
|
||||
"Depth",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Starting Depth internal use only for derived values",
|
||||
),
|
||||
)
|
||||
obj.setEditorMode("StartDepth", 1) # read-only
|
||||
|
||||
self.addOpValues(obj, ["stockz"])
|
||||
|
||||
if FeatureStepDown & features:
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"StepDown",
|
||||
"Depth",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Incremental Step Down of Tool"),
|
||||
)
|
||||
|
||||
if FeatureFinishDepth & features:
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"FinishDepth",
|
||||
"Depth",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Maximum material removed on final pass."
|
||||
),
|
||||
)
|
||||
|
||||
if FeatureHeights & features:
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"ClearanceHeight",
|
||||
"Depth",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"The height needed to clear clamps and obstructions",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"SafeHeight",
|
||||
"Depth",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Rapid Safety Height between locations."
|
||||
),
|
||||
)
|
||||
|
||||
if FeatureStartPoint & features:
|
||||
obj.addProperty(
|
||||
"App::PropertyVectorDistance",
|
||||
"StartPoint",
|
||||
"Start Point",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The start point of this path"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"UseStartPoint",
|
||||
"Start Point",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Make True, if specifying a Start Point"
|
||||
),
|
||||
)
|
||||
|
||||
if FeatureDiameters & features:
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"MinDiameter",
|
||||
"Diameter",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Lower limit of the turning diameter"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"MaxDiameter",
|
||||
"Diameter",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Upper limit of the turning diameter."
|
||||
),
|
||||
)
|
||||
|
||||
for n in self.opPropertyEnumerations():
|
||||
Path.Log.debug("n: {}".format(n))
|
||||
Path.Log.debug("n[0]: {} n[1]: {}".format(n[0], n[1]))
|
||||
if hasattr(obj, n[0]):
|
||||
setattr(obj, n[0], n[1])
|
||||
|
||||
# members being set later
|
||||
self.commandlist = None
|
||||
self.horizFeed = None
|
||||
self.horizRapid = None
|
||||
self.job = None
|
||||
self.model = None
|
||||
self.radius = None
|
||||
self.stock = None
|
||||
self.tool = None
|
||||
self.vertFeed = None
|
||||
self.vertRapid = None
|
||||
self.addNewProps = None
|
||||
|
||||
self.initOperation(obj)
|
||||
|
||||
if not hasattr(obj, "DoNotSetDefaultValues") or not obj.DoNotSetDefaultValues:
|
||||
if parentJob:
|
||||
self.job = PathUtils.addToJob(obj, jobname=parentJob.Name)
|
||||
job = self.setDefaultValues(obj)
|
||||
if job:
|
||||
job.SetupSheet.Proxy.setOperationProperties(obj, name)
|
||||
obj.recompute()
|
||||
obj.Proxy = self
|
||||
|
||||
@classmethod
|
||||
def opPropertyEnumerations(self, dataType="data"):
|
||||
"""opPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
|
||||
Args:
|
||||
dataType = 'data', 'raw', 'translated'
|
||||
Notes:
|
||||
'data' is list of internal string literals used in code
|
||||
'raw' is list of (translated_text, data_string) tuples
|
||||
'translated' is list of translated string literals
|
||||
"""
|
||||
|
||||
enums = {
|
||||
"CoolantMode": [
|
||||
(translate("Path_Operation", "None"), "None"),
|
||||
(translate("Path_Operation", "Flood"), "Flood"),
|
||||
(translate("Path_Operation", "Mist"), "Mist"),
|
||||
],
|
||||
}
|
||||
|
||||
if dataType == "raw":
|
||||
return enums
|
||||
|
||||
data = list()
|
||||
idx = 0 if dataType == "translated" else 1
|
||||
|
||||
Path.Log.debug(enums)
|
||||
|
||||
for k, v in enumerate(enums):
|
||||
data.append((v, [tup[idx] for tup in enums[v]]))
|
||||
Path.Log.debug(data)
|
||||
|
||||
return data
|
||||
|
||||
def setEditorModes(self, obj, features):
|
||||
"""Editor modes are not preserved during document store/restore, set editor modes for all properties"""
|
||||
|
||||
for op in ["OpStartDepth", "OpFinalDepth", "OpToolDiameter", "CycleTime"]:
|
||||
if hasattr(obj, op):
|
||||
obj.setEditorMode(op, 1) # read-only
|
||||
|
||||
if FeatureDepths & features:
|
||||
if FeatureNoFinalDepth & features:
|
||||
obj.setEditorMode("OpFinalDepth", 2)
|
||||
|
||||
def onDocumentRestored(self, obj):
|
||||
Path.Log.track()
|
||||
features = self.opFeatures(obj)
|
||||
if (
|
||||
FeatureBaseGeometry & features
|
||||
and "App::PropertyLinkSubList" == obj.getTypeIdOfProperty("Base")
|
||||
):
|
||||
Path.Log.info("Replacing link property with global link (%s)." % obj.State)
|
||||
base = obj.Base
|
||||
obj.removeProperty("Base")
|
||||
self.addBaseProperty(obj)
|
||||
obj.Base = base
|
||||
obj.touch()
|
||||
obj.Document.recompute()
|
||||
|
||||
if FeatureTool & features and not hasattr(obj, "OpToolDiameter"):
|
||||
self.addOpValues(obj, ["tooldia"])
|
||||
|
||||
if FeatureCoolant & features:
|
||||
oldvalue = str(obj.CoolantMode) if hasattr(obj, "CoolantMode") else "None"
|
||||
if (
|
||||
hasattr(obj, "CoolantMode")
|
||||
and not obj.getTypeIdOfProperty("CoolantMode")
|
||||
== "App::PropertyEnumeration"
|
||||
):
|
||||
obj.removeProperty("CoolantMode")
|
||||
|
||||
if not hasattr(obj, "CoolantMode"):
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"CoolantMode",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Coolant option for this operation"
|
||||
),
|
||||
)
|
||||
for n in self.opPropertyEnumerations():
|
||||
if n[0] == "CoolantMode":
|
||||
setattr(obj, n[0], n[1])
|
||||
obj.CoolantMode = oldvalue
|
||||
|
||||
if FeatureDepths & features and not hasattr(obj, "OpStartDepth"):
|
||||
self.addOpValues(obj, ["start", "final"])
|
||||
if FeatureNoFinalDepth & features:
|
||||
obj.setEditorMode("OpFinalDepth", 2)
|
||||
|
||||
if not hasattr(obj, "OpStockZMax"):
|
||||
self.addOpValues(obj, ["stockz"])
|
||||
|
||||
if not hasattr(obj, "CycleTime"):
|
||||
obj.addProperty(
|
||||
"App::PropertyString",
|
||||
"CycleTime",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Operations Cycle Time Estimation"),
|
||||
)
|
||||
|
||||
self.setEditorModes(obj, features)
|
||||
self.opOnDocumentRestored(obj)
|
||||
|
||||
def __getstate__(self):
|
||||
"""__getstat__(self) ... called when receiver is saved.
|
||||
Can safely be overwritten by subclasses."""
|
||||
return None
|
||||
|
||||
def __setstate__(self, state):
|
||||
"""__getstat__(self) ... called when receiver is restored.
|
||||
Can safely be overwritten by subclasses."""
|
||||
return None
|
||||
|
||||
def opFeatures(self, obj):
|
||||
"""opFeatures(obj) ... returns the OR'ed list of features used and supported by the operation.
|
||||
The default implementation returns "FeatureTool | FeatureDepths | FeatureHeights | FeatureStartPoint"
|
||||
Should be overwritten by subclasses."""
|
||||
return (
|
||||
FeatureTool
|
||||
| FeatureDepths
|
||||
| FeatureHeights
|
||||
| FeatureStartPoint
|
||||
| FeatureBaseGeometry
|
||||
| FeatureFinishDepth
|
||||
| FeatureCoolant
|
||||
)
|
||||
|
||||
def initOperation(self, obj):
|
||||
"""initOperation(obj) ... implement to create additional properties.
|
||||
Should be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def opOnDocumentRestored(self, obj):
|
||||
"""opOnDocumentRestored(obj) ... implement if an op needs special handling like migrating the data model.
|
||||
Should be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def opOnChanged(self, obj, prop):
|
||||
"""opOnChanged(obj, prop) ... overwrite to process property changes.
|
||||
This is a callback function that is invoked each time a property of the
|
||||
receiver is assigned a value. Note that the FC framework does not
|
||||
distinguish between assigning a different value and assigning the same
|
||||
value again.
|
||||
Can safely be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def opSetDefaultValues(self, obj, job):
|
||||
"""opSetDefaultValues(obj, job) ... overwrite to set initial default values.
|
||||
Called after the receiver has been fully created with all properties.
|
||||
Can safely be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def opUpdateDepths(self, obj):
|
||||
"""opUpdateDepths(obj) ... overwrite to implement special depths calculation.
|
||||
Can safely be overwritten by subclass."""
|
||||
pass
|
||||
|
||||
def opExecute(self, obj):
|
||||
"""opExecute(obj) ... called whenever the receiver needs to be recalculated.
|
||||
See documentation of execute() for a list of base functionality provided.
|
||||
Should be overwritten by subclasses."""
|
||||
pass
|
||||
|
||||
def opRejectAddBase(self, obj, base, sub):
|
||||
"""opRejectAddBase(base, sub) ... if op returns True the addition of the feature is prevented.
|
||||
Should be overwritten by subclasses."""
|
||||
return False
|
||||
|
||||
def onChanged(self, obj, prop):
|
||||
"""onChanged(obj, prop) ... base implementation of the FC notification framework.
|
||||
Do not overwrite, overwrite opOnChanged() instead."""
|
||||
|
||||
# there's a bit of cycle going on here, if sanitizeBase causes the transaction to
|
||||
# be cancelled we end right here again with the unsainitized Base - if that is the
|
||||
# case, stop the cycle and return immediately
|
||||
if prop == "Base" and self.sanitizeBase(obj):
|
||||
return
|
||||
|
||||
if "Restore" not in obj.State and prop in ["Base", "StartDepth", "FinalDepth"]:
|
||||
self.updateDepths(obj, True)
|
||||
|
||||
self.opOnChanged(obj, prop)
|
||||
|
||||
def applyExpression(self, obj, prop, expr):
|
||||
"""applyExpression(obj, prop, expr) ... set expression expr on obj.prop if expr is set"""
|
||||
if expr:
|
||||
obj.setExpression(prop, expr)
|
||||
return True
|
||||
return False
|
||||
|
||||
def setDefaultValues(self, obj):
|
||||
"""setDefaultValues(obj) ... base implementation.
|
||||
Do not overwrite, overwrite opSetDefaultValues() instead."""
|
||||
if self.job:
|
||||
job = self.job
|
||||
else:
|
||||
job = PathUtils.addToJob(obj)
|
||||
|
||||
obj.Active = True
|
||||
|
||||
features = self.opFeatures(obj)
|
||||
|
||||
if FeatureTool & features:
|
||||
if 1 < len(job.Operations.Group):
|
||||
obj.ToolController = PathUtil.toolControllerForOp(
|
||||
job.Operations.Group[-2]
|
||||
)
|
||||
else:
|
||||
obj.ToolController = PathUtils.findToolController(obj, self)
|
||||
if not obj.ToolController:
|
||||
raise PathNoTCException()
|
||||
obj.OpToolDiameter = obj.ToolController.Tool.Diameter
|
||||
|
||||
if FeatureCoolant & features:
|
||||
Path.Log.track()
|
||||
Path.Log.debug(obj.getEnumerationsOfProperty("CoolantMode"))
|
||||
obj.CoolantMode = job.SetupSheet.CoolantMode
|
||||
|
||||
if FeatureDepths & features:
|
||||
if self.applyExpression(
|
||||
obj, "StartDepth", job.SetupSheet.StartDepthExpression
|
||||
):
|
||||
obj.OpStartDepth = 1.0
|
||||
else:
|
||||
obj.StartDepth = 1.0
|
||||
if self.applyExpression(
|
||||
obj, "FinalDepth", job.SetupSheet.FinalDepthExpression
|
||||
):
|
||||
obj.OpFinalDepth = 0.0
|
||||
else:
|
||||
obj.FinalDepth = 0.0
|
||||
else:
|
||||
obj.StartDepth = 1.0
|
||||
|
||||
if FeatureStepDown & features:
|
||||
if not self.applyExpression(
|
||||
obj, "StepDown", job.SetupSheet.StepDownExpression
|
||||
):
|
||||
obj.StepDown = "1 mm"
|
||||
|
||||
if FeatureHeights & features:
|
||||
if job.SetupSheet.SafeHeightExpression:
|
||||
if not self.applyExpression(
|
||||
obj, "SafeHeight", job.SetupSheet.SafeHeightExpression
|
||||
):
|
||||
obj.SafeHeight = "3 mm"
|
||||
if job.SetupSheet.ClearanceHeightExpression:
|
||||
if not self.applyExpression(
|
||||
obj, "ClearanceHeight", job.SetupSheet.ClearanceHeightExpression
|
||||
):
|
||||
obj.ClearanceHeight = "5 mm"
|
||||
|
||||
if FeatureDiameters & features:
|
||||
obj.MinDiameter = "0 mm"
|
||||
obj.MaxDiameter = "0 mm"
|
||||
if job.Stock:
|
||||
obj.MaxDiameter = job.Stock.Shape.BoundBox.XLength
|
||||
|
||||
if FeatureStartPoint & features:
|
||||
obj.UseStartPoint = False
|
||||
|
||||
self.opSetDefaultValues(obj, job)
|
||||
return job
|
||||
|
||||
def _setBaseAndStock(self, obj, ignoreErrors=False):
|
||||
job = PathUtils.findParentJob(obj)
|
||||
|
||||
if not job:
|
||||
if not ignoreErrors:
|
||||
Path.Log.error(translate("Path", "No parent job found for operation."))
|
||||
return False
|
||||
if not job.Model.Group:
|
||||
if not ignoreErrors:
|
||||
Path.Log.error(
|
||||
translate("Path", "Parent job %s doesn't have a base object")
|
||||
% job.Label
|
||||
)
|
||||
return False
|
||||
self.job = job
|
||||
self.model = job.Model.Group
|
||||
self.stock = job.Stock
|
||||
return True
|
||||
|
||||
def getJob(self, obj):
|
||||
"""getJob(obj) ... return the job this operation is part of."""
|
||||
if not hasattr(self, "job") or self.job is None:
|
||||
if not self._setBaseAndStock(obj):
|
||||
return None
|
||||
return self.job
|
||||
|
||||
def updateDepths(self, obj, ignoreErrors=False):
|
||||
"""updateDepths(obj) ... base implementation calculating depths depending on base geometry.
|
||||
Should not be overwritten."""
|
||||
|
||||
def faceZmin(bb, fbb):
|
||||
if fbb.ZMax == fbb.ZMin and fbb.ZMax == bb.ZMax: # top face
|
||||
return fbb.ZMin
|
||||
elif fbb.ZMax > fbb.ZMin and fbb.ZMax == bb.ZMax: # vertical face, full cut
|
||||
return fbb.ZMin
|
||||
elif fbb.ZMax > fbb.ZMin and fbb.ZMin > bb.ZMin: # internal vertical wall
|
||||
return fbb.ZMin
|
||||
elif fbb.ZMax == fbb.ZMin and fbb.ZMax > bb.ZMin: # face/shelf
|
||||
return fbb.ZMin
|
||||
return bb.ZMin
|
||||
|
||||
if not self._setBaseAndStock(obj, ignoreErrors):
|
||||
return False
|
||||
|
||||
stockBB = self.stock.Shape.BoundBox
|
||||
zmin = stockBB.ZMin
|
||||
zmax = stockBB.ZMax
|
||||
|
||||
obj.OpStockZMin = zmin
|
||||
obj.OpStockZMax = zmax
|
||||
|
||||
if hasattr(obj, "Base") and obj.Base:
|
||||
for base, sublist in obj.Base:
|
||||
bb = base.Shape.BoundBox
|
||||
zmax = max(zmax, bb.ZMax)
|
||||
for sub in sublist:
|
||||
try:
|
||||
if sub:
|
||||
fbb = base.Shape.getElement(sub).BoundBox
|
||||
else:
|
||||
fbb = base.Shape.BoundBox
|
||||
zmin = max(zmin, faceZmin(bb, fbb))
|
||||
zmax = max(zmax, fbb.ZMax)
|
||||
except Part.OCCError as e:
|
||||
Path.Log.error(e)
|
||||
|
||||
else:
|
||||
# clearing with stock boundaries
|
||||
job = PathUtils.findParentJob(obj)
|
||||
zmax = stockBB.ZMax
|
||||
zmin = job.Proxy.modelBoundBox(job).ZMax
|
||||
|
||||
if FeatureDepths & self.opFeatures(obj):
|
||||
# first set update final depth, it's value is not negotiable
|
||||
if not PathGeom.isRoughly(obj.OpFinalDepth.Value, zmin):
|
||||
obj.OpFinalDepth = zmin
|
||||
zmin = obj.OpFinalDepth.Value
|
||||
|
||||
def minZmax(z):
|
||||
if hasattr(obj, "StepDown") and not PathGeom.isRoughly(
|
||||
obj.StepDown.Value, 0
|
||||
):
|
||||
return z + obj.StepDown.Value
|
||||
else:
|
||||
return z + 1
|
||||
|
||||
# ensure zmax is higher than zmin
|
||||
if (zmax - 0.0001) <= zmin:
|
||||
zmax = minZmax(zmin)
|
||||
|
||||
# update start depth if requested and required
|
||||
if not PathGeom.isRoughly(obj.OpStartDepth.Value, zmax):
|
||||
obj.OpStartDepth = zmax
|
||||
else:
|
||||
# every obj has a StartDepth
|
||||
if obj.StartDepth.Value != zmax:
|
||||
obj.StartDepth = zmax
|
||||
|
||||
self.opUpdateDepths(obj)
|
||||
|
||||
def sanitizeBase(self, obj):
|
||||
"""sanitizeBase(obj) ... check if Base is valid and clear on errors."""
|
||||
if hasattr(obj, "Base"):
|
||||
try:
|
||||
for (o, sublist) in obj.Base:
|
||||
for sub in sublist:
|
||||
o.Shape.getElement(sub)
|
||||
except Part.OCCError:
|
||||
Path.Log.error(
|
||||
"{} - stale base geometry detected - clearing.".format(obj.Label)
|
||||
)
|
||||
obj.Base = []
|
||||
return True
|
||||
return False
|
||||
|
||||
@waiting_effects
|
||||
def execute(self, obj):
|
||||
"""execute(obj) ... base implementation - do not overwrite!
|
||||
Verifies that the operation is assigned to a job and that the job also has a valid Base.
|
||||
It also sets the following instance variables that can and should be safely be used by
|
||||
implementation of opExecute():
|
||||
self.model ... List of base objects of the Job itself
|
||||
self.stock ... Stock object for the Job itself
|
||||
self.vertFeed ... vertical feed rate of assigned tool
|
||||
self.vertRapid ... vertical rapid rate of assigned tool
|
||||
self.horizFeed ... horizontal feed rate of assigned tool
|
||||
self.horizRapid ... norizontal rapid rate of assigned tool
|
||||
self.tool ... the actual tool being used
|
||||
self.radius ... the main radius of the tool being used
|
||||
self.commandlist ... a list for collecting all commands produced by the operation
|
||||
|
||||
Once everything is validated and above variables are set the implementation calls
|
||||
opExecute(obj) - which is expected to add the generated commands to self.commandlist
|
||||
Finally the base implementation adds a rapid move to clearance height and assigns
|
||||
the receiver's Path property from the command list.
|
||||
"""
|
||||
Path.Log.track()
|
||||
|
||||
if not obj.Active:
|
||||
path = Path.Path("(inactive operation)")
|
||||
obj.Path = path
|
||||
return
|
||||
|
||||
if not self._setBaseAndStock(obj):
|
||||
return
|
||||
|
||||
# make sure Base is still valid or clear it
|
||||
self.sanitizeBase(obj)
|
||||
|
||||
if FeatureTool & self.opFeatures(obj):
|
||||
tc = obj.ToolController
|
||||
if tc is None or tc.ToolNumber == 0:
|
||||
Path.Log.error(
|
||||
translate(
|
||||
"Path",
|
||||
"No Tool Controller is selected. We need a tool to build a Path.",
|
||||
)
|
||||
)
|
||||
return
|
||||
else:
|
||||
self.vertFeed = tc.VertFeed.Value
|
||||
self.horizFeed = tc.HorizFeed.Value
|
||||
self.vertRapid = tc.VertRapid.Value
|
||||
self.horizRapid = tc.HorizRapid.Value
|
||||
tool = tc.Proxy.getTool(tc)
|
||||
if not tool or float(tool.Diameter) == 0:
|
||||
Path.Log.error(
|
||||
translate(
|
||||
"Path",
|
||||
"No Tool found or diameter is zero. We need a tool to build a Path.",
|
||||
)
|
||||
)
|
||||
return
|
||||
self.radius = float(tool.Diameter) / 2.0
|
||||
self.tool = tool
|
||||
obj.OpToolDiameter = tool.Diameter
|
||||
|
||||
self.updateDepths(obj)
|
||||
# now that all op values are set make sure the user properties get updated accordingly,
|
||||
# in case they still have an expression referencing any op values
|
||||
obj.recompute()
|
||||
|
||||
self.commandlist = []
|
||||
self.commandlist.append(Path.Command("(%s)" % obj.Label))
|
||||
if obj.Comment:
|
||||
self.commandlist.append(Path.Command("(%s)" % obj.Comment))
|
||||
|
||||
result = self.opExecute(obj)
|
||||
|
||||
if self.commandlist and (FeatureHeights & self.opFeatures(obj)):
|
||||
# Let's finish by rapid to clearance...just for safety
|
||||
self.commandlist.append(
|
||||
Path.Command("G0", {"Z": obj.ClearanceHeight.Value})
|
||||
)
|
||||
|
||||
path = Path.Path(self.commandlist)
|
||||
obj.Path = path
|
||||
obj.CycleTime = self.getCycleTimeEstimate(obj)
|
||||
self.job.Proxy.getCycleTime()
|
||||
return result
|
||||
|
||||
def getCycleTimeEstimate(self, obj):
|
||||
|
||||
tc = obj.ToolController
|
||||
|
||||
if tc is None or tc.ToolNumber == 0:
|
||||
Path.Log.error(translate("Path", "No Tool Controller selected."))
|
||||
return translate("Path", "Tool Error")
|
||||
|
||||
hFeedrate = tc.HorizFeed.Value
|
||||
vFeedrate = tc.VertFeed.Value
|
||||
hRapidrate = tc.HorizRapid.Value
|
||||
vRapidrate = tc.VertRapid.Value
|
||||
|
||||
if (
|
||||
hFeedrate == 0 or vFeedrate == 0
|
||||
) and not PathPreferences.suppressAllSpeedsWarning():
|
||||
Path.Log.warning(
|
||||
translate(
|
||||
"Path",
|
||||
"Tool Controller feedrates required to calculate the cycle time.",
|
||||
)
|
||||
)
|
||||
return translate("Path", "Feedrate Error")
|
||||
|
||||
if (
|
||||
hRapidrate == 0 or vRapidrate == 0
|
||||
) and not PathPreferences.suppressRapidSpeedsWarning():
|
||||
Path.Log.warning(
|
||||
translate(
|
||||
"Path",
|
||||
"Add Tool Controller Rapid Speeds on the SetupSheet for more accurate cycle times.",
|
||||
)
|
||||
)
|
||||
|
||||
# Get the cycle time in seconds
|
||||
seconds = obj.Path.getCycleTime(hFeedrate, vFeedrate, hRapidrate, vRapidrate)
|
||||
|
||||
if not seconds or math.isnan(seconds):
|
||||
return translate("Path", "Cycletime Error")
|
||||
|
||||
# Convert the cycle time to a HH:MM:SS format
|
||||
cycleTime = time.strftime("%H:%M:%S", time.gmtime(seconds))
|
||||
|
||||
return cycleTime
|
||||
|
||||
def addBase(self, obj, base, sub):
|
||||
Path.Log.track(obj, base, sub)
|
||||
base = PathUtil.getPublicObject(base)
|
||||
|
||||
if self._setBaseAndStock(obj):
|
||||
for model in self.job.Model.Group:
|
||||
if base == self.job.Proxy.baseObject(self.job, model):
|
||||
base = model
|
||||
break
|
||||
|
||||
baselist = obj.Base
|
||||
if baselist is None:
|
||||
baselist = []
|
||||
|
||||
for p, el in baselist:
|
||||
if p == base and sub in el:
|
||||
Path.Log.notice(
|
||||
(
|
||||
translate("Path", "Base object %s.%s already in the list")
|
||||
+ "\n"
|
||||
)
|
||||
% (base.Label, sub)
|
||||
)
|
||||
return
|
||||
|
||||
if not self.opRejectAddBase(obj, base, sub):
|
||||
baselist.append((base, sub))
|
||||
obj.Base = baselist
|
||||
else:
|
||||
Path.Log.notice(
|
||||
(
|
||||
translate("Path", "Base object %s.%s rejected by operation")
|
||||
+ "\n"
|
||||
)
|
||||
% (base.Label, sub)
|
||||
)
|
||||
|
||||
def isToolSupported(self, obj, tool):
|
||||
"""toolSupported(obj, tool) ... Returns true if the op supports the given tool.
|
||||
This function can safely be overwritten by subclasses."""
|
||||
|
||||
return True
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,429 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2018 sliptonic <shopinthewoods@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 *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathGeom as PathGeom
|
||||
import math
|
||||
|
||||
# lazily loaded modules
|
||||
from lazy_loader.lazy_loader import LazyLoader
|
||||
|
||||
Part = LazyLoader("Part", globals(), "Part")
|
||||
|
||||
__title__ = "PathOpTools - Tools for Path operations."
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Collection of functions used by various Path operations. The functions are specific to Path and the algorithms employed by Path's operations."
|
||||
|
||||
|
||||
PrintWireDebug = False
|
||||
|
||||
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 debugEdge(label, e):
|
||||
"""debugEdge(label, e) ... prints a python statement to create e
|
||||
Currently lines and arcs are supported."""
|
||||
if not PrintWireDebug:
|
||||
return
|
||||
p0 = e.valueAt(e.FirstParameter)
|
||||
p1 = e.valueAt(e.LastParameter)
|
||||
if Part.Line == type(e.Curve):
|
||||
print(
|
||||
"%s Part.makeLine((%.2f, %.2f, %.2f), (%.2f, %.2f, %.2f))"
|
||||
% (label, p0.x, p0.y, p0.z, p1.x, p1.y, p1.z)
|
||||
)
|
||||
elif Part.Circle == type(e.Curve):
|
||||
r = e.Curve.Radius
|
||||
c = e.Curve.Center
|
||||
a = e.Curve.Axis
|
||||
xu = e.Curve.AngleXU
|
||||
if a.z < 0:
|
||||
first = math.degrees(xu - e.FirstParameter)
|
||||
else:
|
||||
first = math.degrees(xu + e.FirstParameter)
|
||||
last = first + math.degrees(e.LastParameter - e.FirstParameter)
|
||||
print(
|
||||
"%s Part.makeCircle(%.2f, App.Vector(%.2f, %.2f, %.2f), App.Vector(%.2f, %.2f, %.2f), %.2f, %.2f)"
|
||||
% (label, r, c.x, c.y, c.z, a.x, a.y, a.z, first, last)
|
||||
)
|
||||
else:
|
||||
print(
|
||||
"%s %s (%.2f, %.2f, %.2f) -> (%.2f, %.2f, %.2f)"
|
||||
% (label, type(e.Curve).__name__, p0.x, p0.y, p0.z, p1.x, p1.y, p1.z)
|
||||
)
|
||||
|
||||
|
||||
def makeWires(inEdges):
|
||||
"""makeWires ... function to make non-forking wires from a collection of edges"""
|
||||
edgelists = Part.sortEdges(inEdges)
|
||||
result = [Part.Wire(e) for e in edgelists]
|
||||
return result
|
||||
|
||||
|
||||
def debugWire(label, w):
|
||||
"""debugWire(label, w) ... prints python statements for all edges of w to be added to the object tree in a group."""
|
||||
if not PrintWireDebug:
|
||||
return
|
||||
print("#%s wire >>>>>>>>>>>>>>>>>>>>>>>>" % label)
|
||||
print(
|
||||
"grp = FreeCAD.ActiveDocument.addObject('App::DocumentObjectGroup', '%s')"
|
||||
% label
|
||||
)
|
||||
for i, e in enumerate(w.Edges):
|
||||
edge = "%s_e%d" % (label, i)
|
||||
debugEdge("%s = " % edge, e)
|
||||
print("Part.show(%s, '%s')" % (edge, edge))
|
||||
print("grp.addObject(FreeCAD.ActiveDocument.ActiveObject)")
|
||||
print("#%s wire <<<<<<<<<<<<<<<<<<<<<<<<" % label)
|
||||
|
||||
|
||||
def _orientEdges(inEdges):
|
||||
"""_orientEdges(inEdges) ... internal worker function to orient edges so the last vertex of one edge connects to the first vertex of the next edge.
|
||||
Assumes the edges are in an order so they can be connected."""
|
||||
Path.Log.track()
|
||||
# orient all edges of the wire so each edge's last value connects to the next edge's first value
|
||||
e0 = inEdges[0]
|
||||
# well, even the very first edge could be misoriented, so let's try and connect it to the second
|
||||
if 1 < len(inEdges):
|
||||
last = e0.valueAt(e0.LastParameter)
|
||||
e1 = inEdges[1]
|
||||
if not PathGeom.pointsCoincide(
|
||||
last, e1.valueAt(e1.FirstParameter)
|
||||
) and not PathGeom.pointsCoincide(last, e1.valueAt(e1.LastParameter)):
|
||||
debugEdge("# _orientEdges - flip first", e0)
|
||||
e0 = PathGeom.flipEdge(e0)
|
||||
|
||||
edges = [e0]
|
||||
last = e0.valueAt(e0.LastParameter)
|
||||
for e in inEdges[1:]:
|
||||
edge = (
|
||||
e
|
||||
if PathGeom.pointsCoincide(last, e.valueAt(e.FirstParameter))
|
||||
else PathGeom.flipEdge(e)
|
||||
)
|
||||
edges.append(edge)
|
||||
last = edge.valueAt(edge.LastParameter)
|
||||
return edges
|
||||
|
||||
|
||||
def _isWireClockwise(w):
|
||||
"""_isWireClockwise(w) ... return True if wire is oriented clockwise.
|
||||
Assumes the edges of w are already properly oriented - for generic access use isWireClockwise(w)."""
|
||||
# handle wires consisting of a single circle or 2 edges where one is an arc.
|
||||
# in both cases, because the edges are expected to be oriented correctly, the orientation can be
|
||||
# determined by looking at (one of) the circle curves.
|
||||
if 2 >= len(w.Edges) and Part.Circle == type(w.Edges[0].Curve):
|
||||
return 0 > w.Edges[0].Curve.Axis.z
|
||||
if 2 == len(w.Edges) and Part.Circle == type(w.Edges[1].Curve):
|
||||
return 0 > w.Edges[1].Curve.Axis.z
|
||||
|
||||
# for all other wires we presume they are polygonial and refer to Gauss
|
||||
# https://en.wikipedia.org/wiki/Shoelace_formula
|
||||
area = 0
|
||||
for e in w.Edges:
|
||||
v0 = e.valueAt(e.FirstParameter)
|
||||
v1 = e.valueAt(e.LastParameter)
|
||||
area = area + (v0.x * v1.y - v1.x * v0.y)
|
||||
Path.Log.track(area)
|
||||
return area < 0
|
||||
|
||||
|
||||
def isWireClockwise(w):
|
||||
"""isWireClockwise(w) ... returns True if the wire winds clockwise."""
|
||||
return _isWireClockwise(Part.Wire(_orientEdges(w.Edges)))
|
||||
|
||||
|
||||
def orientWire(w, forward=True):
|
||||
"""orientWire(w, forward=True) ... orients given wire in a specific direction.
|
||||
If forward = True (the default) the wire is oriented clockwise, looking down the negative Z axis.
|
||||
If forward = False the wire is oriented counter clockwise.
|
||||
If forward = None the orientation is determined by the order in which the edges appear in the wire."""
|
||||
Path.Log.debug("orienting forward: {}: {} edges".format(forward, len(w.Edges)))
|
||||
wire = Part.Wire(_orientEdges(w.Edges))
|
||||
if forward is not None:
|
||||
if forward != _isWireClockwise(wire):
|
||||
Path.Log.track("orientWire - needs flipping")
|
||||
return PathGeom.flipWire(wire)
|
||||
Path.Log.track("orientWire - ok")
|
||||
return wire
|
||||
|
||||
|
||||
def offsetWire(wire, base, offset, forward, Side=None):
|
||||
"""offsetWire(wire, base, offset, forward) ... offsets the wire away from base and orients the wire accordingly.
|
||||
The function tries to avoid most of the pitfalls of Part.makeOffset2D which is possible because all offsetting
|
||||
happens in the XY plane.
|
||||
"""
|
||||
Path.Log.track("offsetWire")
|
||||
|
||||
if 1 == len(wire.Edges):
|
||||
edge = wire.Edges[0]
|
||||
curve = edge.Curve
|
||||
if Part.Circle == type(curve) and wire.isClosed():
|
||||
# it's a full circle and there are some problems with that, see
|
||||
# https://www.freecadweb.org/wiki/Part%20Offset2D
|
||||
# it's easy to construct them manually though
|
||||
z = -1 if forward else 1
|
||||
new_edge = Part.makeCircle(
|
||||
curve.Radius + offset, curve.Center, FreeCAD.Vector(0, 0, z)
|
||||
)
|
||||
if base.isInside(new_edge.Vertexes[0].Point, offset / 2, True):
|
||||
if offset > curve.Radius or PathGeom.isRoughly(offset, curve.Radius):
|
||||
# offsetting a hole by its own radius (or more) makes the hole vanish
|
||||
return None
|
||||
if Side:
|
||||
Side[0] = "Inside"
|
||||
print("inside")
|
||||
new_edge = Part.makeCircle(
|
||||
curve.Radius - offset, curve.Center, FreeCAD.Vector(0, 0, -z)
|
||||
)
|
||||
|
||||
return Part.Wire([new_edge])
|
||||
|
||||
if Part.Circle == type(curve) and not wire.isClosed():
|
||||
# Process arc segment
|
||||
z = -1 if forward else 1
|
||||
l1 = math.sqrt(
|
||||
(edge.Vertexes[0].Point.x - curve.Center.x) ** 2
|
||||
+ (edge.Vertexes[0].Point.y - curve.Center.y) ** 2
|
||||
)
|
||||
l2 = math.sqrt(
|
||||
(edge.Vertexes[1].Point.x - curve.Center.x) ** 2
|
||||
+ (edge.Vertexes[1].Point.y - curve.Center.y) ** 2
|
||||
)
|
||||
|
||||
# Calculate angles based on x-axis (0 - PI/2)
|
||||
start_angle = math.acos((edge.Vertexes[0].Point.x - curve.Center.x) / l1)
|
||||
end_angle = math.acos((edge.Vertexes[1].Point.x - curve.Center.x) / l2)
|
||||
|
||||
# Angles are based on x-axis (Mirrored on x-axis) -> negative y value means negative angle
|
||||
if edge.Vertexes[0].Point.y < curve.Center.y:
|
||||
start_angle *= -1
|
||||
if edge.Vertexes[1].Point.y < curve.Center.y:
|
||||
end_angle *= -1
|
||||
|
||||
if (
|
||||
edge.Vertexes[0].Point.x > curve.Center.x
|
||||
or edge.Vertexes[1].Point.x > curve.Center.x
|
||||
) and curve.AngleXU < 0:
|
||||
tmp = start_angle
|
||||
start_angle = end_angle
|
||||
end_angle = tmp
|
||||
|
||||
# Inside / Outside
|
||||
if base.isInside(edge.Vertexes[0].Point, offset / 2, True):
|
||||
offset *= -1
|
||||
if Side:
|
||||
Side[0] = "Inside"
|
||||
|
||||
# Create new arc
|
||||
if curve.AngleXU > 0:
|
||||
edge = Part.ArcOfCircle(
|
||||
Part.Circle(
|
||||
curve.Center, FreeCAD.Vector(0, 0, 1), curve.Radius + offset
|
||||
),
|
||||
start_angle,
|
||||
end_angle,
|
||||
).toShape()
|
||||
else:
|
||||
edge = Part.ArcOfCircle(
|
||||
Part.Circle(
|
||||
curve.Center, FreeCAD.Vector(0, 0, 1), curve.Radius - offset
|
||||
),
|
||||
start_angle,
|
||||
end_angle,
|
||||
).toShape()
|
||||
|
||||
return Part.Wire([edge])
|
||||
|
||||
if Part.Line == type(curve) or Part.LineSegment == type(curve):
|
||||
# offsetting a single edge doesn't work because there is an infinite
|
||||
# possible planes into which the edge could be offset
|
||||
# luckily, the plane here must be the XY-plane ...
|
||||
p0 = edge.Vertexes[0].Point
|
||||
v0 = edge.Vertexes[1].Point - p0
|
||||
n = v0.cross(FreeCAD.Vector(0, 0, 1))
|
||||
o = n.normalize() * offset
|
||||
edge.translate(o)
|
||||
|
||||
# offset edde the other way if the result is inside
|
||||
if base.isInside(
|
||||
edge.valueAt((edge.FirstParameter + edge.LastParameter) / 2),
|
||||
offset / 2,
|
||||
True,
|
||||
):
|
||||
edge.translate(-2 * o)
|
||||
|
||||
# flip the edge if it's not on the right side of the original edge
|
||||
if forward is not None:
|
||||
v1 = edge.Vertexes[1].Point - p0
|
||||
left = PathGeom.Side.Left == PathGeom.Side.of(v0, v1)
|
||||
if left != forward:
|
||||
edge = PathGeom.flipEdge(edge)
|
||||
return Part.Wire([edge])
|
||||
|
||||
# if we get to this point the assumption is that makeOffset2D can deal with the edge
|
||||
|
||||
owire = orientWire(wire.makeOffset2D(offset), True)
|
||||
debugWire("makeOffset2D_%d" % len(wire.Edges), owire)
|
||||
|
||||
if wire.isClosed():
|
||||
if not base.isInside(owire.Edges[0].Vertexes[0].Point, offset / 2, True):
|
||||
Path.Log.track("closed - outside")
|
||||
if Side:
|
||||
Side[0] = "Outside"
|
||||
return orientWire(owire, forward)
|
||||
Path.Log.track("closed - inside")
|
||||
if Side:
|
||||
Side[0] = "Inside"
|
||||
try:
|
||||
owire = wire.makeOffset2D(-offset)
|
||||
except Exception:
|
||||
# most likely offsetting didn't work because the wire is a hole
|
||||
# and the offset is too big - making the hole vanish
|
||||
return None
|
||||
# For negative offsets (holes) 'forward' is the other way
|
||||
if forward is None:
|
||||
return orientWire(owire, None)
|
||||
return orientWire(owire, not forward)
|
||||
|
||||
# An edge is considered to be inside of shape if the mid point is inside
|
||||
# Of the remaining edges we take the longest wire to be the engraving side
|
||||
# Looking for a circle with the start vertex as center marks and end
|
||||
# starting from there follow the edges until a circle with the end vertex as center is found
|
||||
# if the traversed edges include any of the remaining from above, all those edges are remaining
|
||||
# this is to also include edges which might partially be inside shape
|
||||
# if they need to be discarded, split, that should happen in a post process
|
||||
# Depending on the Axis of the circle, and which side remains we know if the wire needs to be flipped
|
||||
|
||||
# first, let's make sure all edges are oriented the proper way
|
||||
edges = _orientEdges(wire.Edges)
|
||||
|
||||
# determine the start and end point
|
||||
start = edges[0].firstVertex().Point
|
||||
end = edges[-1].lastVertex().Point
|
||||
debugWire("wire", wire)
|
||||
debugWire("wedges", Part.Wire(edges))
|
||||
|
||||
# find edges that are not inside the shape
|
||||
common = base.common(owire)
|
||||
insideEndpoints = [e.lastVertex().Point for e in common.Edges]
|
||||
insideEndpoints.append(common.Edges[0].firstVertex().Point)
|
||||
|
||||
def isInside(edge):
|
||||
p0 = edge.firstVertex().Point
|
||||
p1 = edge.lastVertex().Point
|
||||
for p in insideEndpoints:
|
||||
if PathGeom.pointsCoincide(p, p0, 0.01) or PathGeom.pointsCoincide(
|
||||
p, p1, 0.01
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
outside = [e for e in owire.Edges if not isInside(e)]
|
||||
# discard all edges that are not part of the longest wire
|
||||
longestWire = None
|
||||
for w in [Part.Wire(el) for el in Part.sortEdges(outside)]:
|
||||
if not longestWire or longestWire.Length < w.Length:
|
||||
longestWire = w
|
||||
|
||||
debugWire("outside", Part.Wire(outside))
|
||||
debugWire("longest", longestWire)
|
||||
|
||||
def isCircleAt(edge, center):
|
||||
"""isCircleAt(edge, center) ... helper function returns True if edge is a circle at the given center."""
|
||||
if Part.Circle == type(edge.Curve) or Part.ArcOfCircle == type(edge.Curve):
|
||||
return PathGeom.pointsCoincide(edge.Curve.Center, center)
|
||||
return False
|
||||
|
||||
# split offset wire into edges to the left side and edges to the right side
|
||||
collectLeft = False
|
||||
collectRight = False
|
||||
leftSideEdges = []
|
||||
rightSideEdges = []
|
||||
|
||||
# traverse through all edges in order and start collecting them when we encounter
|
||||
# an end point (circle centered at one of the end points of the original wire).
|
||||
# should we come to an end point and determine that we've already collected the
|
||||
# next side, we're done
|
||||
for e in owire.Edges + owire.Edges:
|
||||
if isCircleAt(e, start):
|
||||
if PathGeom.pointsCoincide(e.Curve.Axis, FreeCAD.Vector(0, 0, 1)):
|
||||
if not collectLeft and leftSideEdges:
|
||||
break
|
||||
collectLeft = True
|
||||
collectRight = False
|
||||
else:
|
||||
if not collectRight and rightSideEdges:
|
||||
break
|
||||
collectLeft = False
|
||||
collectRight = True
|
||||
elif isCircleAt(e, end):
|
||||
if PathGeom.pointsCoincide(e.Curve.Axis, FreeCAD.Vector(0, 0, 1)):
|
||||
if not collectRight and rightSideEdges:
|
||||
break
|
||||
collectLeft = False
|
||||
collectRight = True
|
||||
else:
|
||||
if not collectLeft and leftSideEdges:
|
||||
break
|
||||
collectLeft = True
|
||||
collectRight = False
|
||||
elif collectLeft:
|
||||
leftSideEdges.append(e)
|
||||
elif collectRight:
|
||||
rightSideEdges.append(e)
|
||||
|
||||
debugWire("left", Part.Wire(leftSideEdges))
|
||||
debugWire("right", Part.Wire(rightSideEdges))
|
||||
|
||||
# figure out if all the left sided edges or the right sided edges are the ones
|
||||
# that are 'outside'. However, we return the full side.
|
||||
edges = leftSideEdges
|
||||
for e in longestWire.Edges:
|
||||
for e0 in rightSideEdges:
|
||||
if PathGeom.edgesMatch(e, e0):
|
||||
edges = rightSideEdges
|
||||
Path.Log.debug("#use right side edges")
|
||||
if not forward:
|
||||
Path.Log.debug("#reverse")
|
||||
edges.reverse()
|
||||
return orientWire(Part.Wire(edges), None)
|
||||
|
||||
# at this point we have the correct edges and they are in the order for forward
|
||||
# traversal (climb milling). If that's not what we want just reverse the order,
|
||||
# orientWire takes care of orienting the edges appropriately.
|
||||
Path.Log.debug("#use left side edges")
|
||||
if not forward:
|
||||
Path.Log.debug("#reverse")
|
||||
edges.reverse()
|
||||
|
||||
return orientWire(Part.Wire(edges), None)
|
||||
@@ -1,929 +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 *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
import FreeCAD
|
||||
import Part
|
||||
import Path
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathScripts.PathPocketBase as PathPocketBase
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
|
||||
# lazily loaded modules
|
||||
from lazy_loader.lazy_loader import LazyLoader
|
||||
|
||||
PathGeom = LazyLoader("PathScripts.PathGeom", globals(), "PathScripts.PathGeom")
|
||||
|
||||
__title__ = "Path 3D Pocket Operation"
|
||||
__author__ = "Yorik van Havre <yorik@uncreated.net>"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Class and implementation of the 3D Pocket operation."
|
||||
__created__ = "2014"
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class ObjectPocket(PathPocketBase.ObjectPocket):
|
||||
"""Proxy object for Pocket operation."""
|
||||
|
||||
def pocketOpFeatures(self, obj):
|
||||
return PathOp.FeatureNoFinalDepth
|
||||
|
||||
def initPocketOp(self, obj):
|
||||
"""initPocketOp(obj) ... setup receiver"""
|
||||
if not hasattr(obj, "HandleMultipleFeatures"):
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"HandleMultipleFeatures",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Choose how to process multiple Base Geometry features.",
|
||||
),
|
||||
)
|
||||
|
||||
if not hasattr(obj, "AdaptivePocketStart"):
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"AdaptivePocketStart",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Use adaptive algorithm to eliminate excessive air milling above planar pocket top.",
|
||||
),
|
||||
)
|
||||
if not hasattr(obj, "AdaptivePocketFinish"):
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"AdaptivePocketFinish",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Use adaptive algorithm to eliminate excessive air milling below planar pocket bottom.",
|
||||
),
|
||||
)
|
||||
if not hasattr(obj, "ProcessStockArea"):
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"ProcessStockArea",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Process the model and stock in an operation with no Base Geometry selected.",
|
||||
),
|
||||
)
|
||||
|
||||
# populate the property enumerations
|
||||
for n in self.propertyEnumerations():
|
||||
setattr(obj, n[0], n[1])
|
||||
|
||||
@classmethod
|
||||
def propertyEnumerations(self, dataType="data"):
|
||||
"""propertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
|
||||
Args:
|
||||
dataType = 'data', 'raw', 'translated'
|
||||
Notes:
|
||||
'data' is list of internal string literals used in code
|
||||
'raw' is list of (translated_text, data_string) tuples
|
||||
'translated' is list of translated string literals
|
||||
"""
|
||||
|
||||
enums = {
|
||||
"HandleMultipleFeatures": [
|
||||
(translate("Path_Pocket", "Collectively"), "Collectively"),
|
||||
(translate("Path_Pocket", "Individually"), "Individually"),
|
||||
],
|
||||
}
|
||||
|
||||
if dataType == "raw":
|
||||
return enums
|
||||
|
||||
data = list()
|
||||
idx = 0 if dataType == "translated" else 1
|
||||
|
||||
Path.Log.debug(enums)
|
||||
|
||||
for k, v in enumerate(enums):
|
||||
data.append((v, [tup[idx] for tup in enums[v]]))
|
||||
Path.Log.debug(data)
|
||||
|
||||
return data
|
||||
|
||||
def opOnDocumentRestored(self, obj):
|
||||
"""opOnDocumentRestored(obj) ... adds the properties if they doesn't exist."""
|
||||
self.initPocketOp(obj)
|
||||
|
||||
def pocketInvertExtraOffset(self):
|
||||
return False
|
||||
|
||||
def opUpdateDepths(self, obj):
|
||||
"""opUpdateDepths(obj) ... Implement special depths calculation."""
|
||||
# Set Final Depth to bottom of model if whole model is used
|
||||
if not obj.Base or len(obj.Base) == 0:
|
||||
if len(self.job.Model.Group) == 1:
|
||||
finDep = self.job.Model.Group[0].Shape.BoundBox.ZMin
|
||||
else:
|
||||
finDep = min([m.Shape.BoundBox.ZMin for m in self.job.Model.Group])
|
||||
obj.setExpression("OpFinalDepth", "{} mm".format(finDep))
|
||||
|
||||
def areaOpShapes(self, obj):
|
||||
"""areaOpShapes(obj) ... return shapes representing the solids to be removed."""
|
||||
Path.Log.track()
|
||||
|
||||
subObjTups = []
|
||||
removalshapes = []
|
||||
|
||||
if obj.Base:
|
||||
Path.Log.debug("base items exist. Processing... ")
|
||||
for base in obj.Base:
|
||||
Path.Log.debug("obj.Base item: {}".format(base))
|
||||
|
||||
# Check if all subs are faces
|
||||
allSubsFaceType = True
|
||||
Faces = []
|
||||
for sub in base[1]:
|
||||
if "Face" in sub:
|
||||
face = getattr(base[0].Shape, sub)
|
||||
Faces.append(face)
|
||||
subObjTups.append((sub, face))
|
||||
else:
|
||||
allSubsFaceType = False
|
||||
break
|
||||
|
||||
if len(Faces) == 0:
|
||||
allSubsFaceType = False
|
||||
|
||||
if (
|
||||
allSubsFaceType is True
|
||||
and obj.HandleMultipleFeatures == "Collectively"
|
||||
):
|
||||
(fzmin, fzmax) = self.getMinMaxOfFaces(Faces)
|
||||
if obj.FinalDepth.Value < fzmin:
|
||||
Path.Log.warning(
|
||||
translate(
|
||||
"PathPocket",
|
||||
"Final depth set below ZMin of face(s) selected.",
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
obj.AdaptivePocketStart is True
|
||||
or obj.AdaptivePocketFinish is True
|
||||
):
|
||||
pocketTup = self.calculateAdaptivePocket(obj, base, subObjTups)
|
||||
if pocketTup is not False:
|
||||
obj.removalshape = pocketTup[0]
|
||||
removalshapes.append(pocketTup) # (shape, isHole, detail)
|
||||
else:
|
||||
shape = Part.makeCompound(Faces)
|
||||
env = PathUtils.getEnvelope(
|
||||
base[0].Shape, subshape=shape, depthparams=self.depthparams
|
||||
)
|
||||
rawRemovalShape = env.cut(base[0].Shape)
|
||||
faceExtrusions = [
|
||||
f.extrude(FreeCAD.Vector(0.0, 0.0, 1.0)) for f in Faces
|
||||
]
|
||||
obj.removalshape = _identifyRemovalSolids(
|
||||
rawRemovalShape, faceExtrusions
|
||||
)
|
||||
removalshapes.append(
|
||||
(obj.removalshape, False, "3DPocket")
|
||||
) # (shape, isHole, detail)
|
||||
else:
|
||||
for sub in base[1]:
|
||||
if "Face" in sub:
|
||||
shape = Part.makeCompound([getattr(base[0].Shape, sub)])
|
||||
else:
|
||||
edges = [getattr(base[0].Shape, sub) for sub in base[1]]
|
||||
shape = Part.makeFace(edges, "Part::FaceMakerSimple")
|
||||
|
||||
env = PathUtils.getEnvelope(
|
||||
base[0].Shape, subshape=shape, depthparams=self.depthparams
|
||||
)
|
||||
rawRemovalShape = env.cut(base[0].Shape)
|
||||
faceExtrusions = [shape.extrude(FreeCAD.Vector(0.0, 0.0, 1.0))]
|
||||
obj.removalshape = _identifyRemovalSolids(
|
||||
rawRemovalShape, faceExtrusions
|
||||
)
|
||||
removalshapes.append((obj.removalshape, False, "3DPocket"))
|
||||
|
||||
else: # process the job base object as a whole
|
||||
Path.Log.debug("processing the whole job base object")
|
||||
for base in self.model:
|
||||
if obj.ProcessStockArea is True:
|
||||
job = PathUtils.findParentJob(obj)
|
||||
|
||||
stockEnvShape = PathUtils.getEnvelope(
|
||||
job.Stock.Shape, subshape=None, depthparams=self.depthparams
|
||||
)
|
||||
|
||||
rawRemovalShape = stockEnvShape.cut(base.Shape)
|
||||
else:
|
||||
env = PathUtils.getEnvelope(
|
||||
base.Shape, subshape=None, depthparams=self.depthparams
|
||||
)
|
||||
rawRemovalShape = env.cut(base.Shape)
|
||||
|
||||
# Identify target removal shapes after cutting envelope with base shape
|
||||
removalSolids = [
|
||||
s
|
||||
for s in rawRemovalShape.Solids
|
||||
if PathGeom.isRoughly(
|
||||
s.BoundBox.ZMax, rawRemovalShape.BoundBox.ZMax
|
||||
)
|
||||
]
|
||||
|
||||
# Fuse multiple solids
|
||||
if len(removalSolids) > 1:
|
||||
seed = removalSolids[0]
|
||||
for tt in removalSolids[1:]:
|
||||
fusion = seed.fuse(tt)
|
||||
seed = fusion
|
||||
removalShape = seed
|
||||
else:
|
||||
removalShape = removalSolids[0]
|
||||
|
||||
obj.removalshape = removalShape
|
||||
removalshapes.append((obj.removalshape, False, "3DPocket"))
|
||||
|
||||
return removalshapes
|
||||
|
||||
def areaOpSetDefaultValues(self, obj, job):
|
||||
"""areaOpSetDefaultValues(obj, job) ... set default values"""
|
||||
obj.StepOver = 100
|
||||
obj.ZigZagAngle = 45
|
||||
obj.HandleMultipleFeatures = "Collectively"
|
||||
obj.AdaptivePocketStart = False
|
||||
obj.AdaptivePocketFinish = False
|
||||
obj.ProcessStockArea = False
|
||||
|
||||
# methods for eliminating air milling with some pockets: adpative start and finish
|
||||
def calculateAdaptivePocket(self, obj, base, subObjTups):
|
||||
"""calculateAdaptivePocket(obj, base, subObjTups)
|
||||
Orient multiple faces around common facial center of mass.
|
||||
Identify edges that are connections for adjacent faces.
|
||||
Attempt to separate unconnected edges into top and bottom loops of the pocket.
|
||||
Trim the top and bottom of the pocket if available and requested.
|
||||
return: tuple with pocket shape information"""
|
||||
low = []
|
||||
high = []
|
||||
removeList = []
|
||||
Faces = []
|
||||
allEdges = []
|
||||
makeHighFace = 0
|
||||
tryNonPlanar = False
|
||||
isHighFacePlanar = True
|
||||
isLowFacePlanar = True
|
||||
|
||||
for (sub, face) in subObjTups:
|
||||
Faces.append(face)
|
||||
|
||||
# identify max and min face heights for top loop
|
||||
(zmin, zmax) = self.getMinMaxOfFaces(Faces)
|
||||
|
||||
# Order faces around common center of mass
|
||||
subObjTups = self.orderFacesAroundCenterOfMass(subObjTups)
|
||||
# find connected edges and map to edge names of base
|
||||
(connectedEdges, touching) = self.findSharedEdges(subObjTups)
|
||||
(low, high) = self.identifyUnconnectedEdges(subObjTups, touching)
|
||||
|
||||
if len(high) > 0 and obj.AdaptivePocketStart is True:
|
||||
# attempt planar face with top edges of pocket
|
||||
allEdges = []
|
||||
makeHighFace = 0
|
||||
tryNonPlanar = False
|
||||
for (sub, face, ei) in high:
|
||||
allEdges.append(face.Edges[ei])
|
||||
|
||||
(hzmin, hzmax) = self.getMinMaxOfFaces(allEdges)
|
||||
|
||||
try:
|
||||
highFaceShape = Part.Face(Part.Wire(Part.__sortEdges__(allEdges)))
|
||||
except Exception as ee:
|
||||
Path.Log.warning(ee)
|
||||
Path.Log.error(
|
||||
translate(
|
||||
"Path",
|
||||
"A planar adaptive start is unavailable. The non-planar will be attempted.",
|
||||
)
|
||||
)
|
||||
tryNonPlanar = True
|
||||
else:
|
||||
makeHighFace = 1
|
||||
|
||||
if tryNonPlanar is True:
|
||||
try:
|
||||
highFaceShape = Part.makeFilledFace(
|
||||
Part.__sortEdges__(allEdges)
|
||||
) # NON-planar face method
|
||||
except Exception as eee:
|
||||
Path.Log.warning(eee)
|
||||
Path.Log.error(
|
||||
translate(
|
||||
"Path", "The non-planar adaptive start is also unavailable."
|
||||
)
|
||||
+ "(1)"
|
||||
)
|
||||
isHighFacePlanar = False
|
||||
else:
|
||||
makeHighFace = 2
|
||||
|
||||
if makeHighFace > 0:
|
||||
FreeCAD.ActiveDocument.addObject("Part::Feature", "topEdgeFace")
|
||||
highFace = FreeCAD.ActiveDocument.ActiveObject
|
||||
highFace.Shape = highFaceShape
|
||||
removeList.append(highFace.Name)
|
||||
|
||||
# verify non-planar face is within high edge loop Z-boundaries
|
||||
if makeHighFace == 2:
|
||||
mx = hzmax + obj.StepDown.Value
|
||||
mn = hzmin - obj.StepDown.Value
|
||||
if (
|
||||
highFace.Shape.BoundBox.ZMax > mx
|
||||
or highFace.Shape.BoundBox.ZMin < mn
|
||||
):
|
||||
Path.Log.warning(
|
||||
"ZMaxDiff: {}; ZMinDiff: {}".format(
|
||||
highFace.Shape.BoundBox.ZMax - mx,
|
||||
highFace.Shape.BoundBox.ZMin - mn,
|
||||
)
|
||||
)
|
||||
Path.Log.error(
|
||||
translate(
|
||||
"Path", "The non-planar adaptive start is also unavailable."
|
||||
)
|
||||
+ "(2)"
|
||||
)
|
||||
isHighFacePlanar = False
|
||||
makeHighFace = 0
|
||||
else:
|
||||
isHighFacePlanar = False
|
||||
|
||||
if len(low) > 0 and obj.AdaptivePocketFinish is True:
|
||||
# attempt planar face with bottom edges of pocket
|
||||
allEdges = []
|
||||
for (sub, face, ei) in low:
|
||||
allEdges.append(face.Edges[ei])
|
||||
|
||||
# (lzmin, lzmax) = self.getMinMaxOfFaces(allEdges)
|
||||
|
||||
try:
|
||||
lowFaceShape = Part.Face(Part.Wire(Part.__sortEdges__(allEdges)))
|
||||
# lowFaceShape = Part.makeFilledFace(Part.__sortEdges__(allEdges)) # NON-planar face method
|
||||
except Exception as ee:
|
||||
Path.Log.error(ee)
|
||||
Path.Log.error("An adaptive finish is unavailable.")
|
||||
isLowFacePlanar = False
|
||||
else:
|
||||
FreeCAD.ActiveDocument.addObject("Part::Feature", "bottomEdgeFace")
|
||||
lowFace = FreeCAD.ActiveDocument.ActiveObject
|
||||
lowFace.Shape = lowFaceShape
|
||||
removeList.append(lowFace.Name)
|
||||
else:
|
||||
isLowFacePlanar = False
|
||||
|
||||
# Start with a regular pocket envelope
|
||||
strDep = obj.StartDepth.Value
|
||||
finDep = obj.FinalDepth.Value
|
||||
cuts = []
|
||||
starts = []
|
||||
finals = []
|
||||
starts.append(obj.StartDepth.Value)
|
||||
finals.append(zmin)
|
||||
if obj.AdaptivePocketStart is True or len(subObjTups) == 1:
|
||||
strDep = zmax + obj.StepDown.Value
|
||||
starts.append(zmax + obj.StepDown.Value)
|
||||
|
||||
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
||||
depthparams = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=strDep,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=finDep,
|
||||
user_depths=None,
|
||||
)
|
||||
shape = Part.makeCompound(Faces)
|
||||
env = PathUtils.getEnvelope(
|
||||
base[0].Shape, subshape=shape, depthparams=depthparams
|
||||
)
|
||||
cuts.append(env.cut(base[0].Shape))
|
||||
|
||||
# Might need to change to .cut(job.Stock.Shape) if pocket has no bottom
|
||||
# job = PathUtils.findParentJob(obj)
|
||||
# envBody = env.cut(job.Stock.Shape)
|
||||
|
||||
if isHighFacePlanar is True and len(subObjTups) > 1:
|
||||
starts.append(hzmax + obj.StepDown.Value)
|
||||
# make shape to trim top of reg pocket
|
||||
strDep1 = obj.StartDepth.Value + (hzmax - hzmin)
|
||||
if makeHighFace == 1:
|
||||
# Planar face
|
||||
finDep1 = highFace.Shape.BoundBox.ZMin + obj.StepDown.Value
|
||||
else:
|
||||
# Non-Planar face
|
||||
finDep1 = hzmin + obj.StepDown.Value
|
||||
depthparams1 = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=strDep1,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=finDep1,
|
||||
user_depths=None,
|
||||
)
|
||||
envTop = PathUtils.getEnvelope(
|
||||
base[0].Shape, subshape=highFace.Shape, depthparams=depthparams1
|
||||
)
|
||||
cbi = len(cuts) - 1
|
||||
cuts.append(cuts[cbi].cut(envTop))
|
||||
|
||||
if isLowFacePlanar is True and len(subObjTups) > 1:
|
||||
# make shape to trim top of pocket
|
||||
if makeHighFace == 1:
|
||||
# Planar face
|
||||
strDep2 = lowFace.Shape.BoundBox.ZMax
|
||||
else:
|
||||
# Non-Planar face
|
||||
strDep2 = hzmax
|
||||
finDep2 = obj.FinalDepth.Value
|
||||
depthparams2 = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=strDep2,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=finDep2,
|
||||
user_depths=None,
|
||||
)
|
||||
envBottom = PathUtils.getEnvelope(
|
||||
base[0].Shape, subshape=lowFace.Shape, depthparams=depthparams2
|
||||
)
|
||||
cbi = len(cuts) - 1
|
||||
cuts.append(cuts[cbi].cut(envBottom))
|
||||
|
||||
# package pocket details into tuple
|
||||
cbi = len(cuts) - 1
|
||||
pocket = (cuts[cbi], False, "3DPocket")
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
|
||||
for rn in removeList:
|
||||
FreeCADGui.ActiveDocument.getObject(rn).Visibility = False
|
||||
|
||||
for rn in removeList:
|
||||
FreeCAD.ActiveDocument.getObject(rn).purgeTouched()
|
||||
self.tempObjectNames.append(rn)
|
||||
return pocket
|
||||
|
||||
def orderFacesAroundCenterOfMass(self, subObjTups):
|
||||
"""orderFacesAroundCenterOfMass(subObjTups)
|
||||
Order list of faces by center of mass in angular order around
|
||||
average center of mass for all faces. Positive X-axis is zero degrees.
|
||||
return: subObjTups [ordered/sorted]"""
|
||||
import math
|
||||
|
||||
newList = []
|
||||
vectList = []
|
||||
comList = []
|
||||
sortList = []
|
||||
subCnt = 0
|
||||
sumCom = FreeCAD.Vector(0.0, 0.0, 0.0)
|
||||
avgCom = FreeCAD.Vector(0.0, 0.0, 0.0)
|
||||
|
||||
def getDrctn(vectItem):
|
||||
return vectItem[3]
|
||||
|
||||
def getFaceIdx(sub):
|
||||
return int(sub.replace("Face", "")) - 1
|
||||
|
||||
# get CenterOfMass for each face and add to sumCenterOfMass for average calculation
|
||||
for (sub, face) in subObjTups:
|
||||
# for (bsNm, fIdx, eIdx, vIdx) in bfevList:
|
||||
# face = FreeCAD.ActiveDocument.getObject(bsNm).Shape.Faces[fIdx]
|
||||
subCnt += 1
|
||||
com = face.CenterOfMass
|
||||
comList.append((sub, face, com))
|
||||
sumCom = sumCom.add(com) # add sub COM to sum
|
||||
|
||||
# Calculate average CenterOfMass for all faces combined
|
||||
avgCom.x = sumCom.x / subCnt
|
||||
avgCom.y = sumCom.y / subCnt
|
||||
avgCom.z = sumCom.z / subCnt
|
||||
|
||||
# calculate vector (mag, direct) for each face from avgCom
|
||||
for (sub, face, com) in comList:
|
||||
adjCom = com.sub(
|
||||
avgCom
|
||||
) # effectively treats avgCom as origin for each face.
|
||||
mag = math.sqrt(
|
||||
adjCom.x**2 + adjCom.y**2
|
||||
) # adjCom.Length without Z values
|
||||
drctn = 0.0
|
||||
# Determine direction of vector
|
||||
if adjCom.x > 0.0:
|
||||
if adjCom.y > 0.0: # Q1
|
||||
drctn = math.degrees(math.atan(adjCom.y / adjCom.x))
|
||||
elif adjCom.y < 0.0:
|
||||
drctn = -math.degrees(math.atan(adjCom.x / adjCom.y)) + 270.0
|
||||
elif adjCom.y == 0.0:
|
||||
drctn = 0.0
|
||||
elif adjCom.x < 0.0:
|
||||
if adjCom.y < 0.0:
|
||||
drctn = math.degrees(math.atan(adjCom.y / adjCom.x)) + 180.0
|
||||
elif adjCom.y > 0.0:
|
||||
drctn = -math.degrees(math.atan(adjCom.x / adjCom.y)) + 90.0
|
||||
elif adjCom.y == 0.0:
|
||||
drctn = 180.0
|
||||
elif adjCom.x == 0.0:
|
||||
if adjCom.y < 0.0:
|
||||
drctn = 270.0
|
||||
elif adjCom.y > 0.0:
|
||||
drctn = 90.0
|
||||
vectList.append((sub, face, mag, drctn))
|
||||
|
||||
# Sort faces by directional component of vector
|
||||
sortList = sorted(vectList, key=getDrctn)
|
||||
|
||||
# remove magnitute and direction values
|
||||
for (sub, face, mag, drctn) in sortList:
|
||||
newList.append((sub, face))
|
||||
|
||||
# Rotate list items so highest face is first
|
||||
zmax = newList[0][1].BoundBox.ZMax
|
||||
idx = 0
|
||||
for i in range(0, len(newList)):
|
||||
(sub, face) = newList[i]
|
||||
fIdx = getFaceIdx(sub)
|
||||
# face = FreeCAD.ActiveDocument.getObject(bsNm).Shape.Faces[fIdx]
|
||||
if face.BoundBox.ZMax > zmax:
|
||||
zmax = face.BoundBox.ZMax
|
||||
idx = i
|
||||
if face.BoundBox.ZMax == zmax:
|
||||
if fIdx < getFaceIdx(newList[idx][0]):
|
||||
idx = i
|
||||
if idx > 0:
|
||||
for z in range(0, idx):
|
||||
newList.append(newList.pop(0))
|
||||
|
||||
return newList
|
||||
|
||||
def findSharedEdges(self, subObjTups):
|
||||
"""findSharedEdges(self, subObjTups)
|
||||
Find connected edges given a group of faces"""
|
||||
checkoutList = []
|
||||
searchedList = []
|
||||
shared = []
|
||||
touching = {}
|
||||
touchingCleaned = {}
|
||||
|
||||
# Prepare dictionary for edges in shared
|
||||
for (sub, face) in subObjTups:
|
||||
touching[sub] = []
|
||||
|
||||
# prepare list of indexes as proxies for subObjTups items
|
||||
numFaces = len(subObjTups)
|
||||
for nf in range(0, numFaces):
|
||||
checkoutList.append(nf)
|
||||
|
||||
for co in range(0, len(checkoutList)):
|
||||
if len(checkoutList) < 2:
|
||||
break
|
||||
|
||||
# Checkout first sub for analysis
|
||||
checkedOut1 = checkoutList.pop()
|
||||
searchedList.append(checkedOut1)
|
||||
(sub1, face1) = subObjTups[checkedOut1]
|
||||
|
||||
# Compare checked out sub to others for shared
|
||||
for co in range(0, len(checkoutList)):
|
||||
# Checkout second sub for analysis
|
||||
(sub2, face2) = subObjTups[co]
|
||||
|
||||
# analyze two subs for common faces
|
||||
for ei1 in range(0, len(face1.Edges)):
|
||||
edg1 = face1.Edges[ei1]
|
||||
for ei2 in range(0, len(face2.Edges)):
|
||||
edg2 = face2.Edges[ei2]
|
||||
if edg1.isSame(edg2) is True:
|
||||
Path.Log.debug(
|
||||
"{}.Edges[{}] connects at {}.Edges[{}]".format(
|
||||
sub1, ei1, sub2, ei2
|
||||
)
|
||||
)
|
||||
shared.append((sub1, face1, ei1))
|
||||
touching[sub1].append(ei1)
|
||||
touching[sub2].append(ei2)
|
||||
# Efor
|
||||
# Remove duplicates from edge lists
|
||||
for sub in touching:
|
||||
touchingCleaned[sub] = []
|
||||
for s in touching[sub]:
|
||||
if s not in touchingCleaned[sub]:
|
||||
touchingCleaned[sub].append(s)
|
||||
|
||||
return (shared, touchingCleaned)
|
||||
|
||||
def identifyUnconnectedEdges(self, subObjTups, touching):
|
||||
"""identifyUnconnectedEdges(subObjTups, touching)
|
||||
Categorize unconnected edges into two groups, if possible: low and high"""
|
||||
# Identify unconnected edges
|
||||
# (should be top edge loop if all faces form loop with bottom face(s) included)
|
||||
high = []
|
||||
low = []
|
||||
holding = []
|
||||
|
||||
for (sub, face) in subObjTups:
|
||||
holding = []
|
||||
for ei in range(0, len(face.Edges)):
|
||||
if ei not in touching[sub]:
|
||||
holding.append((sub, face, ei))
|
||||
# Assign unconnected edges based upon category: high or low
|
||||
if len(holding) == 1:
|
||||
high.append(holding.pop())
|
||||
elif len(holding) == 2:
|
||||
edg0 = holding[0][1].Edges[holding[0][2]]
|
||||
edg1 = holding[1][1].Edges[holding[1][2]]
|
||||
if self.hasCommonVertex(edg0, edg1, show=False) < 0:
|
||||
# Edges not connected - probably top and bottom if faces in loop
|
||||
if edg0.CenterOfMass.z > edg1.CenterOfMass.z:
|
||||
high.append(holding[0])
|
||||
low.append(holding[1])
|
||||
else:
|
||||
high.append(holding[1])
|
||||
low.append(holding[0])
|
||||
else:
|
||||
# Edges are connected - all top, or all bottom edges
|
||||
com = FreeCAD.Vector(0, 0, 0)
|
||||
com.add(edg0.CenterOfMass)
|
||||
com.add(edg1.CenterOfMass)
|
||||
avgCom = FreeCAD.Vector(com.x / 2.0, com.y / 2.0, com.z / 2.0)
|
||||
if avgCom.z > face.CenterOfMass.z:
|
||||
high.extend(holding)
|
||||
else:
|
||||
low.extend(holding)
|
||||
elif len(holding) > 2:
|
||||
# attempt to break edges into two groups of connected edges.
|
||||
# determine which group has higher center of mass, and assign as high, the other as low
|
||||
(lw, hgh) = self.groupConnectedEdges(holding)
|
||||
low.extend(lw)
|
||||
high.extend(hgh)
|
||||
# Eif
|
||||
# Efor
|
||||
return (low, high)
|
||||
|
||||
def hasCommonVertex(self, edge1, edge2, show=False):
|
||||
"""findCommonVertexIndexes(edge1, edge2, show=False)
|
||||
Compare vertexes of two edges to identify a common vertex.
|
||||
Returns the vertex index of edge1 to which edge2 is connected"""
|
||||
if show is True:
|
||||
Path.Log.info("New findCommonVertex()... ")
|
||||
|
||||
oIdx = 0
|
||||
listOne = edge1.Vertexes
|
||||
listTwo = edge2.Vertexes
|
||||
|
||||
# Find common vertexes
|
||||
for o in listOne:
|
||||
if show is True:
|
||||
Path.Log.info(" one ({}, {}, {})".format(o.X, o.Y, o.Z))
|
||||
for t in listTwo:
|
||||
if show is True:
|
||||
Path.Log.error("two ({}, {}, {})".format(t.X, t.Y, t.Z))
|
||||
if o.X == t.X:
|
||||
if o.Y == t.Y:
|
||||
if o.Z == t.Z:
|
||||
if show is True:
|
||||
Path.Log.info("found")
|
||||
return oIdx
|
||||
oIdx += 1
|
||||
return -1
|
||||
|
||||
def groupConnectedEdges(self, holding):
|
||||
"""groupConnectedEdges(self, holding)
|
||||
Take edges and determine which are connected.
|
||||
Group connected chains/loops into: low and high"""
|
||||
holds = []
|
||||
grps = []
|
||||
searched = []
|
||||
stop = False
|
||||
attachments = []
|
||||
loops = 1
|
||||
|
||||
def updateAttachments(grps):
|
||||
atchmnts = []
|
||||
lenGrps = len(grps)
|
||||
if lenGrps > 0:
|
||||
lenG0 = len(grps[0])
|
||||
if lenG0 < 2:
|
||||
atchmnts.append((0, 0))
|
||||
else:
|
||||
atchmnts.append((0, 0))
|
||||
atchmnts.append((0, lenG0 - 1))
|
||||
if lenGrps == 2:
|
||||
lenG1 = len(grps[1])
|
||||
if lenG1 < 2:
|
||||
atchmnts.append((1, 0))
|
||||
else:
|
||||
atchmnts.append((1, 0))
|
||||
atchmnts.append((1, lenG1 - 1))
|
||||
return atchmnts
|
||||
|
||||
def isSameVertex(o, t):
|
||||
if o.X == t.X:
|
||||
if o.Y == t.Y:
|
||||
if o.Z == t.Z:
|
||||
return True
|
||||
return False
|
||||
|
||||
for hi in range(0, len(holding)):
|
||||
holds.append(hi)
|
||||
|
||||
# Place initial edge in first group and update attachments
|
||||
h0 = holds.pop()
|
||||
grps.append([h0])
|
||||
attachments = updateAttachments(grps)
|
||||
|
||||
while len(holds) > 0:
|
||||
if loops > 500:
|
||||
Path.Log.error("BREAK --- LOOPS LIMIT of 500 ---")
|
||||
break
|
||||
save = False
|
||||
|
||||
h2 = holds.pop()
|
||||
(sub2, face2, ei2) = holding[h2]
|
||||
|
||||
# Cycle through attachments for connection to existing
|
||||
for (g, t) in attachments:
|
||||
h1 = grps[g][t]
|
||||
(sub1, face1, ei1) = holding[h1]
|
||||
|
||||
edg1 = face1.Edges[ei1]
|
||||
edg2 = face2.Edges[ei2]
|
||||
|
||||
# CV = self.hasCommonVertex(edg1, edg2, show=False)
|
||||
|
||||
# Check attachment based on attachments order
|
||||
if t == 0:
|
||||
# is last vertex of h2 == first vertex of h1
|
||||
e2lv = len(edg2.Vertexes) - 1
|
||||
one = edg2.Vertexes[e2lv]
|
||||
two = edg1.Vertexes[0]
|
||||
if isSameVertex(one, two) is True:
|
||||
# Connected, insert h1 in front of h2
|
||||
grps[g].insert(0, h2)
|
||||
stop = True
|
||||
else:
|
||||
# is last vertex of h1 == first vertex of h2
|
||||
e1lv = len(edg1.Vertexes) - 1
|
||||
one = edg1.Vertexes[e1lv]
|
||||
two = edg2.Vertexes[0]
|
||||
if isSameVertex(one, two) is True:
|
||||
# Connected, append h1 after h2
|
||||
grps[g].append(h2)
|
||||
stop = True
|
||||
|
||||
if stop is True:
|
||||
# attachment was found
|
||||
attachments = updateAttachments(grps)
|
||||
holds.extend(searched)
|
||||
stop = False
|
||||
break
|
||||
else:
|
||||
# no attachment found
|
||||
save = True
|
||||
# Efor
|
||||
if save is True:
|
||||
searched.append(h2)
|
||||
if len(holds) == 0:
|
||||
if len(grps) == 1:
|
||||
h0 = searched.pop(0)
|
||||
grps.append([h0])
|
||||
attachments = updateAttachments(grps)
|
||||
holds.extend(searched)
|
||||
# Eif
|
||||
loops += 1
|
||||
# Ewhile
|
||||
|
||||
low = []
|
||||
high = []
|
||||
if len(grps) == 1:
|
||||
grps.append([])
|
||||
grp0 = []
|
||||
grp1 = []
|
||||
com0 = FreeCAD.Vector(0, 0, 0)
|
||||
com1 = FreeCAD.Vector(0, 0, 0)
|
||||
if len(grps[0]) > 0:
|
||||
for g in grps[0]:
|
||||
grp0.append(holding[g])
|
||||
(sub, face, ei) = holding[g]
|
||||
com0 = com0.add(face.Edges[ei].CenterOfMass)
|
||||
com0z = com0.z / len(grps[0])
|
||||
if len(grps[1]) > 0:
|
||||
for g in grps[1]:
|
||||
grp1.append(holding[g])
|
||||
(sub, face, ei) = holding[g]
|
||||
com1 = com1.add(face.Edges[ei].CenterOfMass)
|
||||
com1z = com1.z / len(grps[1])
|
||||
|
||||
if len(grps[1]) > 0:
|
||||
if com0z > com1z:
|
||||
low = grp1
|
||||
high = grp0
|
||||
else:
|
||||
low = grp0
|
||||
high = grp1
|
||||
else:
|
||||
low = grp0
|
||||
high = grp0
|
||||
|
||||
return (low, high)
|
||||
|
||||
def getMinMaxOfFaces(self, Faces):
|
||||
"""getMinMaxOfFaces(Faces)
|
||||
return the zmin and zmax values for given set of faces or edges."""
|
||||
zmin = Faces[0].BoundBox.ZMax
|
||||
zmax = Faces[0].BoundBox.ZMin
|
||||
for f in Faces:
|
||||
if f.BoundBox.ZMin < zmin:
|
||||
zmin = f.BoundBox.ZMin
|
||||
if f.BoundBox.ZMax > zmax:
|
||||
zmax = f.BoundBox.ZMax
|
||||
return (zmin, zmax)
|
||||
|
||||
|
||||
def _identifyRemovalSolids(sourceShape, commonShapes):
|
||||
"""_identifyRemovalSolids(sourceShape, commonShapes)
|
||||
Loops through solids in sourceShape to identify commonality with solids in commonShapes.
|
||||
The sourceShape solids with commonality are returned as Part.Compound shape."""
|
||||
common = Part.makeCompound(commonShapes)
|
||||
removalSolids = [s for s in sourceShape.Solids if s.common(common).Volume > 0.0]
|
||||
return Part.makeCompound(removalSolids)
|
||||
|
||||
|
||||
def _extrudeBaseDown(base):
|
||||
"""_extrudeBaseDown(base)
|
||||
Extrudes and fuses all non-vertical faces downward to a level 1.0 mm below base ZMin."""
|
||||
allExtrusions = list()
|
||||
zMin = base.Shape.BoundBox.ZMin
|
||||
bbFace = PathGeom.makeBoundBoxFace(base.Shape.BoundBox, offset=5.0)
|
||||
bbFace.translate(
|
||||
FreeCAD.Vector(0.0, 0.0, float(int(base.Shape.BoundBox.ZMin - 5.0)))
|
||||
)
|
||||
direction = FreeCAD.Vector(0.0, 0.0, -1.0)
|
||||
|
||||
# Make projections of each non-vertical face and extrude it
|
||||
for f in base.Shape.Faces:
|
||||
fbb = f.BoundBox
|
||||
if not PathGeom.isRoughly(f.normalAt(0, 0).z, 0.0):
|
||||
pp = bbFace.makeParallelProjection(f.Wires[0], direction)
|
||||
face = Part.Face(Part.Wire(pp.Edges))
|
||||
face.translate(FreeCAD.Vector(0.0, 0.0, fbb.ZMin))
|
||||
ext = face.extrude(FreeCAD.Vector(0.0, 0.0, zMin - fbb.ZMin - 1.0))
|
||||
allExtrusions.append(ext)
|
||||
|
||||
# Fuse all extrusions together
|
||||
seed = allExtrusions.pop()
|
||||
fusion = seed.fuse(allExtrusions)
|
||||
fusion.translate(FreeCAD.Vector(0.0, 0.0, zMin - fusion.BoundBox.ZMin - 1.0))
|
||||
|
||||
return fusion.cut(base.Shape)
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
return PathPocketBase.SetupProperties() + ["HandleMultipleFeatures"]
|
||||
|
||||
|
||||
def Create(name, obj=None, parentJob=None):
|
||||
"""Create(name) ... Creates and returns a Pocket operation."""
|
||||
if obj is None:
|
||||
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
||||
obj.Proxy = ObjectPocket(obj, name, parentJob)
|
||||
return obj
|
||||
@@ -1,283 +0,0 @@
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * Copyright (c) 2020 russ4262 (Russell Johnson) *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathAreaOp as PathAreaOp
|
||||
import PathScripts.PathOp as PathOp
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
|
||||
__title__ = "Base Path Pocket Operation"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Base class and implementation for Path pocket operations."
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class ObjectPocket(PathAreaOp.ObjectOp):
|
||||
"""Base class for proxy objects of all pocket operations."""
|
||||
|
||||
@classmethod
|
||||
def pocketPropertyEnumerations(cls, dataType="data"):
|
||||
"""pocketPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
|
||||
Args:
|
||||
dataType = 'data', 'raw', 'translated'
|
||||
Notes:
|
||||
'data' is list of internal string literals used in code
|
||||
'raw' is list of (translated_text, data_string) tuples
|
||||
'translated' is list of translated string literals
|
||||
"""
|
||||
|
||||
enums = {
|
||||
"CutMode": [
|
||||
(translate("Path_Pocket", "Climb"), "Climb"),
|
||||
(translate("Path_Pocket", "Conventional"), "Conventional"),
|
||||
], # this is the direction that the profile runs
|
||||
"StartAt": [
|
||||
(translate("Path_Pocket", "Center"), "Center"),
|
||||
(translate("Path_Pocket", "Edge"), "Edge"),
|
||||
],
|
||||
"OffsetPattern": [
|
||||
(translate("Path_Pocket", "ZigZag"), "ZigZag"),
|
||||
(translate("Path_Pocket", "Offset"), "Offset"),
|
||||
(translate("Path_Pocket", "ZigZagOffset"), "ZigZagOffset"),
|
||||
(translate("Path_Pocket", "Line"), "Line"),
|
||||
(translate("Path_Pocket", "Grid"), "Grid"),
|
||||
], # Fill Pattern
|
||||
}
|
||||
|
||||
if dataType == "raw":
|
||||
return enums
|
||||
|
||||
data = list()
|
||||
idx = 0 if dataType == "translated" else 1
|
||||
|
||||
Path.Log.debug(enums)
|
||||
|
||||
for k, v in enumerate(enums):
|
||||
data.append((v, [tup[idx] for tup in enums[v]]))
|
||||
Path.Log.debug(data)
|
||||
|
||||
return data
|
||||
|
||||
def areaOpFeatures(self, obj):
|
||||
"""areaOpFeatures(obj) ... Pockets have a FinishDepth and work on Faces"""
|
||||
return (
|
||||
PathOp.FeatureBaseFaces
|
||||
| PathOp.FeatureFinishDepth
|
||||
| self.pocketOpFeatures(obj)
|
||||
)
|
||||
|
||||
def pocketOpFeatures(self, obj):
|
||||
return 0
|
||||
|
||||
def initPocketOp(self, obj):
|
||||
"""initPocketOp(obj) ... overwrite to initialize subclass.
|
||||
Can safely be overwritten by subclass."""
|
||||
pass
|
||||
|
||||
def areaOpSetDefaultValues(self, obj, job):
|
||||
obj.PocketLastStepOver = 0
|
||||
|
||||
def pocketInvertExtraOffset(self):
|
||||
"""pocketInvertExtraOffset() ... return True if ExtraOffset's direction is inward.
|
||||
Can safely be overwritten by subclass."""
|
||||
return False
|
||||
|
||||
def initAreaOp(self, obj):
|
||||
"""initAreaOp(obj) ... create pocket specific properties.
|
||||
Do not overwrite, implement initPocketOp(obj) instead."""
|
||||
Path.Log.track()
|
||||
|
||||
# Pocket Properties
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"CutMode",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"The direction that the toolpath should go around the part ClockWise (CW) or CounterClockWise (CCW)",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"ExtraOffset",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Extra offset to apply to the operation. Direction is operation dependent.",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"StartAt",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Start pocketing at center or boundary"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyPercent",
|
||||
"StepOver",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Percent of cutter diameter to step over on each pass"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyFloat",
|
||||
"ZigZagAngle",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Angle of the zigzag pattern"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"OffsetPattern",
|
||||
"Face",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Clearing pattern to use"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"MinTravel",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Use 3D Sorting of Path"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"KeepToolDown",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Attempts to avoid unnecessary retractions."
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyPercent",
|
||||
"PocketLastStepOver",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Last Stepover Radius. If 0, 50% of cutter is used. Tuning this can be used to improve stepover for some shapes",
|
||||
),
|
||||
)
|
||||
|
||||
for n in self.pocketPropertyEnumerations():
|
||||
setattr(obj, n[0], n[1])
|
||||
|
||||
self.initPocketOp(obj)
|
||||
|
||||
def areaOpRetractTool(self, obj):
|
||||
Path.Log.debug("retracting tool: %d" % (not obj.KeepToolDown))
|
||||
return not obj.KeepToolDown
|
||||
|
||||
def areaOpUseProjection(self, obj):
|
||||
"""areaOpUseProjection(obj) ... return False"""
|
||||
return False
|
||||
|
||||
def areaOpAreaParams(self, obj, isHole):
|
||||
"""areaOpAreaParams(obj, isHole) ... return dictionary with pocket's area parameters"""
|
||||
Path.Log.track()
|
||||
params = {}
|
||||
params["Fill"] = 0
|
||||
params["Coplanar"] = 0
|
||||
params["PocketMode"] = 1
|
||||
params["SectionCount"] = -1
|
||||
params["Angle"] = obj.ZigZagAngle
|
||||
params["FromCenter"] = obj.StartAt == "Center"
|
||||
params["PocketStepover"] = (self.radius * 2) * (float(obj.StepOver) / 100)
|
||||
extraOffset = obj.ExtraOffset.Value
|
||||
if self.pocketInvertExtraOffset():
|
||||
extraOffset = 0 - extraOffset
|
||||
params["PocketExtraOffset"] = extraOffset
|
||||
params["ToolRadius"] = self.radius
|
||||
params["PocketLastStepover"] = obj.PocketLastStepOver
|
||||
|
||||
Pattern = {
|
||||
"ZigZag": 1,
|
||||
"Offset": 2,
|
||||
"ZigZagOffset": 4,
|
||||
"Line": 5,
|
||||
"Grid": 6,
|
||||
}
|
||||
|
||||
params["PocketMode"] = Pattern.get(obj.OffsetPattern, 1)
|
||||
|
||||
if obj.SplitArcs:
|
||||
params["Explode"] = True
|
||||
params["FitArcs"] = False
|
||||
|
||||
return params
|
||||
|
||||
def opOnDocumentRestored(self, obj):
|
||||
super().opOnDocumentRestored(obj)
|
||||
if not hasattr(obj, "PocketLastStepOver"):
|
||||
obj.addProperty(
|
||||
"App::PropertyPercent",
|
||||
"PocketLastStepOver",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Last Stepover Radius. If 0, 50% of cutter is used. Tuning this can be used to improve stepover for some shapes",
|
||||
),
|
||||
)
|
||||
obj.PocketLastStepOver = 0
|
||||
Path.Log.track()
|
||||
|
||||
def areaOpPathParams(self, obj, isHole):
|
||||
"""areaOpAreaParams(obj, isHole) ... return dictionary with pocket's path parameters"""
|
||||
params = {}
|
||||
|
||||
CutMode = ["Conventional", "Climb"]
|
||||
params["orientation"] = CutMode.index(obj.CutMode)
|
||||
|
||||
# if MinTravel is turned on, set path sorting to 3DSort
|
||||
# 3DSort shouldn't be used without a valid start point. Can cause
|
||||
# tool crash without it.
|
||||
#
|
||||
# ml: experimental feature, turning off for now (see https://forum.freecadweb.org/viewtopic.php?f=15&t=24422&start=30#p192458)
|
||||
# realthunder: I've fixed it with a new sorting algorithm, which I
|
||||
# tested fine, but of course need more test. Please let know if there is
|
||||
# any problem
|
||||
#
|
||||
if obj.MinTravel and obj.UseStartPoint and obj.StartPoint is not None:
|
||||
params["sort_mode"] = 3
|
||||
params["threshold"] = self.radius * 2
|
||||
return params
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
setup = PathAreaOp.SetupProperties()
|
||||
setup.append("CutMode")
|
||||
setup.append("ExtraOffset")
|
||||
setup.append("StepOver")
|
||||
setup.append("ZigZagAngle")
|
||||
setup.append("OffsetPattern")
|
||||
setup.append("StartAt")
|
||||
setup.append("MinTravel")
|
||||
setup.append("KeepToolDown")
|
||||
return setup
|
||||
@@ -1,197 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import PathGui as PGui # ensure Path/Gui/Resources are loaded
|
||||
import PathScripts.PathGui as PathGui
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathPocket as PathPocket
|
||||
|
||||
__title__ = "Path Pocket Base Operation UI"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Base page controller and command implementation for path pocket operations."
|
||||
|
||||
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
|
||||
|
||||
FeaturePocket = 0x01
|
||||
FeatureFacing = 0x02
|
||||
FeatureOutline = 0x04
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""Page controller class for pocket operations, supports:
|
||||
FeaturePocket ... used for pocketing operation
|
||||
FeatureFacing ... used for face milling operation
|
||||
FeatureOutline ... used for pocket-shape operation
|
||||
"""
|
||||
|
||||
def pocketFeatures(self):
|
||||
"""pocketFeatures() ... return which features of the UI are supported by the operation.
|
||||
FeaturePocket ... used for pocketing operation
|
||||
FeatureFacing ... used for face milling operation
|
||||
FeatureOutline ... used for pocket-shape operation
|
||||
Must be overwritten by subclasses"""
|
||||
pass
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... returns UI, adapted to the results from pocketFeatures()"""
|
||||
form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpPocketFullEdit.ui")
|
||||
|
||||
comboToPropertyMap = [
|
||||
("cutMode", "CutMode"),
|
||||
("offsetPattern", "OffsetPattern"),
|
||||
]
|
||||
enumTups = PathPocket.ObjectPocket.pocketPropertyEnumerations(dataType="raw")
|
||||
|
||||
self.populateCombobox(form, enumTups, comboToPropertyMap)
|
||||
|
||||
if not FeatureFacing & self.pocketFeatures():
|
||||
form.facingWidget.hide()
|
||||
form.clearEdges.hide()
|
||||
|
||||
if FeaturePocket & self.pocketFeatures():
|
||||
form.extraOffset_label.setText(translate("PathPocket", "Pass Extension"))
|
||||
form.extraOffset.setToolTip(
|
||||
translate(
|
||||
"PathPocket",
|
||||
"The distance the facing operation will extend beyond the boundary shape.",
|
||||
)
|
||||
)
|
||||
|
||||
if not (FeatureOutline & self.pocketFeatures()):
|
||||
form.useOutline.hide()
|
||||
|
||||
# if True:
|
||||
# # currently doesn't have an effect or is experimental
|
||||
# form.minTravel.hide()
|
||||
|
||||
return form
|
||||
|
||||
def updateMinTravel(self, obj, setModel=True):
|
||||
if obj.UseStartPoint:
|
||||
self.form.minTravel.setEnabled(True)
|
||||
else:
|
||||
self.form.minTravel.setChecked(False)
|
||||
self.form.minTravel.setEnabled(False)
|
||||
|
||||
if setModel and obj.MinTravel != self.form.minTravel.isChecked():
|
||||
obj.MinTravel = self.form.minTravel.isChecked()
|
||||
|
||||
def updateZigZagAngle(self, obj, setModel=True):
|
||||
if obj.OffsetPattern in ["Offset"]:
|
||||
self.form.zigZagAngle.setEnabled(False)
|
||||
else:
|
||||
self.form.zigZagAngle.setEnabled(True)
|
||||
|
||||
if setModel:
|
||||
PathGui.updateInputField(obj, "ZigZagAngle", self.form.zigZagAngle)
|
||||
|
||||
def getFields(self, obj):
|
||||
"""getFields(obj) ... transfers values from UI to obj's proprties"""
|
||||
if obj.CutMode != str(self.form.cutMode.currentData()):
|
||||
obj.CutMode = str(self.form.cutMode.currentData())
|
||||
if obj.StepOver != self.form.stepOverPercent.value():
|
||||
obj.StepOver = self.form.stepOverPercent.value()
|
||||
if obj.OffsetPattern != str(self.form.offsetPattern.currentData()):
|
||||
obj.OffsetPattern = str(self.form.offsetPattern.currentData())
|
||||
|
||||
PathGui.updateInputField(obj, "ExtraOffset", self.form.extraOffset)
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
self.updateCoolant(obj, self.form.coolantController)
|
||||
self.updateZigZagAngle(obj)
|
||||
|
||||
if obj.UseStartPoint != self.form.useStartPoint.isChecked():
|
||||
obj.UseStartPoint = self.form.useStartPoint.isChecked()
|
||||
|
||||
if FeatureOutline & self.pocketFeatures():
|
||||
if obj.UseOutline != self.form.useOutline.isChecked():
|
||||
obj.UseOutline = self.form.useOutline.isChecked()
|
||||
|
||||
self.updateMinTravel(obj)
|
||||
|
||||
if FeatureFacing & self.pocketFeatures():
|
||||
print(obj.BoundaryShape)
|
||||
print(self.form.boundaryShape.currentText())
|
||||
print(self.form.boundaryShape.currentData())
|
||||
if obj.BoundaryShape != str(self.form.boundaryShape.currentData()):
|
||||
obj.BoundaryShape = str(self.form.boundaryShape.currentData())
|
||||
if obj.ClearEdges != self.form.clearEdges.isChecked():
|
||||
obj.ClearEdges = self.form.clearEdges.isChecked()
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... transfers obj's property values to UI"""
|
||||
self.form.stepOverPercent.setValue(obj.StepOver)
|
||||
self.form.extraOffset.setText(
|
||||
FreeCAD.Units.Quantity(
|
||||
obj.ExtraOffset.Value, FreeCAD.Units.Length
|
||||
).UserString
|
||||
)
|
||||
self.form.useStartPoint.setChecked(obj.UseStartPoint)
|
||||
if FeatureOutline & self.pocketFeatures():
|
||||
self.form.useOutline.setChecked(obj.UseOutline)
|
||||
|
||||
self.form.zigZagAngle.setText(
|
||||
FreeCAD.Units.Quantity(obj.ZigZagAngle, FreeCAD.Units.Angle).UserString
|
||||
)
|
||||
self.updateZigZagAngle(obj, False)
|
||||
|
||||
self.form.minTravel.setChecked(obj.MinTravel)
|
||||
self.updateMinTravel(obj, False)
|
||||
|
||||
self.selectInComboBox(obj.OffsetPattern, self.form.offsetPattern)
|
||||
self.selectInComboBox(obj.CutMode, self.form.cutMode)
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.setupCoolant(obj, self.form.coolantController)
|
||||
|
||||
if FeatureFacing & self.pocketFeatures():
|
||||
self.selectInComboBox(obj.BoundaryShape, self.form.boundaryShape)
|
||||
self.form.clearEdges.setChecked(obj.ClearEdges)
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
|
||||
signals = []
|
||||
|
||||
signals.append(self.form.cutMode.currentIndexChanged)
|
||||
signals.append(self.form.offsetPattern.currentIndexChanged)
|
||||
signals.append(self.form.stepOverPercent.editingFinished)
|
||||
signals.append(self.form.zigZagAngle.editingFinished)
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.extraOffset.editingFinished)
|
||||
signals.append(self.form.useStartPoint.clicked)
|
||||
signals.append(self.form.useOutline.clicked)
|
||||
signals.append(self.form.minTravel.clicked)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
|
||||
if FeatureFacing & self.pocketFeatures():
|
||||
signals.append(self.form.boundaryShape.currentIndexChanged)
|
||||
signals.append(self.form.clearEdges.clicked)
|
||||
|
||||
return signals
|
||||
@@ -1,64 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathPocket as PathPocket
|
||||
import PathScripts.PathPocketBaseGui as PathPocketBaseGui
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
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())
|
||||
|
||||
|
||||
__title__ = "Path Pocket Operation UI"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Pocket operation page controller and command implementation."
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathPocketBaseGui.TaskPanelOpPage):
|
||||
"""Page controller class for Pocket operation"""
|
||||
|
||||
def pocketFeatures(self):
|
||||
"""pocketFeatures() ... return FeaturePocket (see PathPocketBaseGui)"""
|
||||
return PathPocketBaseGui.FeaturePocket
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Pocket3D",
|
||||
PathPocket.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_3DPocket",
|
||||
QT_TRANSLATE_NOOP("Path_Pocket3D", "3D Pocket"),
|
||||
QT_TRANSLATE_NOOP(
|
||||
"Path_Pocket3D", "Creates a Path 3D Pocket object from a face or faces"
|
||||
),
|
||||
PathPocket.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathPocketGui... done\n")
|
||||
@@ -1,294 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathGeom as PathGeom
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathScripts.PathPocketBase as PathPocketBase
|
||||
|
||||
|
||||
# lazily loaded modules
|
||||
from lazy_loader.lazy_loader import LazyLoader
|
||||
|
||||
Part = LazyLoader("Part", globals(), "Part")
|
||||
TechDraw = LazyLoader("TechDraw", globals(), "TechDraw")
|
||||
math = LazyLoader("math", globals(), "math")
|
||||
PathUtils = LazyLoader("PathScripts.PathUtils", globals(), "PathScripts.PathUtils")
|
||||
FeatureExtensions = LazyLoader(
|
||||
"PathScripts.PathFeatureExtensions", globals(), "PathScripts.PathFeatureExtensions"
|
||||
)
|
||||
|
||||
|
||||
__title__ = "Path Pocket Shape Operation"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Class and implementation of shape based Pocket operation."
|
||||
|
||||
|
||||
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 ObjectPocket(PathPocketBase.ObjectPocket):
|
||||
"""Proxy object for Pocket operation."""
|
||||
|
||||
def areaOpFeatures(self, obj):
|
||||
return super(self.__class__, self).areaOpFeatures(obj) | PathOp.FeatureLocations
|
||||
|
||||
def initPocketOp(self, obj):
|
||||
"""initPocketOp(obj) ... setup receiver"""
|
||||
if not hasattr(obj, "UseOutline"):
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"UseOutline",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Uses the outline of the base geometry."
|
||||
),
|
||||
)
|
||||
|
||||
FeatureExtensions.initialize_properties(obj)
|
||||
|
||||
def areaOpOnDocumentRestored(self, obj):
|
||||
"""opOnDocumentRestored(obj) ... adds the UseOutline property if it doesn't exist."""
|
||||
self.initPocketOp(obj)
|
||||
|
||||
def pocketInvertExtraOffset(self):
|
||||
return False
|
||||
|
||||
def areaOpSetDefaultValues(self, obj, job):
|
||||
"""areaOpSetDefaultValues(obj, job) ... set default values"""
|
||||
obj.StepOver = 100
|
||||
obj.ZigZagAngle = 45
|
||||
obj.UseOutline = False
|
||||
FeatureExtensions.set_default_property_values(obj, job)
|
||||
|
||||
def areaOpShapes(self, obj):
|
||||
"""areaOpShapes(obj) ... return shapes representing the solids to be removed."""
|
||||
Path.Log.track()
|
||||
self.removalshapes = []
|
||||
|
||||
# self.isDebug = True if Path.Log.getLevel(Path.Log.thisModule()) == 4 else False
|
||||
self.removalshapes = []
|
||||
avoidFeatures = list()
|
||||
|
||||
# Get extensions and identify faces to avoid
|
||||
extensions = FeatureExtensions.getExtensions(obj)
|
||||
for e in extensions:
|
||||
if e.avoid:
|
||||
avoidFeatures.append(e.feature)
|
||||
|
||||
if obj.Base:
|
||||
Path.Log.debug("base items exist. Processing...")
|
||||
self.horiz = []
|
||||
self.vert = []
|
||||
for (base, subList) in obj.Base:
|
||||
for sub in subList:
|
||||
if "Face" in sub:
|
||||
if sub not in avoidFeatures and not self.clasifySub(base, sub):
|
||||
Path.Log.error(
|
||||
"Pocket does not support shape {}.{}".format(
|
||||
base.Label, sub
|
||||
)
|
||||
)
|
||||
|
||||
# Convert horizontal faces to use outline only if requested
|
||||
if obj.UseOutline and self.horiz:
|
||||
horiz = [Part.Face(f.Wire1) for f in self.horiz]
|
||||
self.horiz = horiz
|
||||
|
||||
# Check if selected vertical faces form a loop
|
||||
if len(self.vert) > 0:
|
||||
self.vertical = PathGeom.combineConnectedShapes(self.vert)
|
||||
self.vWires = [
|
||||
TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1))
|
||||
for shape in self.vertical
|
||||
]
|
||||
for wire in self.vWires:
|
||||
w = PathGeom.removeDuplicateEdges(wire)
|
||||
face = Part.Face(w)
|
||||
# face.tessellate(0.1)
|
||||
if PathGeom.isRoughly(face.Area, 0):
|
||||
Path.Log.error("Vertical faces do not form a loop - ignoring")
|
||||
else:
|
||||
self.horiz.append(face)
|
||||
|
||||
# Add faces for extensions
|
||||
self.exts = []
|
||||
for ext in extensions:
|
||||
if not ext.avoid:
|
||||
wire = ext.getWire()
|
||||
if wire:
|
||||
faces = ext.getExtensionFaces(wire)
|
||||
for f in faces:
|
||||
self.horiz.append(f)
|
||||
self.exts.append(f)
|
||||
|
||||
# check all faces and see if they are touching/overlapping and combine and simplify
|
||||
self.horizontal = PathGeom.combineHorizontalFaces(self.horiz)
|
||||
|
||||
# Move all faces to final depth less buffer before extrusion
|
||||
# Small negative buffer is applied to compensate for internal significant digits/rounding issue
|
||||
if self.job.GeometryTolerance.Value == 0.0:
|
||||
buffer = 0.000001
|
||||
else:
|
||||
buffer = self.job.GeometryTolerance.Value / 10.0
|
||||
for h in self.horizontal:
|
||||
h.translate(
|
||||
FreeCAD.Vector(
|
||||
0.0, 0.0, obj.FinalDepth.Value - h.BoundBox.ZMin - buffer
|
||||
)
|
||||
)
|
||||
|
||||
# extrude all faces up to StartDepth plus buffer and those are the removal shapes
|
||||
extent = FreeCAD.Vector(
|
||||
0, 0, obj.StartDepth.Value - obj.FinalDepth.Value + buffer
|
||||
)
|
||||
self.removalshapes = [
|
||||
(face.removeSplitter().extrude(extent), False)
|
||||
for face in self.horizontal
|
||||
]
|
||||
|
||||
else: # process the job base object as a whole
|
||||
Path.Log.debug("processing the whole job base object")
|
||||
self.outlines = [
|
||||
Part.Face(
|
||||
TechDraw.findShapeOutline(base.Shape, 1, FreeCAD.Vector(0, 0, 1))
|
||||
)
|
||||
for base in self.model
|
||||
]
|
||||
stockBB = self.stock.Shape.BoundBox
|
||||
|
||||
self.bodies = []
|
||||
for outline in self.outlines:
|
||||
outline.translate(FreeCAD.Vector(0, 0, stockBB.ZMin - 1))
|
||||
body = outline.extrude(FreeCAD.Vector(0, 0, stockBB.ZLength + 2))
|
||||
self.bodies.append(body)
|
||||
self.removalshapes.append((self.stock.Shape.cut(body), False))
|
||||
|
||||
# Tessellate all working faces
|
||||
# for (shape, hole) in self.removalshapes:
|
||||
# shape.tessellate(0.05) # originally 0.1
|
||||
|
||||
if self.removalshapes:
|
||||
obj.removalshape = Part.makeCompound([tup[0] for tup in self.removalshapes])
|
||||
|
||||
return self.removalshapes
|
||||
|
||||
# Support methods
|
||||
def isVerticalExtrusionFace(self, face):
|
||||
fBB = face.BoundBox
|
||||
if PathGeom.isRoughly(fBB.ZLength, 0.0):
|
||||
return False
|
||||
extr = face.extrude(FreeCAD.Vector(0.0, 0.0, fBB.ZLength))
|
||||
if hasattr(extr, "Volume"):
|
||||
if PathGeom.isRoughly(extr.Volume, 0.0):
|
||||
return True
|
||||
return False
|
||||
|
||||
def clasifySub(self, bs, sub):
|
||||
"""clasifySub(bs, sub)...
|
||||
Given a base and a sub-feature name, returns True
|
||||
if the sub-feature is a horizontally oriented flat face.
|
||||
"""
|
||||
face = bs.Shape.getElement(sub)
|
||||
|
||||
if type(face.Surface) == Part.Plane:
|
||||
Path.Log.debug("type() == Part.Plane")
|
||||
if PathGeom.isVertical(face.Surface.Axis):
|
||||
Path.Log.debug(" -isVertical()")
|
||||
# it's a flat horizontal face
|
||||
self.horiz.append(face)
|
||||
return True
|
||||
|
||||
elif PathGeom.isHorizontal(face.Surface.Axis):
|
||||
Path.Log.debug(" -isHorizontal()")
|
||||
self.vert.append(face)
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical(
|
||||
face.Surface.Axis
|
||||
):
|
||||
Path.Log.debug("type() == Part.Cylinder")
|
||||
# vertical cylinder wall
|
||||
if any(e.isClosed() for e in face.Edges):
|
||||
Path.Log.debug(" -e.isClosed()")
|
||||
# complete cylinder
|
||||
circle = Part.makeCircle(face.Surface.Radius, face.Surface.Center)
|
||||
disk = Part.Face(Part.Wire(circle))
|
||||
disk.translate(
|
||||
FreeCAD.Vector(0, 0, face.BoundBox.ZMin - disk.BoundBox.ZMin)
|
||||
)
|
||||
self.horiz.append(disk)
|
||||
return True
|
||||
|
||||
else:
|
||||
Path.Log.debug(" -none isClosed()")
|
||||
# partial cylinder wall
|
||||
self.vert.append(face)
|
||||
return True
|
||||
|
||||
elif type(face.Surface) == Part.SurfaceOfExtrusion:
|
||||
# extrusion wall
|
||||
Path.Log.debug("type() == Part.SurfaceOfExtrusion")
|
||||
# Save face to self.horiz for processing or display error
|
||||
if self.isVerticalExtrusionFace(face):
|
||||
self.vert.append(face)
|
||||
return True
|
||||
else:
|
||||
Path.Log.error("Failed to identify vertical face from {}".format(sub))
|
||||
|
||||
else:
|
||||
Path.Log.debug(" -type(face.Surface): {}".format(type(face.Surface)))
|
||||
return False
|
||||
|
||||
|
||||
# Eclass
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
setup = PathPocketBase.SetupProperties() # Add properties from PocketBase module
|
||||
setup.extend(
|
||||
FeatureExtensions.SetupProperties()
|
||||
) # Add properties from Extensions Feature
|
||||
|
||||
# Add properties initialized here in PocketShape
|
||||
setup.append("UseOutline")
|
||||
return setup
|
||||
|
||||
|
||||
def Create(name, obj=None, parentJob=None):
|
||||
"""Create(name) ... Creates and returns a Pocket operation."""
|
||||
if obj is None:
|
||||
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
||||
obj.Proxy = ObjectPocket(obj, name, parentJob)
|
||||
return obj
|
||||
|
||||
return obj
|
||||
@@ -1,77 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathPocketShape as PathPocketShape
|
||||
import PathScripts.PathPocketBaseGui as PathPocketBaseGui
|
||||
import PathScripts.PathFeatureExtensionsGui as PathFeatureExtensionsGui
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
# lazily loaded modules
|
||||
from lazy_loader.lazy_loader import LazyLoader
|
||||
|
||||
Part = LazyLoader("Part", globals(), "Part")
|
||||
|
||||
__title__ = "Path Pocket Shape Operation UI"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Pocket Shape operation page controller and command implementation."
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathPocketBaseGui.TaskPanelOpPage):
|
||||
"""Page controller class for Pocket operation"""
|
||||
|
||||
def pocketFeatures(self):
|
||||
"""pocketFeatures() ... return FeaturePocket (see PathPocketBaseGui)"""
|
||||
return PathPocketBaseGui.FeaturePocket | PathPocketBaseGui.FeatureOutline
|
||||
|
||||
def taskPanelBaseLocationPage(self, obj, features):
|
||||
if not hasattr(self, "extensionsPanel"):
|
||||
self.extensionsPanel = PathFeatureExtensionsGui.TaskPanelExtensionPage(
|
||||
obj, features
|
||||
)
|
||||
return self.extensionsPanel
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Pocket Shape",
|
||||
PathPocketShape.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_Pocket",
|
||||
QT_TRANSLATE_NOOP("Path_Pocket_Shape", "Pocket Shape"),
|
||||
QT_TRANSLATE_NOOP(
|
||||
"Path_Pocket_Shape", "Creates a Path Pocket object from a face or faces"
|
||||
),
|
||||
PathPocketShape.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathPocketShapeGui... done\n")
|
||||
@@ -1,147 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2018 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 __future__ import print_function
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
|
||||
__title__ = "Path Probing Operation"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Path Probing operation."
|
||||
|
||||
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 ObjectProbing(PathOp.ObjectOp):
|
||||
"""Proxy object for Probing operation."""
|
||||
|
||||
def opFeatures(self, obj):
|
||||
"""opFeatures(obj) ... Probing works on the stock object."""
|
||||
return PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureTool
|
||||
|
||||
def initOperation(self, obj):
|
||||
obj.addProperty(
|
||||
"App::PropertyLength",
|
||||
"Xoffset",
|
||||
"Probe",
|
||||
QT_TRANSLATE_NOOP("App::Property", "X offset between tool and probe"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyLength",
|
||||
"Yoffset",
|
||||
"Probe",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Y offset between tool and probe"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyInteger",
|
||||
"PointCountX",
|
||||
"Probe",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Number of points to probe in X direction"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyInteger",
|
||||
"PointCountY",
|
||||
"Probe",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Number of points to probe in Y direction"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyFile",
|
||||
"OutputFileName",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "The output location for the probe data to be written"
|
||||
),
|
||||
)
|
||||
|
||||
def nextpoint(self, startpoint=0.0, endpoint=0.0, count=3):
|
||||
curstep = 0
|
||||
dist = (endpoint - startpoint) / (count - 1)
|
||||
while curstep <= count - 1:
|
||||
yield startpoint + (curstep * dist)
|
||||
curstep += 1
|
||||
|
||||
def opExecute(self, obj):
|
||||
"""opExecute(obj) ... generate probe locations."""
|
||||
Path.Log.track()
|
||||
self.commandlist.append(Path.Command("(Begin Probing)"))
|
||||
|
||||
stock = PathUtils.findParentJob(obj).Stock
|
||||
bb = stock.Shape.BoundBox
|
||||
|
||||
openstring = "(PROBEOPEN {})".format(obj.OutputFileName)
|
||||
self.commandlist.append(Path.Command(openstring))
|
||||
self.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value}))
|
||||
|
||||
for y in self.nextpoint(bb.YMin, bb.YMax, obj.PointCountY):
|
||||
for x in self.nextpoint(bb.XMin, bb.XMax, obj.PointCountX):
|
||||
self.commandlist.append(
|
||||
Path.Command(
|
||||
"G0",
|
||||
{
|
||||
"X": x + obj.Xoffset.Value,
|
||||
"Y": y + obj.Yoffset.Value,
|
||||
"Z": obj.SafeHeight.Value,
|
||||
},
|
||||
)
|
||||
)
|
||||
self.commandlist.append(
|
||||
Path.Command(
|
||||
"G38.2",
|
||||
{
|
||||
"Z": obj.FinalDepth.Value,
|
||||
"F": obj.ToolController.VertFeed.Value,
|
||||
},
|
||||
)
|
||||
)
|
||||
self.commandlist.append(Path.Command("G0", {"Z": obj.SafeHeight.Value}))
|
||||
|
||||
self.commandlist.append(Path.Command("(PROBECLOSE)"))
|
||||
|
||||
def opSetDefaultValues(self, obj, job):
|
||||
"""opSetDefaultValues(obj, job) ... set default value for RetractHeight"""
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
setup = ["Xoffset", "Yoffset", "PointCountX", "PointCountY", "OutputFileName"]
|
||||
return setup
|
||||
|
||||
|
||||
def Create(name, obj=None, parentJob=None):
|
||||
"""Create(name) ... Creates and returns a Probing operation."""
|
||||
if obj is None:
|
||||
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
||||
proxy = ObjectProbing(obj, name, parentJob)
|
||||
return obj
|
||||
@@ -1,112 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import PathGui as PGui # ensure Path/Gui/Resources are loaded
|
||||
import PathScripts.PathProbe as PathProbe
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathGui as PathGui
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
__title__ = "Path Probing Operation UI"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Probing operation page controller and command implementation."
|
||||
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""Page controller class for the Probing operation."""
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... returns UI"""
|
||||
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpProbeEdit.ui")
|
||||
|
||||
def getFields(self, obj):
|
||||
"""getFields(obj) ... transfers values from UI to obj's proprties"""
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
PathGui.updateInputField(obj, "Xoffset", self.form.Xoffset)
|
||||
PathGui.updateInputField(obj, "Yoffset", self.form.Yoffset)
|
||||
obj.PointCountX = self.form.PointCountX.value()
|
||||
obj.PointCountY = self.form.PointCountY.value()
|
||||
obj.OutputFileName = str(self.form.OutputFileName.text())
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... transfers obj's property values to UI"""
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.form.Xoffset.setText(
|
||||
FreeCAD.Units.Quantity(obj.Xoffset.Value, FreeCAD.Units.Length).UserString
|
||||
)
|
||||
self.form.Yoffset.setText(
|
||||
FreeCAD.Units.Quantity(obj.Yoffset.Value, FreeCAD.Units.Length).UserString
|
||||
)
|
||||
self.form.OutputFileName.setText(obj.OutputFileName)
|
||||
self.form.PointCountX.setValue(obj.PointCountX)
|
||||
self.form.PointCountY.setValue(obj.PointCountY)
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
|
||||
signals = []
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.PointCountX.valueChanged)
|
||||
signals.append(self.form.PointCountY.valueChanged)
|
||||
signals.append(self.form.OutputFileName.editingFinished)
|
||||
signals.append(self.form.Xoffset.valueChanged)
|
||||
signals.append(self.form.Yoffset.valueChanged)
|
||||
self.form.SetOutputFileName.clicked.connect(self.SetOutputFileName)
|
||||
return signals
|
||||
|
||||
def SetOutputFileName(self):
|
||||
filename = QtGui.QFileDialog.getSaveFileName(
|
||||
self.form,
|
||||
translate("Path_Probe", "Select Output File"),
|
||||
None,
|
||||
translate("Path_Probe", "All Files (*.*)"),
|
||||
)
|
||||
if filename and filename[0]:
|
||||
self.obj.OutputFileName = str(filename[0])
|
||||
self.setFields(self.obj)
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Probe",
|
||||
PathProbe.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_Probe",
|
||||
QtCore.QT_TRANSLATE_NOOP("Path_Probe", "Probe"),
|
||||
QtCore.QT_TRANSLATE_NOOP("Path_Probe", "Create a Probing Grid from a job stock"),
|
||||
PathProbe.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathProbeGui... done\n")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@
|
||||
# * Major modifications: 2020 Russell Johnson <russ4262@gmail.com> *
|
||||
|
||||
import FreeCAD
|
||||
import PathScripts.PathProfile as PathProfile
|
||||
import Path.Op.Profile as PathProfile
|
||||
|
||||
|
||||
__title__ = "Path Contour Operation (depreciated)"
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
# * Major modifications: 2020 Russell Johnson <russ4262@gmail.com> *
|
||||
|
||||
import FreeCAD
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathProfile as PathProfile
|
||||
import PathScripts.PathProfileGui as PathProfileGui
|
||||
import Path
|
||||
import Path.Op.Gui.Base as PathOpGui
|
||||
import Path.Op.Gui.Profile as PathProfileGui
|
||||
import Path.Op.Profile as PathProfile
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
__title__ = "Path Contour Operation UI (depreciated)"
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
# * Major modifications: 2020 Russell Johnson <russ4262@gmail.com> *
|
||||
|
||||
import FreeCAD
|
||||
import PathScripts.PathProfile as PathProfile
|
||||
import Path
|
||||
import Path.Op.Profile as PathProfile
|
||||
|
||||
|
||||
__title__ = "Path Profile Edges Operation (depreciated)"
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
# * Major modifications: 2020 Russell Johnson <russ4262@gmail.com> *
|
||||
|
||||
import FreeCAD
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathProfile as PathProfile
|
||||
import PathScripts.PathProfileGui as PathProfileGui
|
||||
import Path.Op.Gui.Base as PathOpGui
|
||||
import Path.Op.Gui.Profile as PathProfileGui
|
||||
import Path.Op.Profile as PathProfile
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
__title__ = "Path Profile Edges Operation UI (depreciated)"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
# * Major modifications: 2020 Russell Johnson <russ4262@gmail.com> *
|
||||
|
||||
import FreeCAD
|
||||
import PathScripts.PathProfile as PathProfile
|
||||
import Path.Op.Profile as PathProfile
|
||||
|
||||
|
||||
__title__ = "Path Profile Faces Operation (depreciated)"
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
# * Major modifications: 2020 Russell Johnson <russ4262@gmail.com> *
|
||||
|
||||
import FreeCAD
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathProfile as PathProfile
|
||||
import PathScripts.PathProfileGui as PathProfileGui
|
||||
import Path.Op.Gui.Base as PathOpGui
|
||||
import Path.Op.Gui.Profile as PathProfileGui
|
||||
import Path.Op.Profile as PathProfile
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
__title__ = "Path Profile Faces Operation UI (depreciated)"
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import PathGui as PGui # ensure Path/Gui/Resources are loaded
|
||||
import PathScripts.PathGui as PathGui
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathProfile as PathProfile
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
|
||||
__title__ = "Path Profile Operation UI"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Profile operation page controller and command implementation."
|
||||
|
||||
|
||||
FeatureSide = 0x01
|
||||
FeatureProcessing = 0x02
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""Base class for profile operation page controllers. Two sub features are supported:
|
||||
FeatureSide ... Is the Side property exposed in the UI
|
||||
FeatureProcessing ... Are the processing check boxes supported by the operation
|
||||
"""
|
||||
|
||||
def initPage(self, obj):
|
||||
self.setTitle("Profile - " + obj.Label)
|
||||
self.updateVisibility()
|
||||
|
||||
def profileFeatures(self):
|
||||
"""profileFeatures() ... return which of the optional profile features are supported.
|
||||
Currently two features are supported and returned:
|
||||
FeatureSide ... Is the Side property exposed in the UI
|
||||
FeatureProcessing ... Are the processing check boxes supported by the operation
|
||||
."""
|
||||
return FeatureSide | FeatureProcessing
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... returns UI customized according to profileFeatures()"""
|
||||
form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpProfileFullEdit.ui")
|
||||
|
||||
comboToPropertyMap = [("cutSide", "Side"), ("direction", "Direction")]
|
||||
enumTups = PathProfile.ObjectProfile.areaOpPropertyEnumerations(dataType="raw")
|
||||
|
||||
self.populateCombobox(form, enumTups, comboToPropertyMap)
|
||||
return form
|
||||
|
||||
def getFields(self, obj):
|
||||
"""getFields(obj) ... transfers values from UI to obj's proprties"""
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
self.updateCoolant(obj, self.form.coolantController)
|
||||
|
||||
if obj.Side != str(self.form.cutSide.currentData()):
|
||||
obj.Side = str(self.form.cutSide.currentData())
|
||||
if obj.Direction != str(self.form.direction.currentData()):
|
||||
obj.Direction = str(self.form.direction.currentData())
|
||||
PathGui.updateInputField(obj, "OffsetExtra", self.form.extraOffset)
|
||||
|
||||
if obj.UseComp != self.form.useCompensation.isChecked():
|
||||
obj.UseComp = self.form.useCompensation.isChecked()
|
||||
if obj.UseStartPoint != self.form.useStartPoint.isChecked():
|
||||
obj.UseStartPoint = self.form.useStartPoint.isChecked()
|
||||
|
||||
if obj.processHoles != self.form.processHoles.isChecked():
|
||||
obj.processHoles = self.form.processHoles.isChecked()
|
||||
if obj.processPerimeter != self.form.processPerimeter.isChecked():
|
||||
obj.processPerimeter = self.form.processPerimeter.isChecked()
|
||||
if obj.processCircles != self.form.processCircles.isChecked():
|
||||
obj.processCircles = self.form.processCircles.isChecked()
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... transfers obj's property values to UI"""
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.setupCoolant(obj, self.form.coolantController)
|
||||
|
||||
self.selectInComboBox(obj.Side, self.form.cutSide)
|
||||
self.selectInComboBox(obj.Direction, self.form.direction)
|
||||
self.form.extraOffset.setText(
|
||||
FreeCAD.Units.Quantity(
|
||||
obj.OffsetExtra.Value, FreeCAD.Units.Length
|
||||
).UserString
|
||||
)
|
||||
|
||||
self.form.useCompensation.setChecked(obj.UseComp)
|
||||
self.form.useStartPoint.setChecked(obj.UseStartPoint)
|
||||
self.form.processHoles.setChecked(obj.processHoles)
|
||||
self.form.processPerimeter.setChecked(obj.processPerimeter)
|
||||
self.form.processCircles.setChecked(obj.processCircles)
|
||||
|
||||
self.updateVisibility()
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
|
||||
signals = []
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
signals.append(self.form.cutSide.currentIndexChanged)
|
||||
signals.append(self.form.direction.currentIndexChanged)
|
||||
signals.append(self.form.extraOffset.editingFinished)
|
||||
signals.append(self.form.useCompensation.stateChanged)
|
||||
signals.append(self.form.useStartPoint.stateChanged)
|
||||
signals.append(self.form.processHoles.stateChanged)
|
||||
signals.append(self.form.processPerimeter.stateChanged)
|
||||
signals.append(self.form.processCircles.stateChanged)
|
||||
|
||||
return signals
|
||||
|
||||
def updateVisibility(self):
|
||||
hasFace = False
|
||||
objBase = list()
|
||||
|
||||
if hasattr(self.obj, "Base"):
|
||||
objBase = self.obj.Base
|
||||
|
||||
if objBase.__len__() > 0:
|
||||
for (base, subsList) in objBase:
|
||||
for sub in subsList:
|
||||
if sub[:4] == "Face":
|
||||
hasFace = True
|
||||
break
|
||||
|
||||
if hasFace:
|
||||
self.form.processCircles.show()
|
||||
self.form.processHoles.show()
|
||||
self.form.processPerimeter.show()
|
||||
else:
|
||||
self.form.processCircles.hide()
|
||||
self.form.processHoles.hide()
|
||||
self.form.processPerimeter.hide()
|
||||
|
||||
def registerSignalHandlers(self, obj):
|
||||
self.form.useCompensation.stateChanged.connect(self.updateVisibility)
|
||||
|
||||
|
||||
# Eclass
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Profile",
|
||||
PathProfile.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_Contour",
|
||||
QT_TRANSLATE_NOOP("Path", "Profile"),
|
||||
QT_TRANSLATE_NOOP(
|
||||
"Path", "Profile entire model, selected face(s) or selected edge(s)"
|
||||
),
|
||||
PathProfile.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathProfileFacesGui... done\n")
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import PathScripts
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
@@ -45,7 +46,7 @@ class CommandPathSimpleCopy:
|
||||
return False
|
||||
try:
|
||||
obj = FreeCADGui.Selection.getSelectionEx()[0].Object
|
||||
return isinstance(obj.Proxy, PathScripts.PathOp.ObjectOp)
|
||||
return isinstance(obj.Proxy, Path.Op.Base.ObjectOp)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -71,9 +72,9 @@ class CommandPathSimpleCopy:
|
||||
)
|
||||
|
||||
FreeCADGui.addModule("PathScripts.PathUtils")
|
||||
FreeCADGui.addModule("PathScripts.PathCustom")
|
||||
FreeCADGui.addModule("Path.Op.Custom")
|
||||
FreeCADGui.doCommand(
|
||||
'obj = PathScripts.PathCustom.Create("'
|
||||
'obj = Path.Op.Custom.Create("'
|
||||
+ selection[0].Name
|
||||
+ '_SimpleCopy")'
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,288 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2020 Russell Johnson (russ4262) <russ4262@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import PathGui as PGui # ensure Path/Gui/Resources are loaded
|
||||
import PathScripts.PathSlot as PathSlot
|
||||
import PathScripts.PathGui as PathGui
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
|
||||
from PySide import QtCore
|
||||
|
||||
__title__ = "Path Slot Operation UI"
|
||||
__author__ = "russ4262 (Russell Johnson)"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Slot operation page controller and command implementation."
|
||||
__contributors__ = ""
|
||||
|
||||
|
||||
DEBUG = False
|
||||
|
||||
|
||||
def debugMsg(msg):
|
||||
global DEBUG
|
||||
if DEBUG:
|
||||
FreeCAD.Console.PrintMessage("PathSlotGui:: " + msg + "\n")
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""Page controller class for the Slot operation."""
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... returns UI"""
|
||||
debugMsg("getForm()")
|
||||
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpSlotEdit.ui")
|
||||
|
||||
def initPage(self, obj):
|
||||
"""initPage(obj) ... Is called after getForm() to initiate the task panel."""
|
||||
debugMsg("initPage()")
|
||||
self.CATS = [None, None]
|
||||
self.propEnums = PathSlot.ObjectSlot.propertyEnumerations(dataType="raw")
|
||||
self.ENUMS = dict()
|
||||
self.setTitle("Slot - " + obj.Label)
|
||||
# retrieve property enumerations
|
||||
# Requirements due to Gui::QuantitySpinBox class use in UI panel
|
||||
self.geo1Extension = PathGui.QuantitySpinBox(
|
||||
self.form.geo1Extension, obj, "ExtendPathStart"
|
||||
)
|
||||
self.geo2Extension = PathGui.QuantitySpinBox(
|
||||
self.form.geo2Extension, obj, "ExtendPathEnd"
|
||||
)
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... transfers obj's property values to UI"""
|
||||
debugMsg("setFields()")
|
||||
debugMsg("... calling updateVisibility()")
|
||||
self.updateVisibility()
|
||||
|
||||
self.updateQuantitySpinBoxes()
|
||||
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.setupCoolant(obj, self.form.coolantController)
|
||||
|
||||
enums = [t[1] for t in self.propEnums["Reference1"]]
|
||||
if "Reference1" in self.ENUMS:
|
||||
enums = self.ENUMS["Reference1"]
|
||||
debugMsg(" -enums1: {}".format(enums))
|
||||
idx = 0
|
||||
if obj.Reference1 in enums:
|
||||
idx = enums.index(obj.Reference1)
|
||||
self.form.geo1Reference.setCurrentIndex(idx)
|
||||
|
||||
enums = [t[1] for t in self.propEnums["Reference2"]]
|
||||
if "Reference2" in self.ENUMS:
|
||||
enums = self.ENUMS["Reference2"]
|
||||
debugMsg(" -enums2: {}".format(enums))
|
||||
idx = 0
|
||||
if obj.Reference2 in enums:
|
||||
idx = enums.index(obj.Reference2)
|
||||
self.form.geo2Reference.setCurrentIndex(idx)
|
||||
|
||||
self.selectInComboBox(obj.LayerMode, self.form.layerMode)
|
||||
self.selectInComboBox(obj.PathOrientation, self.form.pathOrientation)
|
||||
|
||||
if obj.ReverseDirection:
|
||||
self.form.reverseDirection.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
self.form.reverseDirection.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
def updateQuantitySpinBoxes(self):
|
||||
self.geo1Extension.updateSpinBox()
|
||||
self.geo2Extension.updateSpinBox()
|
||||
|
||||
def getFields(self, obj):
|
||||
"""getFields(obj) ... transfers values from UI to obj's proprties"""
|
||||
debugMsg("getFields()")
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
self.updateCoolant(obj, self.form.coolantController)
|
||||
|
||||
obj.Reference1 = str(self.form.geo1Reference.currentText())
|
||||
self.geo1Extension.updateProperty()
|
||||
|
||||
obj.Reference2 = str(self.form.geo2Reference.currentText())
|
||||
self.geo2Extension.updateProperty()
|
||||
|
||||
val = self.propEnums["LayerMode"][self.form.layerMode.currentIndex()][1]
|
||||
obj.LayerMode = val
|
||||
|
||||
val = self.propEnums["PathOrientation"][
|
||||
self.form.pathOrientation.currentIndex()
|
||||
][1]
|
||||
obj.PathOrientation = val
|
||||
|
||||
obj.ReverseDirection = self.form.reverseDirection.isChecked()
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
|
||||
debugMsg("getSignalsForUpdate()")
|
||||
signals = []
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
signals.append(self.form.geo1Extension.editingFinished)
|
||||
signals.append(self.form.geo1Reference.currentIndexChanged)
|
||||
signals.append(self.form.geo2Extension.editingFinished)
|
||||
signals.append(self.form.geo2Reference.currentIndexChanged)
|
||||
signals.append(self.form.layerMode.currentIndexChanged)
|
||||
signals.append(self.form.pathOrientation.currentIndexChanged)
|
||||
signals.append(self.form.reverseDirection.stateChanged)
|
||||
return signals
|
||||
|
||||
def updateVisibility(self, sentObj=None):
|
||||
"""updateVisibility(sentObj=None)... Updates visibility of Tasks panel objects."""
|
||||
# debugMsg('updateVisibility()')
|
||||
hideFeatures = True
|
||||
if hasattr(self.obj, "Base"):
|
||||
if self.obj.Base:
|
||||
self.form.customPoints.hide()
|
||||
self.form.featureReferences.show()
|
||||
self.form.pathOrientation_label.show()
|
||||
self.form.pathOrientation.show()
|
||||
hideFeatures = False
|
||||
base, sublist = self.obj.Base[0]
|
||||
subCnt = len(sublist)
|
||||
|
||||
if subCnt == 1:
|
||||
debugMsg(" -subCnt == 1")
|
||||
# Save value, then reset choices
|
||||
n1 = sublist[0]
|
||||
# s1 = getattr(base.Shape, n1)
|
||||
# Show Reference1 and cusomize options within
|
||||
self.form.geo1Reference.show()
|
||||
self.form.geo1Reference_label.show()
|
||||
self.form.geo1Reference_label.setText("Reference: {}".format(n1))
|
||||
self.customizeReference_1(n1, single=True)
|
||||
# Hide Reference2
|
||||
self.form.geo2Reference.hide()
|
||||
self.form.geo2Reference_label.hide()
|
||||
self.form.geo2Reference_label.setText("End Reference")
|
||||
if self.CATS[1]:
|
||||
self.CATS[1] = None
|
||||
elif subCnt == 2:
|
||||
debugMsg(" -subCnt == 2")
|
||||
n1 = sublist[0]
|
||||
n2 = sublist[1]
|
||||
# s1 = getattr(base.Shape, n1)
|
||||
# s2 = getattr(base.Shape, n2)
|
||||
# Show Reference1 and cusomize options within
|
||||
self.form.geo1Reference.show()
|
||||
self.form.geo1Reference_label.show()
|
||||
self.form.geo1Reference_label.setText(
|
||||
"Start Reference: {}".format(n1)
|
||||
)
|
||||
self.customizeReference_1(n1)
|
||||
# Show Reference2 and cusomize options within
|
||||
self.form.geo2Reference.show()
|
||||
self.form.geo2Reference_label.show()
|
||||
self.form.geo2Reference_label.setText(
|
||||
"End Reference: {}".format(n2)
|
||||
)
|
||||
self.customizeReference_2(n2)
|
||||
else:
|
||||
self.form.pathOrientation_label.hide()
|
||||
self.form.pathOrientation.hide()
|
||||
|
||||
if hideFeatures:
|
||||
# reset values
|
||||
self.CATS = [None, None]
|
||||
self.selectInComboBox("Start to End", self.form.pathOrientation)
|
||||
# hide inputs and show message
|
||||
self.form.featureReferences.hide()
|
||||
self.form.customPoints.show()
|
||||
|
||||
def customizeReference_1(self, sub, single=False):
|
||||
debugMsg("customizeReference_1()")
|
||||
# Customize Reference1 combobox options
|
||||
# by removing unavailable choices
|
||||
cat = sub[:4]
|
||||
if cat != self.CATS[0]:
|
||||
self.CATS[0] = cat
|
||||
slot = PathSlot.ObjectSlot
|
||||
enums = slot._makeReference1Enumerations(slot, sub, single)
|
||||
self.ENUMS["Reference1"] = enums
|
||||
debugMsg("Ref1: {}".format(enums))
|
||||
rawEnums = slot.propertyEnumerations(dataType="raw")["Reference1"]
|
||||
enumTups = [(t, d) for t, d in rawEnums if d in enums]
|
||||
self._updateComboBox(self.form.geo1Reference, enumTups)
|
||||
|
||||
def customizeReference_2(self, sub):
|
||||
debugMsg("customizeReference_2()")
|
||||
# Customize Reference2 combobox options
|
||||
# by removing unavailable choices
|
||||
cat = sub[:4]
|
||||
if cat != self.CATS[1]:
|
||||
self.CATS[1] = cat
|
||||
slot = PathSlot.ObjectSlot
|
||||
enums = slot._makeReference2Enumerations(slot, sub)
|
||||
self.ENUMS["Reference2"] = enums
|
||||
debugMsg("Ref2: {}".format(enums))
|
||||
rawEnums = slot.propertyEnumerations(dataType="raw")["Reference2"]
|
||||
enumTups = [(t, d) for t, d in rawEnums if d in enums]
|
||||
self._updateComboBox(self.form.geo2Reference, enumTups)
|
||||
|
||||
def registerSignalHandlers(self, obj):
|
||||
# debugMsg('registerSignalHandlers()')
|
||||
# self.form.pathOrientation.currentIndexChanged.connect(self.updateVisibility)
|
||||
pass
|
||||
|
||||
def _updateComboBox(self, cBox, enumTups):
|
||||
cBox.blockSignals(True)
|
||||
cBox.clear()
|
||||
for text, data in enumTups: # load enumerations
|
||||
cBox.addItem(text, data)
|
||||
self.selectInSlotComboBox(data, cBox)
|
||||
cBox.blockSignals(False)
|
||||
|
||||
def selectInSlotComboBox(self, name, combo):
|
||||
"""selectInSlotComboBox(name, combo) ...
|
||||
helper function to select a specific value in a combo box."""
|
||||
|
||||
# Search using currentData and return if found
|
||||
newindex = combo.findData(name)
|
||||
if newindex >= 0:
|
||||
combo.setCurrentIndex(newindex)
|
||||
return
|
||||
|
||||
# if not found, search using current text
|
||||
newindex = combo.findText(name, QtCore.Qt.MatchFixedString)
|
||||
if newindex >= 0:
|
||||
combo.setCurrentIndex(newindex)
|
||||
return
|
||||
|
||||
# not found, return unchanged
|
||||
combo.setCurrentIndex(0)
|
||||
return
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Slot",
|
||||
PathSlot.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_Slot",
|
||||
QtCore.QT_TRANSLATE_NOOP("Path_Slot", "Slot"),
|
||||
QtCore.QT_TRANSLATE_NOOP(
|
||||
"Path_Slot", "Create a Slot operation from selected geometry or custom points."
|
||||
),
|
||||
PathSlot.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathSlotGui... done\n")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,289 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 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
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import PathGui as PGui # ensure Path/Gui/Resources are loaded
|
||||
import PathScripts.PathGui as PathGui
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathSurface as PathSurface
|
||||
|
||||
|
||||
__title__ = "Path Surface Operation UI"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecadweb.org"
|
||||
__doc__ = "Surface operation page controller and command implementation."
|
||||
|
||||
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 TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""Page controller class for the Surface operation."""
|
||||
|
||||
def initPage(self, obj):
|
||||
self.setTitle("3D Surface - " + obj.Label)
|
||||
# self.updateVisibility()
|
||||
# retrieve property enumerations
|
||||
# self.propEnums = PathSurface.ObjectSurface.opPropertyEnumerations(False)
|
||||
self.propEnums = PathSurface.ObjectSurface.propertyEnumerations(False)
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... returns UI"""
|
||||
form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpSurfaceEdit.ui")
|
||||
comboToPropertyMap = [
|
||||
("boundBoxSelect", "BoundBox"),
|
||||
("scanType", "ScanType"),
|
||||
("cutPattern", "CutPattern"),
|
||||
("profileEdges", "ProfileEdges"),
|
||||
("layerMode", "LayerMode"),
|
||||
("dropCutterDirSelect", "DropCutterDir"),
|
||||
]
|
||||
enumTups = PathSurface.ObjectSurface.propertyEnumerations(dataType="raw")
|
||||
PathGui.populateCombobox(form, enumTups, comboToPropertyMap)
|
||||
|
||||
return form
|
||||
|
||||
def getFields(self, obj):
|
||||
"""getFields(obj) ... transfers values from UI to obj's proprties"""
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
self.updateCoolant(obj, self.form.coolantController)
|
||||
|
||||
if obj.BoundBox != str(self.form.boundBoxSelect.currentData()):
|
||||
obj.BoundBox = str(self.form.boundBoxSelect.currentData())
|
||||
|
||||
if obj.ScanType != str(self.form.scanType.currentData()):
|
||||
obj.ScanType = str(self.form.scanType.currentData())
|
||||
|
||||
if obj.LayerMode != str(self.form.layerMode.currentData()):
|
||||
obj.LayerMode = str(self.form.layerMode.currentData())
|
||||
|
||||
"""
|
||||
The following method of getting values from the UI form
|
||||
allows for translations of combobox options in the UI.
|
||||
The requirement is that the enumeration lists must
|
||||
be in the same order in both the opPropertyEnumerations() method
|
||||
and the UI panel QComboBox list.
|
||||
Another step to ensure synchronization of the two lists is to
|
||||
populate the list dynamically in this Gui module in `initPage()`
|
||||
using the property enumerations list when loading the UI panel.
|
||||
This type of dynamic combobox population is done for the
|
||||
Tool Controller selection.
|
||||
"""
|
||||
# val = self.propEnums["CutPattern"][self.form.cutPattern.currentIndex()]
|
||||
# if obj.CutPattern != val:
|
||||
# obj.CutPattern = val
|
||||
|
||||
# val = self.propEnums["ProfileEdges"][self.form.profileEdges.currentIndex()]
|
||||
# if obj.ProfileEdges != val:
|
||||
# obj.ProfileEdges = val
|
||||
|
||||
obj.CutPattern = self.form.cutPattern.currentData()
|
||||
obj.ProfileEdges = self.form.profileEdges.currentData()
|
||||
|
||||
if obj.AvoidLastX_Faces != self.form.avoidLastX_Faces.value():
|
||||
obj.AvoidLastX_Faces = self.form.avoidLastX_Faces.value()
|
||||
|
||||
obj.DropCutterExtraOffset.x = FreeCAD.Units.Quantity(
|
||||
self.form.boundBoxExtraOffsetX.text()
|
||||
).Value
|
||||
obj.DropCutterExtraOffset.y = FreeCAD.Units.Quantity(
|
||||
self.form.boundBoxExtraOffsetY.text()
|
||||
).Value
|
||||
|
||||
if obj.DropCutterDir != str(self.form.dropCutterDirSelect.currentData()):
|
||||
obj.DropCutterDir = str(self.form.dropCutterDirSelect.currentData())
|
||||
|
||||
PathGui.updateInputField(obj, "DepthOffset", self.form.depthOffset)
|
||||
|
||||
if obj.StepOver != self.form.stepOver.value():
|
||||
obj.StepOver = self.form.stepOver.value()
|
||||
|
||||
PathGui.updateInputField(obj, "SampleInterval", self.form.sampleInterval)
|
||||
|
||||
if obj.UseStartPoint != self.form.useStartPoint.isChecked():
|
||||
obj.UseStartPoint = self.form.useStartPoint.isChecked()
|
||||
|
||||
if obj.BoundaryEnforcement != self.form.boundaryEnforcement.isChecked():
|
||||
obj.BoundaryEnforcement = self.form.boundaryEnforcement.isChecked()
|
||||
|
||||
if obj.OptimizeLinearPaths != self.form.optimizeEnabled.isChecked():
|
||||
obj.OptimizeLinearPaths = self.form.optimizeEnabled.isChecked()
|
||||
|
||||
if (
|
||||
obj.OptimizeStepOverTransitions
|
||||
!= self.form.optimizeStepOverTransitions.isChecked()
|
||||
):
|
||||
obj.OptimizeStepOverTransitions = (
|
||||
self.form.optimizeStepOverTransitions.isChecked()
|
||||
)
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... transfers obj's property values to UI"""
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.setupCoolant(obj, self.form.coolantController)
|
||||
self.selectInComboBox(obj.BoundBox, self.form.boundBoxSelect)
|
||||
self.selectInComboBox(obj.ScanType, self.form.scanType)
|
||||
self.selectInComboBox(obj.LayerMode, self.form.layerMode)
|
||||
|
||||
"""
|
||||
The following method of setting values in the UI form
|
||||
allows for translations of combobox options in the UI.
|
||||
The requirement is that the enumeration lists must
|
||||
be in the same order in both the opPropertyEnumerations() method
|
||||
and the UI panel QComboBox list.
|
||||
The original method is commented out below.
|
||||
"""
|
||||
# idx = self.propEnums["CutPattern"].index(obj.CutPattern)
|
||||
# self.form.cutPattern.setCurrentIndex(idx)
|
||||
# idx = self.propEnums["ProfileEdges"].index(obj.ProfileEdges)
|
||||
# self.form.profileEdges.setCurrentIndex(idx)
|
||||
self.selectInComboBox(obj.CutPattern, self.form.cutPattern)
|
||||
self.selectInComboBox(obj.ProfileEdges, self.form.profileEdges)
|
||||
|
||||
self.form.avoidLastX_Faces.setValue(obj.AvoidLastX_Faces)
|
||||
self.form.boundBoxExtraOffsetX.setText(
|
||||
FreeCAD.Units.Quantity(
|
||||
obj.DropCutterExtraOffset.x, FreeCAD.Units.Length
|
||||
).UserString
|
||||
)
|
||||
self.form.boundBoxExtraOffsetY.setText(
|
||||
FreeCAD.Units.Quantity(
|
||||
obj.DropCutterExtraOffset.y, FreeCAD.Units.Length
|
||||
).UserString
|
||||
)
|
||||
self.selectInComboBox(obj.DropCutterDir, self.form.dropCutterDirSelect)
|
||||
self.form.depthOffset.setText(
|
||||
FreeCAD.Units.Quantity(
|
||||
obj.DepthOffset.Value, FreeCAD.Units.Length
|
||||
).UserString
|
||||
)
|
||||
self.form.stepOver.setValue(obj.StepOver)
|
||||
self.form.sampleInterval.setText(
|
||||
FreeCAD.Units.Quantity(
|
||||
obj.SampleInterval.Value, FreeCAD.Units.Length
|
||||
).UserString
|
||||
)
|
||||
|
||||
if obj.UseStartPoint:
|
||||
self.form.useStartPoint.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
self.form.useStartPoint.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
if obj.BoundaryEnforcement:
|
||||
self.form.boundaryEnforcement.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
self.form.boundaryEnforcement.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
if obj.OptimizeLinearPaths:
|
||||
self.form.optimizeEnabled.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
self.form.optimizeEnabled.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
if obj.OptimizeStepOverTransitions:
|
||||
self.form.optimizeStepOverTransitions.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
self.form.optimizeStepOverTransitions.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
self.updateVisibility()
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
|
||||
signals = []
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
signals.append(self.form.boundBoxSelect.currentIndexChanged)
|
||||
signals.append(self.form.scanType.currentIndexChanged)
|
||||
signals.append(self.form.layerMode.currentIndexChanged)
|
||||
signals.append(self.form.cutPattern.currentIndexChanged)
|
||||
signals.append(self.form.profileEdges.currentIndexChanged)
|
||||
signals.append(self.form.avoidLastX_Faces.editingFinished)
|
||||
signals.append(self.form.boundBoxExtraOffsetX.editingFinished)
|
||||
signals.append(self.form.boundBoxExtraOffsetY.editingFinished)
|
||||
signals.append(self.form.dropCutterDirSelect.currentIndexChanged)
|
||||
signals.append(self.form.depthOffset.editingFinished)
|
||||
signals.append(self.form.stepOver.editingFinished)
|
||||
signals.append(self.form.sampleInterval.editingFinished)
|
||||
signals.append(self.form.useStartPoint.stateChanged)
|
||||
signals.append(self.form.boundaryEnforcement.stateChanged)
|
||||
signals.append(self.form.optimizeEnabled.stateChanged)
|
||||
signals.append(self.form.optimizeStepOverTransitions.stateChanged)
|
||||
|
||||
return signals
|
||||
|
||||
def updateVisibility(self, sentObj=None):
|
||||
"""updateVisibility(sentObj=None)... Updates visibility of Tasks panel objects."""
|
||||
if self.form.scanType.currentText() == "Planar":
|
||||
self.form.cutPattern.show()
|
||||
self.form.cutPattern_label.show()
|
||||
self.form.optimizeStepOverTransitions.show()
|
||||
if hasattr(self.form, "profileEdges"):
|
||||
self.form.profileEdges.show()
|
||||
self.form.profileEdges_label.show()
|
||||
self.form.avoidLastX_Faces.show()
|
||||
self.form.avoidLastX_Faces_label.show()
|
||||
|
||||
self.form.boundBoxExtraOffsetX.hide()
|
||||
self.form.boundBoxExtraOffsetY.hide()
|
||||
self.form.boundBoxExtraOffset_label.hide()
|
||||
self.form.dropCutterDirSelect.hide()
|
||||
self.form.dropCutterDirSelect_label.hide()
|
||||
elif self.form.scanType.currentText() == "Rotational":
|
||||
self.form.cutPattern.hide()
|
||||
self.form.cutPattern_label.hide()
|
||||
self.form.optimizeStepOverTransitions.hide()
|
||||
if hasattr(self.form, "profileEdges"):
|
||||
self.form.profileEdges.hide()
|
||||
self.form.profileEdges_label.hide()
|
||||
self.form.avoidLastX_Faces.hide()
|
||||
self.form.avoidLastX_Faces_label.hide()
|
||||
|
||||
self.form.boundBoxExtraOffsetX.show()
|
||||
self.form.boundBoxExtraOffsetY.show()
|
||||
self.form.boundBoxExtraOffset_label.show()
|
||||
self.form.dropCutterDirSelect.show()
|
||||
self.form.dropCutterDirSelect_label.show()
|
||||
|
||||
def registerSignalHandlers(self, obj):
|
||||
self.form.scanType.currentIndexChanged.connect(self.updateVisibility)
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Surface",
|
||||
PathSurface.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_3DSurface",
|
||||
QtCore.QT_TRANSLATE_NOOP("Path_Surface", "3D Surface"),
|
||||
QtCore.QT_TRANSLATE_NOOP(
|
||||
"Path_Surface", "Create a 3D Surface Operation from a model"
|
||||
),
|
||||
PathSurface.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathSurfaceGui... done\n")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,548 +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 __future__ import print_function
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
import PathScripts.PathCircularHoleBase as PathCircularHoleBase
|
||||
import PathScripts.PathGeom as PathGeom
|
||||
import PathScripts.PathOp as PathOp
|
||||
import Generators.threadmilling_generator as threadmilling
|
||||
import math
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
__title__ = "Path Thread Milling Operation"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Path thread milling operation."
|
||||
|
||||
# math.sqrt(3)/2 ... 60deg triangle height
|
||||
SQRT_3_DIVIDED_BY_2 = 0.8660254037844386
|
||||
|
||||
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
|
||||
|
||||
# Constants
|
||||
LeftHand = "LeftHand"
|
||||
RightHand = "RightHand"
|
||||
ThreadTypeCustomExternal = "CustomExternal"
|
||||
ThreadTypeCustomInternal = "CustomInternal"
|
||||
ThreadTypeImperialExternal2A = "ImperialExternal2A"
|
||||
ThreadTypeImperialExternal3A = "ImperialExternal3A"
|
||||
ThreadTypeImperialInternal2B = "ImperialInternal2B"
|
||||
ThreadTypeImperialInternal3B = "ImperialInternal3B"
|
||||
ThreadTypeMetricExternal4G6G = "MetricExternal4G6G"
|
||||
ThreadTypeMetricExternal6G = "MetricExternal6G"
|
||||
ThreadTypeMetricInternal6H = "MetricInternal6H"
|
||||
DirectionClimb = "Climb"
|
||||
DirectionConventional = "Conventional"
|
||||
|
||||
ThreadOrientations = [LeftHand, RightHand]
|
||||
|
||||
ThreadTypeData = {
|
||||
ThreadTypeImperialExternal2A: "imperial-external-2A.csv",
|
||||
ThreadTypeImperialExternal3A: "imperial-external-3A.csv",
|
||||
ThreadTypeImperialInternal2B: "imperial-internal-2B.csv",
|
||||
ThreadTypeImperialInternal3B: "imperial-internal-3B.csv",
|
||||
ThreadTypeMetricExternal4G6G: "metric-external-4G6G.csv",
|
||||
ThreadTypeMetricExternal6G: "metric-external-6G.csv",
|
||||
ThreadTypeMetricInternal6H: "metric-internal-6H.csv",
|
||||
}
|
||||
|
||||
ThreadTypesExternal = [
|
||||
ThreadTypeCustomExternal,
|
||||
ThreadTypeImperialExternal2A,
|
||||
ThreadTypeImperialExternal3A,
|
||||
ThreadTypeMetricExternal4G6G,
|
||||
ThreadTypeMetricExternal6G,
|
||||
]
|
||||
ThreadTypesInternal = [
|
||||
ThreadTypeCustomInternal,
|
||||
ThreadTypeImperialInternal2B,
|
||||
ThreadTypeImperialInternal3B,
|
||||
ThreadTypeMetricInternal6H,
|
||||
]
|
||||
ThreadTypesImperial = [
|
||||
ThreadTypeImperialExternal2A,
|
||||
ThreadTypeImperialExternal3A,
|
||||
ThreadTypeImperialInternal2B,
|
||||
ThreadTypeImperialInternal3B,
|
||||
]
|
||||
ThreadTypesMetric = [
|
||||
ThreadTypeMetricExternal4G6G,
|
||||
ThreadTypeMetricExternal6G,
|
||||
ThreadTypeMetricInternal6H,
|
||||
]
|
||||
ThreadTypes = ThreadTypesInternal + ThreadTypesExternal
|
||||
Directions = [DirectionClimb, DirectionConventional]
|
||||
|
||||
def _isThreadInternal(obj):
|
||||
return obj.ThreadType in ThreadTypesInternal
|
||||
|
||||
def threadSetupInternal(obj, zTop, zBottom):
|
||||
Path.Log.track()
|
||||
if obj.ThreadOrientation == RightHand:
|
||||
# Right hand thread, G2, top down -> conventional milling
|
||||
if obj.Direction == DirectionConventional:
|
||||
return ("G2", zTop, zBottom)
|
||||
# For climb milling we need to cut the thread from the bottom up
|
||||
# in the opposite direction -> G3
|
||||
return ("G3", zBottom, zTop)
|
||||
# Left hand thread, G3, top down -> climb milling
|
||||
if obj.Direction == DirectionClimb:
|
||||
return ("G3", zTop, zBottom)
|
||||
# for conventional milling, cut bottom up with G2
|
||||
return ("G2", zBottom, zTop)
|
||||
|
||||
def threadSetupExternal(obj, zTop, zBottom):
|
||||
Path.Log.track()
|
||||
if obj.ThreadOrientation == RightHand:
|
||||
# right hand thread, G2, top down -> climb milling
|
||||
if obj.Direction == DirectionClimb:
|
||||
return ("G2", zTop, zBottom)
|
||||
# for conventional, mill bottom up the other way around
|
||||
return ("G3", zBottom, zTop)
|
||||
# left hand thread, G3, top down -> conventional milling
|
||||
if obj.Direction == DirectionConventional:
|
||||
return ("G3", zTop, zBottom)
|
||||
# for climb milling need to go bottom up and the other way
|
||||
return ("G2", zBottom, zTop)
|
||||
|
||||
def threadSetup(obj):
|
||||
"""Return (cmd, zbegin, zend) of thread milling operation"""
|
||||
Path.Log.track()
|
||||
|
||||
zTop = obj.StartDepth.Value
|
||||
zBottom = obj.FinalDepth.Value
|
||||
|
||||
if _isThreadInternal(obj):
|
||||
return threadSetupInternal(obj, zTop, zBottom)
|
||||
else:
|
||||
return threadSetupExternal(obj, zTop, zBottom)
|
||||
|
||||
|
||||
def threadRadii(internal, majorDia, minorDia, toolDia, toolCrest):
|
||||
"""threadRadii(majorDia, minorDia, toolDia, toolCrest) ... returns the minimum and maximum radius for thread."""
|
||||
Path.Log.track(internal, majorDia, minorDia, toolDia, toolCrest)
|
||||
if toolCrest is None:
|
||||
toolCrest = 0.0
|
||||
# As it turns out metric and imperial standard threads follow the same rules.
|
||||
# The determining factor is the height of the full 60 degree triangle H.
|
||||
# - The minor diameter is 1/4 * H smaller than the pitch diameter.
|
||||
# - The major diameter is 3/8 * H bigger than the pitch diameter
|
||||
# Since we already have the outer diameter it's simpler to just add 1/8 * H
|
||||
# to get the outer tip of the thread.
|
||||
H = ((majorDia - minorDia) / 2.0) * 1.6 # (D - d)/2 = 5/8 * H
|
||||
if internal:
|
||||
# mill inside out
|
||||
outerTip = majorDia / 2.0 + H / 8.0
|
||||
# Compensate for the crest of the tool
|
||||
toolTip = outerTip - toolCrest * SQRT_3_DIVIDED_BY_2
|
||||
radii = ((minorDia - toolDia) / 2.0, toolTip - toolDia / 2.0)
|
||||
else:
|
||||
# mill outside in
|
||||
innerTip = minorDia / 2.0 - H / 4.0
|
||||
# Compensate for the crest of the tool
|
||||
toolTip = innerTip - toolCrest * SQRT_3_DIVIDED_BY_2
|
||||
radii = ((majorDia + toolDia) / 2.0, toolTip + toolDia / 2.0)
|
||||
Path.Log.track(radii)
|
||||
return radii
|
||||
|
||||
|
||||
def threadPasses(count, radii, internal, majorDia, minorDia, toolDia, toolCrest):
|
||||
Path.Log.track(count, radii, internal, majorDia, minorDia, toolDia, toolCrest)
|
||||
# the logic goes as follows, total area to be removed:
|
||||
# A = H * W ... where H is the depth and W is half the width of a thread
|
||||
# H = k * sin(30) = k * 1/2 -> k = 2 * H
|
||||
# W = k * cos(30) = k * sqrt(3)/2
|
||||
# -> W = (2 * H) * sqrt(3) / 2 = H * sqrt(3)
|
||||
# A = sqrt(3) * H^2
|
||||
# Each pass has to remove the same area
|
||||
# An = A / count = sqrt(3) * H^2 / count
|
||||
# Because each successive pass doesn't have to remove the aera of the previous
|
||||
# passes the result for the height:
|
||||
# Ai = (i + 1) * An = (i + 1) * sqrt(3) * Hi^2 = sqrt(3) * H^2 / count
|
||||
# Hi = sqrt(H^2 * (i + 1) / count)
|
||||
# Hi = H * sqrt((i + 1) / count)
|
||||
minor, major = radii(internal, majorDia, minorDia, toolDia, toolCrest)
|
||||
H = float(major - minor)
|
||||
Hi = [H * math.sqrt((i + 1) / count) for i in range(count)]
|
||||
|
||||
# For external threads threadRadii returns the radii in reverse order because that's
|
||||
# the order in which they have to get milled. As a result H ends up being negative
|
||||
# and the math for internal and external threads is identical.
|
||||
passes = [minor + h for h in Hi]
|
||||
Path.Log.debug(f"threadPasses({minor}, {major}) -> H={H} : {Hi} --> {passes}")
|
||||
|
||||
return passes
|
||||
|
||||
|
||||
def elevatorRadius(obj, center, internal, tool):
|
||||
"""elevatorLocation(obj, center, internal, tool) ... return suitable location for the tool elevator"""
|
||||
|
||||
if internal:
|
||||
dy = float(obj.MinorDiameter - tool.Diameter) / 2 - 1
|
||||
if dy < 0:
|
||||
if obj.MinorDiameter < tool.Diameter:
|
||||
Path.Log.error(
|
||||
"The selected tool is too big (d={}) for milling a thread with minor diameter D={}".format(
|
||||
tool.Diameter, obj.MinorDiameter
|
||||
)
|
||||
)
|
||||
dy = 0
|
||||
else:
|
||||
dy = float(obj.MajorDiameter + tool.Diameter) / 2 + 1
|
||||
|
||||
return dy
|
||||
|
||||
|
||||
class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
|
||||
"""Proxy object for thread milling operation."""
|
||||
|
||||
@classmethod
|
||||
def propertyEnumerations(self, dataType="data"):
|
||||
"""helixOpPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
|
||||
Args:
|
||||
dataType = 'data', 'raw', 'translated'
|
||||
Notes:
|
||||
'data' is list of internal string literals used in code
|
||||
'raw' is list of (translated_text, data_string) tuples
|
||||
'translated' is list of translated string literals
|
||||
"""
|
||||
Path.Log.track()
|
||||
|
||||
# Enumeration lists for App::PropertyEnumeration properties
|
||||
enums = {
|
||||
"ThreadType": [
|
||||
(
|
||||
translate("Path_ThreadMilling", "Custom External"),
|
||||
ThreadTypeCustomExternal,
|
||||
),
|
||||
(
|
||||
translate("Path_ThreadMilling", "Custom Internal"),
|
||||
ThreadTypeCustomInternal,
|
||||
),
|
||||
(
|
||||
translate("Path_ThreadMilling", "Imperial External (2A)"),
|
||||
ThreadTypeImperialExternal2A,
|
||||
),
|
||||
(
|
||||
translate("Path_ThreadMilling", "Imperial External (3A)"),
|
||||
ThreadTypeImperialExternal3A,
|
||||
),
|
||||
(
|
||||
translate("Path_ThreadMilling", "Imperial Internal (2B)"),
|
||||
ThreadTypeImperialInternal2B,
|
||||
),
|
||||
(
|
||||
translate("Path_ThreadMilling", "Imperial Internal (3B)"),
|
||||
ThreadTypeImperialInternal3B,
|
||||
),
|
||||
(
|
||||
translate("Path_ThreadMilling", "Metric External (4G6G)"),
|
||||
ThreadTypeMetricExternal4G6G,
|
||||
),
|
||||
(
|
||||
translate("Path_ThreadMilling", "Metric External (6G)"),
|
||||
ThreadTypeMetricExternal6G,
|
||||
),
|
||||
(
|
||||
translate("Path_ThreadMilling", "Metric Internal (6H)"),
|
||||
ThreadTypeMetricInternal6H,
|
||||
),
|
||||
],
|
||||
"ThreadOrientation": [
|
||||
(
|
||||
translate("Path_ThreadMilling", "LeftHand"),
|
||||
LeftHand,
|
||||
),
|
||||
(
|
||||
translate("Path_ThreadMilling", "RightHand"),
|
||||
RightHand,
|
||||
),
|
||||
],
|
||||
"Direction": [
|
||||
(
|
||||
translate("Path_ThreadMilling", "Climb"),
|
||||
DirectionClimb,
|
||||
),
|
||||
(
|
||||
translate("Path_ThreadMilling", "Conventional"),
|
||||
DirectionConventional,
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
if dataType == "raw":
|
||||
return enums
|
||||
|
||||
data = list()
|
||||
idx = 0 if dataType == "translated" else 1
|
||||
|
||||
Path.Log.debug(enums)
|
||||
|
||||
for k, v in enumerate(enums):
|
||||
data.append((v, [tup[idx] for tup in enums[v]]))
|
||||
Path.Log.debug(data)
|
||||
|
||||
return data
|
||||
|
||||
def circularHoleFeatures(self, obj):
|
||||
Path.Log.track()
|
||||
return PathOp.FeatureBaseGeometry
|
||||
|
||||
def initCircularHoleOperation(self, obj):
|
||||
Path.Log.track()
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"ThreadOrientation",
|
||||
"Thread",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Set thread orientation"),
|
||||
)
|
||||
# obj.ThreadOrientation = ThreadOrientations
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"ThreadType",
|
||||
"Thread",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Currently only internal"),
|
||||
)
|
||||
# obj.ThreadType = ThreadTypes
|
||||
obj.addProperty(
|
||||
"App::PropertyString",
|
||||
"ThreadName",
|
||||
"Thread",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Defines which standard thread was chosen"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyLength",
|
||||
"MajorDiameter",
|
||||
"Thread",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Set thread's major diameter"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyLength",
|
||||
"MinorDiameter",
|
||||
"Thread",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Set thread's minor diameter"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyLength",
|
||||
"Pitch",
|
||||
"Thread",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Set thread's pitch - used for metric threads"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyInteger",
|
||||
"TPI",
|
||||
"Thread",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Set thread's TPI (turns per inch) - used for imperial threads",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyInteger",
|
||||
"ThreadFit",
|
||||
"Thread",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Set how many passes are used to cut the thread"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyInteger",
|
||||
"Passes",
|
||||
"Operation",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Set how many passes are used to cut the thread"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"Direction",
|
||||
"Operation",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Direction of thread cutting operation"),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"LeadInOut",
|
||||
"Operation",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Set to True to get lead in and lead out arcs at the start and end of the thread cut",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyLink",
|
||||
"ClearanceOp",
|
||||
"Operation",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Operation to clear the inside of the thread"
|
||||
),
|
||||
)
|
||||
|
||||
for n in self.propertyEnumerations():
|
||||
setattr(obj, n[0], n[1])
|
||||
|
||||
def threadPassRadii(self, obj):
|
||||
Path.Log.track(obj.Label)
|
||||
rMajor = (obj.MajorDiameter.Value - self.tool.Diameter) / 2.0
|
||||
rMinor = (obj.MinorDiameter.Value - self.tool.Diameter) / 2.0
|
||||
if obj.Passes < 1:
|
||||
obj.Passes = 1
|
||||
rPass = (rMajor - rMinor) / obj.Passes
|
||||
passes = [rMajor]
|
||||
for i in range(1, obj.Passes):
|
||||
passes.append(rMajor - rPass * i)
|
||||
return list(reversed(passes))
|
||||
|
||||
def executeThreadMill(self, obj, loc, gcode, zStart, zFinal, pitch):
|
||||
Path.Log.track(obj.Label, loc, gcode, zStart, zFinal, pitch)
|
||||
elevator = elevatorRadius(obj, loc, _isThreadInternal(obj), self.tool)
|
||||
|
||||
move2clearance = Path.Command(
|
||||
"G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}
|
||||
)
|
||||
self.commandlist.append(move2clearance)
|
||||
|
||||
start = None
|
||||
for radius in threadPasses(
|
||||
obj.Passes,
|
||||
threadRadii,
|
||||
_isThreadInternal(obj),
|
||||
obj.MajorDiameter.Value,
|
||||
obj.MinorDiameter.Value,
|
||||
float(self.tool.Diameter),
|
||||
float(self.tool.Crest),
|
||||
):
|
||||
if (
|
||||
not start is None
|
||||
and not _isThreadInternal(obj)
|
||||
and not obj.LeadInOut
|
||||
):
|
||||
# external thread without lead in/out have to go up and over
|
||||
# in other words we need a move to clearance and not take any
|
||||
# shortcuts when moving to the elevator position
|
||||
self.commandlist.append(move2clearance)
|
||||
start = None
|
||||
commands, start = threadmilling.generate(
|
||||
loc,
|
||||
gcode,
|
||||
zStart,
|
||||
zFinal,
|
||||
pitch,
|
||||
radius,
|
||||
obj.LeadInOut,
|
||||
elevator,
|
||||
start,
|
||||
)
|
||||
|
||||
for cmd in commands:
|
||||
p = cmd.Parameters
|
||||
if cmd.Name in ["G0"]:
|
||||
p.update({"F": self.vertRapid})
|
||||
if cmd.Name in ["G1", "G2", "G3"]:
|
||||
p.update({"F": self.horizFeed})
|
||||
cmd.Parameters = p
|
||||
self.commandlist.extend(commands)
|
||||
|
||||
self.commandlist.append(
|
||||
Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid})
|
||||
)
|
||||
|
||||
def circularHoleExecute(self, obj, holes):
|
||||
Path.Log.track()
|
||||
if self.isToolSupported(obj, self.tool):
|
||||
self.commandlist.append(Path.Command("(Begin Thread Milling)"))
|
||||
|
||||
(cmd, zStart, zFinal) = threadSetup(obj)
|
||||
pitch = obj.Pitch.Value
|
||||
if obj.TPI > 0:
|
||||
pitch = 25.4 / obj.TPI
|
||||
if pitch <= 0:
|
||||
Path.Log.error("Cannot create thread with pitch {}".format(pitch))
|
||||
return
|
||||
|
||||
# rapid to clearance height
|
||||
for loc in holes:
|
||||
self.executeThreadMill(
|
||||
obj,
|
||||
FreeCAD.Vector(loc["x"], loc["y"], 0),
|
||||
cmd,
|
||||
zStart,
|
||||
zFinal,
|
||||
pitch,
|
||||
)
|
||||
else:
|
||||
Path.Log.error("No suitable Tool found for thread milling operation")
|
||||
|
||||
def opSetDefaultValues(self, obj, job):
|
||||
Path.Log.track()
|
||||
obj.ThreadOrientation = RightHand
|
||||
obj.ThreadType = ThreadTypeMetricInternal6H
|
||||
obj.ThreadFit = 50
|
||||
obj.Pitch = 1
|
||||
obj.TPI = 0
|
||||
obj.Passes = 1
|
||||
obj.Direction = DirectionClimb
|
||||
obj.LeadInOut = False
|
||||
|
||||
def isToolSupported(self, obj, tool):
|
||||
"""Thread milling only supports thread milling cutters."""
|
||||
support = hasattr(tool, "Diameter") and hasattr(tool, "Crest")
|
||||
Path.Log.track(tool.Label, support)
|
||||
return support
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
setup = []
|
||||
setup.append("ThreadOrientation")
|
||||
setup.append("ThreadType")
|
||||
setup.append("ThreadName")
|
||||
setup.append("ThreadFit")
|
||||
setup.append("MajorDiameter")
|
||||
setup.append("MinorDiameter")
|
||||
setup.append("Pitch")
|
||||
setup.append("TPI")
|
||||
setup.append("Passes")
|
||||
setup.append("Direction")
|
||||
setup.append("LeadInOut")
|
||||
return setup
|
||||
|
||||
|
||||
def Create(name, obj=None, parentJob=None):
|
||||
"""Create(name) ... Creates and returns a thread milling operation."""
|
||||
if obj is None:
|
||||
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
||||
obj.Proxy = ObjectThreadMilling(obj, name, parentJob)
|
||||
if obj.Proxy:
|
||||
obj.Proxy.findAllHoles(obj)
|
||||
return obj
|
||||
@@ -1,267 +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 *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import PathGui as PGui # ensure Path/Gui/Resources are loaded
|
||||
import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui
|
||||
import PathScripts.PathThreadMilling as PathThreadMilling
|
||||
import PathScripts.PathGui as PathGui
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import csv
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
|
||||
from PySide import QtCore
|
||||
|
||||
__title__ = "Path Thread Milling Operation UI."
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "UI and Command for Path Thread Milling Operation."
|
||||
|
||||
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 fillThreads(form, dataFile, defaultSelect):
|
||||
form.threadName.blockSignals(True)
|
||||
select = form.threadName.currentText()
|
||||
Path.Log.debug("select = '{}'".format(select))
|
||||
form.threadName.clear()
|
||||
with open(
|
||||
"{}Mod/Path/Data/Threads/{}".format(FreeCAD.getHomePath(), dataFile)
|
||||
) as fp:
|
||||
reader = csv.DictReader(fp)
|
||||
for row in reader:
|
||||
form.threadName.addItem(row["name"], row)
|
||||
if select:
|
||||
form.threadName.setCurrentText(select)
|
||||
elif defaultSelect:
|
||||
form.threadName.setCurrentText(defaultSelect)
|
||||
form.threadName.setEnabled(True)
|
||||
form.threadName.blockSignals(False)
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
|
||||
"""Controller for the thread milling operation's page"""
|
||||
|
||||
def initPage(self, obj):
|
||||
self.majorDia = PathGui.QuantitySpinBox(
|
||||
self.form.threadMajor, obj, "MajorDiameter"
|
||||
)
|
||||
self.minorDia = PathGui.QuantitySpinBox(
|
||||
self.form.threadMinor, obj, "MinorDiameter"
|
||||
)
|
||||
self.pitch = PathGui.QuantitySpinBox(self.form.threadPitch, obj, "Pitch")
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... return UI"""
|
||||
form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpThreadMillingEdit.ui")
|
||||
comboToPropertyMap = [
|
||||
("threadOrientation", "ThreadOrientation"),
|
||||
("threadType", "ThreadType"),
|
||||
("opDirection", "Direction"),
|
||||
]
|
||||
enumTups = PathThreadMilling.ObjectThreadMilling.propertyEnumerations(
|
||||
dataType="raw"
|
||||
)
|
||||
self.populateCombobox(form, enumTups, comboToPropertyMap)
|
||||
|
||||
return form
|
||||
|
||||
def getFields(self, obj):
|
||||
"""getFields(obj) ... update obj's properties with values from the UI"""
|
||||
Path.Log.track()
|
||||
|
||||
self.majorDia.updateProperty()
|
||||
self.minorDia.updateProperty()
|
||||
self.pitch.updateProperty()
|
||||
|
||||
obj.ThreadOrientation = self.form.threadOrientation.currentData()
|
||||
obj.ThreadType = self.form.threadType.currentData()
|
||||
obj.ThreadName = self.form.threadName.currentText()
|
||||
obj.Direction = self.form.opDirection.currentData()
|
||||
obj.Passes = self.form.opPasses.value()
|
||||
obj.LeadInOut = self.form.leadInOut.checkState() == QtCore.Qt.Checked
|
||||
obj.TPI = self.form.threadTPI.value()
|
||||
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... update UI with obj properties' values"""
|
||||
Path.Log.track()
|
||||
|
||||
self.selectInComboBox(obj.ThreadOrientation, self.form.threadOrientation)
|
||||
self.selectInComboBox(obj.ThreadType, self.form.threadType)
|
||||
self.selectInComboBox(obj.Direction, self.form.opDirection)
|
||||
|
||||
self.form.threadName.blockSignals(True)
|
||||
self.form.threadName.setCurrentText(obj.ThreadName)
|
||||
self.form.threadName.blockSignals(False)
|
||||
self.form.threadTPI.setValue(obj.TPI)
|
||||
|
||||
self.form.opPasses.setValue(obj.Passes)
|
||||
self.form.leadInOut.setCheckState(
|
||||
QtCore.Qt.Checked if obj.LeadInOut else QtCore.Qt.Unchecked
|
||||
)
|
||||
|
||||
self.majorDia.updateSpinBox()
|
||||
self.minorDia.updateSpinBox()
|
||||
self.pitch.updateSpinBox()
|
||||
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self._updateFromThreadType()
|
||||
|
||||
def _isThreadCustom(self):
|
||||
return self.form.threadType.currentData() in [
|
||||
PathThreadMilling.ThreadTypeCustomInternal,
|
||||
PathThreadMilling.ThreadTypeCustomExternal,
|
||||
]
|
||||
|
||||
def _isThreadImperial(self):
|
||||
return (
|
||||
self.form.threadType.currentData()
|
||||
in PathThreadMilling.ThreadTypesImperial
|
||||
)
|
||||
|
||||
def _isThreadMetric(self):
|
||||
return (
|
||||
self.form.threadType.currentData()
|
||||
in PathThreadMilling.ThreadTypesMetric
|
||||
)
|
||||
|
||||
def _isThreadInternal(self):
|
||||
return (
|
||||
self.form.threadType.currentData()
|
||||
in PathThreadMilling.ThreadTypesInternal
|
||||
)
|
||||
|
||||
def _isThreadExternal(self):
|
||||
return (
|
||||
self.form.threadType.currentData()
|
||||
in PathThreadMilling.ThreadTypesExternal
|
||||
)
|
||||
|
||||
def _updateFromThreadType(self):
|
||||
|
||||
if self._isThreadCustom():
|
||||
self.form.threadName.setEnabled(False)
|
||||
self.form.threadFit.setEnabled(False)
|
||||
self.form.threadFitLabel.setEnabled(False)
|
||||
self.form.threadPitch.setEnabled(True)
|
||||
self.form.threadPitchLabel.setEnabled(True)
|
||||
self.form.threadTPI.setEnabled(True)
|
||||
self.form.threadTPILabel.setEnabled(True)
|
||||
else:
|
||||
self.form.threadFit.setEnabled(True)
|
||||
self.form.threadFitLabel.setEnabled(True)
|
||||
if self._isThreadMetric():
|
||||
self.form.threadPitch.setEnabled(True)
|
||||
self.form.threadPitchLabel.setEnabled(True)
|
||||
self.form.threadTPI.setEnabled(False)
|
||||
self.form.threadTPILabel.setEnabled(False)
|
||||
self.form.threadTPI.setValue(0)
|
||||
else:
|
||||
self.form.threadPitch.setEnabled(False)
|
||||
self.form.threadPitchLabel.setEnabled(False)
|
||||
self.form.threadTPI.setEnabled(True)
|
||||
self.form.threadTPILabel.setEnabled(True)
|
||||
self.pitch.updateSpinBox(0)
|
||||
fillThreads(
|
||||
self.form,
|
||||
PathThreadMilling.ThreadTypeData[
|
||||
self.form.threadType.currentData()
|
||||
],
|
||||
self.obj.ThreadName,
|
||||
)
|
||||
self._updateFromThreadName()
|
||||
|
||||
def _updateFromThreadName(self):
|
||||
if not self._isThreadCustom():
|
||||
thread = self.form.threadName.currentData()
|
||||
fit = float(self.form.threadFit.value()) / 100
|
||||
maxmin = float(thread["dMajorMin"])
|
||||
maxmax = float(thread["dMajorMax"])
|
||||
major = maxmin + (maxmax - maxmin) * fit
|
||||
minmin = float(thread["dMinorMin"])
|
||||
minmax = float(thread["dMinorMax"])
|
||||
minor = minmin + (minmax - minmin) * fit
|
||||
|
||||
if self._isThreadMetric():
|
||||
pitch = float(thread["pitch"])
|
||||
self.pitch.updateSpinBox(pitch)
|
||||
|
||||
if self._isThreadImperial():
|
||||
tpi = int(thread["tpi"])
|
||||
self.form.threadTPI.setValue(tpi)
|
||||
minor = minor * 25.4
|
||||
major = major * 25.4
|
||||
|
||||
self.majorDia.updateSpinBox(major)
|
||||
self.minorDia.updateSpinBox(minor)
|
||||
|
||||
self.setDirty()
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals which cause the receiver to update the model"""
|
||||
signals = []
|
||||
|
||||
signals.append(self.form.threadMajor.editingFinished)
|
||||
signals.append(self.form.threadMinor.editingFinished)
|
||||
signals.append(self.form.threadPitch.editingFinished)
|
||||
signals.append(self.form.threadOrientation.currentIndexChanged)
|
||||
signals.append(self.form.threadTPI.editingFinished)
|
||||
signals.append(self.form.opDirection.currentIndexChanged)
|
||||
signals.append(self.form.opPasses.editingFinished)
|
||||
signals.append(self.form.leadInOut.stateChanged)
|
||||
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
|
||||
return signals
|
||||
|
||||
def registerSignalHandlers(self, obj):
|
||||
self.form.threadType.currentIndexChanged.connect(self._updateFromThreadType)
|
||||
self.form.threadName.currentIndexChanged.connect(self._updateFromThreadName)
|
||||
self.form.threadFit.valueChanged.connect(self._updateFromThreadName)
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"ThreadMilling",
|
||||
PathThreadMilling.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_ThreadMilling",
|
||||
QT_TRANSLATE_NOOP("Path_ThreadMilling", "Thread Milling"),
|
||||
QT_TRANSLATE_NOOP(
|
||||
"Path_ThreadMilling",
|
||||
"Creates a Path Thread Milling operation from features of a base object",
|
||||
),
|
||||
PathThreadMilling.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathThreadMillingGui ... done\n")
|
||||
@@ -24,7 +24,6 @@
|
||||
import FreeCAD
|
||||
from FreeCAD import Vector
|
||||
from PySide import QtCore
|
||||
from PySide import QtGui
|
||||
import Path
|
||||
import PathScripts.PathGeom as PathGeom
|
||||
import PathScripts.PathJob as PathJob
|
||||
@@ -64,6 +63,7 @@ def waiting_effects(function):
|
||||
def new_function(*args, **kwargs):
|
||||
if not FreeCAD.GuiUp:
|
||||
return function(*args, **kwargs)
|
||||
from PySide import QtGui
|
||||
QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
|
||||
res = None
|
||||
try:
|
||||
@@ -299,7 +299,7 @@ def getOffsetArea(
|
||||
):
|
||||
"""Make an offset area of a shape, projected onto a plane.
|
||||
Positive offsets expand the area, negative offsets shrink it.
|
||||
Inspired by _buildPathArea() from PathAreaOp.py module. Adjustments made
|
||||
Inspired by _buildPathArea() from Path.Op.Area.py module. Adjustments made
|
||||
based on notes by @sliptonic at this webpage:
|
||||
https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes."""
|
||||
Path.Log.debug("getOffsetArea()")
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
import FreeCADGui
|
||||
import FreeCAD
|
||||
import Path
|
||||
import Path.Tools.Controller as PathToolsController
|
||||
import Path.Tools.Controller as PathToolController
|
||||
import PathGui as PGui # ensure Path/Gui/Resources are loaded
|
||||
import PathScripts.PathJobCmd as PathJobCmd
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
|
||||
@@ -1,423 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2020 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import Part
|
||||
import Path
|
||||
import PathScripts.PathEngraveBase as PathEngraveBase
|
||||
import PathScripts.PathOp as PathOp
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
import PathScripts.PathGeom as PathGeom
|
||||
import PathScripts.PathPreferences as PathPreferences
|
||||
import math
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
|
||||
from PySide import QtCore
|
||||
|
||||
__doc__ = "Class and implementation of Path Vcarve operation"
|
||||
|
||||
PRIMARY = 0
|
||||
SECONDARY = 1
|
||||
EXTERIOR1 = 2
|
||||
EXTERIOR2 = 3
|
||||
COLINEAR = 4
|
||||
TWIN = 5
|
||||
BORDERLINE = 6
|
||||
|
||||
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
|
||||
_sorting = "global"
|
||||
|
||||
|
||||
def _collectVoronoiWires(vd):
|
||||
edges = [e for e in vd.Edges if e.Color == PRIMARY]
|
||||
vertex = {}
|
||||
for e in edges:
|
||||
for v in e.Vertices:
|
||||
i = v.Index
|
||||
j = vertex.get(i, [])
|
||||
j.append(e)
|
||||
vertex[i] = j
|
||||
|
||||
# knots are the start and end points of a wire
|
||||
knots = [i for i in vertex if len(vertex[i]) == 1]
|
||||
knots.extend([i for i in vertex if len(vertex[i]) > 2])
|
||||
if len(knots) == 0:
|
||||
for i in vertex:
|
||||
if len(vertex[i]) > 0:
|
||||
knots.append(i)
|
||||
break
|
||||
|
||||
def consume(v, edge):
|
||||
vertex[v] = [e for e in vertex[v] if e.Index != edge.Index]
|
||||
return len(vertex[v]) == 0
|
||||
|
||||
def traverse(vStart, edge, edges):
|
||||
if vStart == edge.Vertices[0].Index:
|
||||
vEnd = edge.Vertices[1].Index
|
||||
edges.append(edge)
|
||||
else:
|
||||
vEnd = edge.Vertices[0].Index
|
||||
edges.append(edge.Twin)
|
||||
|
||||
consume(vStart, edge)
|
||||
if consume(vEnd, edge):
|
||||
return None
|
||||
return vEnd
|
||||
|
||||
wires = []
|
||||
while knots:
|
||||
we = []
|
||||
vFirst = knots[0]
|
||||
vStart = vFirst
|
||||
vLast = vFirst
|
||||
if len(vertex[vStart]):
|
||||
while vStart is not None:
|
||||
vLast = vStart
|
||||
edges = vertex[vStart]
|
||||
if len(edges) > 0:
|
||||
edge = edges[0]
|
||||
vStart = traverse(vStart, edge, we)
|
||||
else:
|
||||
vStart = None
|
||||
wires.append(we)
|
||||
if len(vertex[vFirst]) == 0:
|
||||
knots = [v for v in knots if v != vFirst]
|
||||
if len(vertex[vLast]) == 0:
|
||||
knots = [v for v in knots if v != vLast]
|
||||
return wires
|
||||
|
||||
|
||||
def _sortVoronoiWires(wires, start=FreeCAD.Vector(0, 0, 0)):
|
||||
def closestTo(start, point):
|
||||
p = None
|
||||
l = None
|
||||
for i in point:
|
||||
if l is None or l > start.distanceToPoint(point[i]):
|
||||
l = start.distanceToPoint(point[i])
|
||||
p = i
|
||||
return (p, l)
|
||||
|
||||
begin = {}
|
||||
end = {}
|
||||
|
||||
for i, w in enumerate(wires):
|
||||
begin[i] = w[0].Vertices[0].toPoint()
|
||||
end[i] = w[-1].Vertices[1].toPoint()
|
||||
|
||||
result = []
|
||||
while begin:
|
||||
(bIdx, bLen) = closestTo(start, begin)
|
||||
(eIdx, eLen) = closestTo(start, end)
|
||||
if bLen < eLen:
|
||||
result.append(wires[bIdx])
|
||||
start = end[bIdx]
|
||||
del begin[bIdx]
|
||||
del end[bIdx]
|
||||
else:
|
||||
result.append([e.Twin for e in reversed(wires[eIdx])])
|
||||
start = begin[eIdx]
|
||||
del begin[eIdx]
|
||||
del end[eIdx]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class _Geometry(object):
|
||||
"""POD class so the limits only have to be calculated once."""
|
||||
|
||||
def __init__(self, zStart, zStop, zScale):
|
||||
self.start = zStart
|
||||
self.stop = zStop
|
||||
self.scale = zScale
|
||||
|
||||
@classmethod
|
||||
def FromTool(cls, tool, zStart, zFinal):
|
||||
rMax = float(tool.Diameter) / 2.0
|
||||
rMin = float(tool.TipDiameter) / 2.0
|
||||
toolangle = math.tan(math.radians(tool.CuttingEdgeAngle.Value / 2.0))
|
||||
zScale = 1.0 / toolangle
|
||||
zStop = zStart - rMax * zScale
|
||||
zOff = rMin * zScale
|
||||
|
||||
return _Geometry(zStart + zOff, max(zStop + zOff, zFinal), zScale)
|
||||
|
||||
@classmethod
|
||||
def FromObj(cls, obj, model):
|
||||
zStart = model.Shape.BoundBox.ZMax
|
||||
finalDepth = obj.FinalDepth.Value
|
||||
|
||||
return cls.FromTool(obj.ToolController.Tool, zStart, finalDepth)
|
||||
|
||||
|
||||
def _calculate_depth(MIC, geom):
|
||||
# given a maximum inscribed circle (MIC) and tool angle,
|
||||
# return depth of cut relative to zStart.
|
||||
depth = geom.start - round(MIC * geom.scale, 4)
|
||||
Path.Log.debug("zStart value: {} depth: {}".format(geom.start, depth))
|
||||
|
||||
return max(depth, geom.stop)
|
||||
|
||||
|
||||
def _getPartEdge(edge, depths):
|
||||
dist = edge.getDistances()
|
||||
zBegin = _calculate_depth(dist[0], depths)
|
||||
zEnd = _calculate_depth(dist[1], depths)
|
||||
return edge.toShape(zBegin, zEnd)
|
||||
|
||||
|
||||
class ObjectVcarve(PathEngraveBase.ObjectOp):
|
||||
"""Proxy class for Vcarve operation."""
|
||||
|
||||
def opFeatures(self, obj):
|
||||
"""opFeatures(obj) ... return all standard features and edges based geometries"""
|
||||
return (
|
||||
PathOp.FeatureTool
|
||||
| PathOp.FeatureHeights
|
||||
| PathOp.FeatureDepths
|
||||
| PathOp.FeatureBaseFaces
|
||||
| PathOp.FeatureCoolant
|
||||
)
|
||||
|
||||
def setupAdditionalProperties(self, obj):
|
||||
if not hasattr(obj, "BaseShapes"):
|
||||
obj.addProperty(
|
||||
"App::PropertyLinkList",
|
||||
"BaseShapes",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "Additional base objects to be engraved"
|
||||
),
|
||||
)
|
||||
obj.setEditorMode("BaseShapes", 2) # hide
|
||||
|
||||
def initOperation(self, obj):
|
||||
"""initOperation(obj) ... create vcarve specific properties."""
|
||||
obj.addProperty(
|
||||
"App::PropertyFloat",
|
||||
"Discretize",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property", "The deflection value for discretizing arcs"
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyFloat",
|
||||
"Colinear",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Cutoff for removing colinear segments (degrees). \
|
||||
default=10.0.",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyFloat",
|
||||
"Tolerance",
|
||||
"Path",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Vcarve Tolerance"),
|
||||
)
|
||||
obj.Colinear = 10.0
|
||||
obj.Discretize = 0.01
|
||||
obj.Tolerance = PathPreferences.defaultGeometryTolerance()
|
||||
self.setupAdditionalProperties(obj)
|
||||
|
||||
def opOnDocumentRestored(self, obj):
|
||||
# upgrade ...
|
||||
self.setupAdditionalProperties(obj)
|
||||
|
||||
def _getPartEdges(self, obj, vWire, geom):
|
||||
edges = []
|
||||
for e in vWire:
|
||||
edges.append(_getPartEdge(e, geom))
|
||||
return edges
|
||||
|
||||
def buildPathMedial(self, obj, faces):
|
||||
"""constructs a medial axis path using openvoronoi"""
|
||||
|
||||
def insert_many_wires(vd, wires):
|
||||
for wire in wires:
|
||||
Path.Log.debug("discretize value: {}".format(obj.Discretize))
|
||||
pts = wire.discretize(QuasiDeflection=obj.Discretize)
|
||||
ptv = [FreeCAD.Vector(p.x, p.y) for p in pts]
|
||||
ptv.append(ptv[0])
|
||||
|
||||
for i in range(len(pts)):
|
||||
vd.addSegment(ptv[i], ptv[i + 1])
|
||||
|
||||
def cutWire(edges):
|
||||
path = []
|
||||
path.append(Path.Command("G0 Z{}".format(obj.SafeHeight.Value)))
|
||||
e = edges[0]
|
||||
p = e.valueAt(e.FirstParameter)
|
||||
path.append(
|
||||
Path.Command("G0 X{} Y{} Z{}".format(p.x, p.y, obj.SafeHeight.Value))
|
||||
)
|
||||
hSpeed = obj.ToolController.HorizFeed.Value
|
||||
vSpeed = obj.ToolController.VertFeed.Value
|
||||
path.append(
|
||||
Path.Command("G1 X{} Y{} Z{} F{}".format(p.x, p.y, p.z, vSpeed))
|
||||
)
|
||||
for e in edges:
|
||||
path.extend(PathGeom.cmdsForEdge(e, hSpeed=hSpeed, vSpeed=vSpeed))
|
||||
|
||||
return path
|
||||
|
||||
voronoiWires = []
|
||||
for f in faces:
|
||||
vd = Path.Voronoi.Diagram()
|
||||
insert_many_wires(vd, f.Wires)
|
||||
|
||||
vd.construct()
|
||||
|
||||
for e in vd.Edges:
|
||||
if e.isPrimary():
|
||||
if e.isBorderline():
|
||||
e.Color = BORDERLINE
|
||||
else:
|
||||
e.Color = PRIMARY
|
||||
else:
|
||||
e.Color = SECONDARY
|
||||
vd.colorExterior(EXTERIOR1)
|
||||
vd.colorExterior(
|
||||
EXTERIOR2,
|
||||
lambda v: not f.isInside(
|
||||
v.toPoint(f.BoundBox.ZMin), obj.Tolerance, True
|
||||
),
|
||||
)
|
||||
vd.colorColinear(COLINEAR, obj.Colinear)
|
||||
vd.colorTwins(TWIN)
|
||||
|
||||
wires = _collectVoronoiWires(vd)
|
||||
if _sorting != "global":
|
||||
wires = _sortVoronoiWires(wires)
|
||||
voronoiWires.extend(wires)
|
||||
|
||||
if _sorting == "global":
|
||||
voronoiWires = _sortVoronoiWires(voronoiWires)
|
||||
|
||||
geom = _Geometry.FromObj(obj, self.model[0])
|
||||
|
||||
pathlist = []
|
||||
pathlist.append(Path.Command("(starting)"))
|
||||
for w in voronoiWires:
|
||||
pWire = self._getPartEdges(obj, w, geom)
|
||||
if pWire:
|
||||
wires.append(pWire)
|
||||
pathlist.extend(cutWire(pWire))
|
||||
self.commandlist = pathlist
|
||||
|
||||
def opExecute(self, obj):
|
||||
"""opExecute(obj) ... process engraving operation"""
|
||||
Path.Log.track()
|
||||
|
||||
if not hasattr(obj.ToolController.Tool, "CuttingEdgeAngle"):
|
||||
Path.Log.error(
|
||||
translate(
|
||||
"Path_Vcarve",
|
||||
"VCarve requires an engraving cutter with CuttingEdgeAngle",
|
||||
)
|
||||
)
|
||||
|
||||
if obj.ToolController.Tool.CuttingEdgeAngle >= 180.0:
|
||||
Path.Log.error(
|
||||
translate(
|
||||
"Path_Vcarve", "Engraver Cutting Edge Angle must be < 180 degrees."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
faces = []
|
||||
|
||||
for base in obj.BaseShapes:
|
||||
faces.extend(base.Shape.Faces)
|
||||
|
||||
for base in obj.Base:
|
||||
for sub in base[1]:
|
||||
shape = getattr(base[0].Shape, sub)
|
||||
if isinstance(shape, Part.Face):
|
||||
faces.append(shape)
|
||||
|
||||
if not faces:
|
||||
for model in self.model:
|
||||
if model.isDerivedFrom(
|
||||
"Sketcher::SketchObject"
|
||||
) or model.isDerivedFrom("Part::Part2DObject"):
|
||||
faces.extend(model.Shape.Faces)
|
||||
|
||||
if faces:
|
||||
self.buildPathMedial(obj, faces)
|
||||
else:
|
||||
Path.Log.error(
|
||||
translate(
|
||||
"PathVcarve",
|
||||
"The Job Base Object has no engraveable element. Engraving operation will produce no output.",
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
Path.Log.error(
|
||||
"Error processing Base object. Engraving operation will produce no output."
|
||||
)
|
||||
|
||||
def opUpdateDepths(self, obj, ignoreErrors=False):
|
||||
"""updateDepths(obj) ... engraving is always done at the top most z-value"""
|
||||
job = PathUtils.findParentJob(obj)
|
||||
self.opSetDefaultValues(obj, job)
|
||||
|
||||
def opSetDefaultValues(self, obj, job):
|
||||
"""opSetDefaultValues(obj) ... set depths for vcarving"""
|
||||
if PathOp.FeatureDepths & self.opFeatures(obj):
|
||||
if job and len(job.Model.Group) > 0:
|
||||
bb = job.Proxy.modelBoundBox(job)
|
||||
obj.OpStartDepth = bb.ZMax
|
||||
obj.OpFinalDepth = job.Stock.Shape.BoundBox.ZMin
|
||||
else:
|
||||
obj.OpFinalDepth = -0.1
|
||||
|
||||
def isToolSupported(self, obj, tool):
|
||||
"""isToolSupported(obj, tool) ... returns True if v-carve op can work with tool."""
|
||||
return (
|
||||
hasattr(tool, "Diameter")
|
||||
and hasattr(tool, "CuttingEdgeAngle")
|
||||
and hasattr(tool, "TipDiameter")
|
||||
)
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
return ["Discretize"]
|
||||
|
||||
|
||||
def Create(name, obj=None, parentJob=None):
|
||||
"""Create(name) ... Creates and returns a Vcarve operation."""
|
||||
if obj is None:
|
||||
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
||||
obj.Proxy = ObjectVcarve(obj, name, parentJob)
|
||||
return obj
|
||||
@@ -1,171 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import PathGui as PGui # ensure Path/Gui/Resources are loaded
|
||||
import PathScripts.PathVcarve as PathVcarve
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
|
||||
__title__ = "Path Vcarve Operation UI"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Vcarve operation page controller and command implementation."
|
||||
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
|
||||
"""Enhanced base geometry page to also allow special base objects."""
|
||||
|
||||
def super(self):
|
||||
return super(TaskPanelBaseGeometryPage, self)
|
||||
|
||||
def addBaseGeometry(self, selection):
|
||||
Path.Log.track(selection)
|
||||
added = False
|
||||
shapes = self.obj.BaseShapes
|
||||
for sel in selection:
|
||||
job = PathUtils.findParentJob(self.obj)
|
||||
base = job.Proxy.resourceClone(job, sel.Object)
|
||||
if not base:
|
||||
Path.Log.notice(
|
||||
(
|
||||
translate("Path", "%s is not a Base Model object of the job %s")
|
||||
+ "\n"
|
||||
)
|
||||
% (sel.Object.Label, job.Label)
|
||||
)
|
||||
continue
|
||||
if base in shapes:
|
||||
Path.Log.notice(
|
||||
"Base shape %s already in the list".format(sel.Object.Label)
|
||||
)
|
||||
continue
|
||||
if base.isDerivedFrom("Part::Part2DObject"):
|
||||
if sel.HasSubObjects:
|
||||
# selectively add some elements of the drawing to the Base
|
||||
for sub in sel.SubElementNames:
|
||||
if "Vertex" in sub:
|
||||
Path.Log.info("Ignoring vertex")
|
||||
else:
|
||||
self.obj.Proxy.addBase(self.obj, base, sub)
|
||||
else:
|
||||
# when adding an entire shape to BaseShapes we can take its sub shapes out of Base
|
||||
self.obj.Base = [(p, el) for p, el in self.obj.Base if p != base]
|
||||
shapes.append(base)
|
||||
self.obj.BaseShapes = shapes
|
||||
added = True
|
||||
|
||||
if not added:
|
||||
# user wants us to engrave an edge of face of a base model
|
||||
Path.Log.info(" call default")
|
||||
base = self.super().addBaseGeometry(selection)
|
||||
added = added or base
|
||||
|
||||
return added
|
||||
|
||||
def setFields(self, obj):
|
||||
self.super().setFields(obj)
|
||||
self.form.baseList.blockSignals(True)
|
||||
for shape in self.obj.BaseShapes:
|
||||
item = QtGui.QListWidgetItem(shape.Label)
|
||||
item.setData(self.super().DataObject, shape)
|
||||
item.setData(self.super().DataObjectSub, None)
|
||||
self.form.baseList.addItem(item)
|
||||
self.form.baseList.blockSignals(False)
|
||||
|
||||
def updateBase(self):
|
||||
Path.Log.track()
|
||||
shapes = []
|
||||
for i in range(self.form.baseList.count()):
|
||||
item = self.form.baseList.item(i)
|
||||
obj = item.data(self.super().DataObject)
|
||||
sub = item.data(self.super().DataObjectSub)
|
||||
if not sub:
|
||||
shapes.append(obj)
|
||||
Path.Log.debug(
|
||||
"Setting new base shapes: %s -> %s" % (self.obj.BaseShapes, shapes)
|
||||
)
|
||||
self.obj.BaseShapes = shapes
|
||||
return self.super().updateBase()
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""Page controller class for the Vcarve operation."""
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... returns UI"""
|
||||
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpVcarveEdit.ui")
|
||||
|
||||
def getFields(self, obj):
|
||||
"""getFields(obj) ... transfers values from UI to obj's proprties"""
|
||||
if obj.Discretize != self.form.discretize.value():
|
||||
obj.Discretize = self.form.discretize.value()
|
||||
if obj.Colinear != self.form.colinearFilter.value():
|
||||
obj.Colinear = self.form.colinearFilter.value()
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
self.updateCoolant(obj, self.form.coolantController)
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... transfers obj's property values to UI"""
|
||||
self.form.discretize.setValue(obj.Discretize)
|
||||
self.form.colinearFilter.setValue(obj.Colinear)
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.setupCoolant(obj, self.form.coolantController)
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
|
||||
signals = []
|
||||
signals.append(self.form.discretize.editingFinished)
|
||||
signals.append(self.form.colinearFilter.editingFinished)
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
return signals
|
||||
|
||||
def taskPanelBaseGeometryPage(self, obj, features):
|
||||
"""taskPanelBaseGeometryPage(obj, features) ... return page for adding base geometries."""
|
||||
return TaskPanelBaseGeometryPage(obj, features)
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Vcarve",
|
||||
PathVcarve.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_Vcarve",
|
||||
QtCore.QT_TRANSLATE_NOOP("Path_Vcarve", "Vcarve"),
|
||||
QtCore.QT_TRANSLATE_NOOP("Path_Vcarve", "Creates a medial line engraving path"),
|
||||
PathVcarve.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathVcarveGui... done\n")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,182 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2020 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * Copyright (c) 2020 russ4262 <russ4262@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from PySide import QtCore
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import PathScripts.PathGui as PathGui
|
||||
import PathScripts.PathOpGui as PathOpGui
|
||||
import PathScripts.PathWaterline as PathWaterline
|
||||
|
||||
__title__ = "Path Waterline Operation UI"
|
||||
__author__ = "sliptonic (Brad Collette), russ4262 (Russell Johnson)"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Waterline operation page controller and command implementation."
|
||||
|
||||
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 TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
"""Page controller class for the Waterline operation."""
|
||||
|
||||
def initPage(self, obj):
|
||||
self.setTitle("Waterline - " + obj.Label)
|
||||
self.updateVisibility()
|
||||
|
||||
def getForm(self):
|
||||
"""getForm() ... returns UI"""
|
||||
form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpWaterlineEdit.ui")
|
||||
comboToPropertyMap = [
|
||||
("algorithmSelect", "Algorithm"),
|
||||
("boundBoxSelect", "BoundBox"),
|
||||
("layerMode", "LayerMode"),
|
||||
("cutPattern", "CutPattern"),
|
||||
]
|
||||
enumTups = PathWaterline.ObjectWaterline.propertyEnumerations(dataType="raw")
|
||||
PathGui.populateCombobox(form, enumTups, comboToPropertyMap)
|
||||
return form
|
||||
|
||||
def getFields(self, obj):
|
||||
"""getFields(obj) ... transfers values from UI to obj's proprties"""
|
||||
self.updateToolController(obj, self.form.toolController)
|
||||
self.updateCoolant(obj, self.form.coolantController)
|
||||
|
||||
if obj.Algorithm != str(self.form.algorithmSelect.currentData()):
|
||||
obj.Algorithm = str(self.form.algorithmSelect.currentData())
|
||||
|
||||
if obj.BoundBox != str(self.form.boundBoxSelect.currentData()):
|
||||
obj.BoundBox = str(self.form.boundBoxSelect.currentData())
|
||||
|
||||
if obj.LayerMode != str(self.form.layerMode.currentData()):
|
||||
obj.LayerMode = str(self.form.layerMode.currentData())
|
||||
|
||||
if obj.CutPattern != str(self.form.cutPattern.currentData()):
|
||||
obj.CutPattern = str(self.form.cutPattern.currentData())
|
||||
|
||||
PathGui.updateInputField(
|
||||
obj, "BoundaryAdjustment", self.form.boundaryAdjustment
|
||||
)
|
||||
|
||||
if obj.StepOver != self.form.stepOver.value():
|
||||
obj.StepOver = self.form.stepOver.value()
|
||||
|
||||
PathGui.updateInputField(obj, "SampleInterval", self.form.sampleInterval)
|
||||
|
||||
if obj.OptimizeLinearPaths != self.form.optimizeEnabled.isChecked():
|
||||
obj.OptimizeLinearPaths = self.form.optimizeEnabled.isChecked()
|
||||
|
||||
def setFields(self, obj):
|
||||
"""setFields(obj) ... transfers obj's property values to UI"""
|
||||
self.setupToolController(obj, self.form.toolController)
|
||||
self.setupCoolant(obj, self.form.coolantController)
|
||||
self.selectInComboBox(obj.Algorithm, self.form.algorithmSelect)
|
||||
self.selectInComboBox(obj.BoundBox, self.form.boundBoxSelect)
|
||||
self.selectInComboBox(obj.LayerMode, self.form.layerMode)
|
||||
self.selectInComboBox(obj.CutPattern, self.form.cutPattern)
|
||||
self.form.boundaryAdjustment.setText(
|
||||
FreeCAD.Units.Quantity(
|
||||
obj.BoundaryAdjustment.Value, FreeCAD.Units.Length
|
||||
).UserString
|
||||
)
|
||||
self.form.stepOver.setValue(obj.StepOver)
|
||||
self.form.sampleInterval.setText(
|
||||
FreeCAD.Units.Quantity(
|
||||
obj.SampleInterval.Value, FreeCAD.Units.Length
|
||||
).UserString
|
||||
)
|
||||
|
||||
if obj.OptimizeLinearPaths:
|
||||
self.form.optimizeEnabled.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
self.form.optimizeEnabled.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
self.updateVisibility()
|
||||
|
||||
def getSignalsForUpdate(self, obj):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals for updating obj"""
|
||||
signals = []
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
signals.append(self.form.algorithmSelect.currentIndexChanged)
|
||||
signals.append(self.form.boundBoxSelect.currentIndexChanged)
|
||||
signals.append(self.form.layerMode.currentIndexChanged)
|
||||
signals.append(self.form.cutPattern.currentIndexChanged)
|
||||
signals.append(self.form.boundaryAdjustment.editingFinished)
|
||||
signals.append(self.form.stepOver.editingFinished)
|
||||
signals.append(self.form.sampleInterval.editingFinished)
|
||||
signals.append(self.form.optimizeEnabled.stateChanged)
|
||||
|
||||
return signals
|
||||
|
||||
def updateVisibility(self, sentObj=None):
|
||||
"""updateVisibility(sentObj=None)... Updates visibility of Tasks panel objects."""
|
||||
Algorithm = self.form.algorithmSelect.currentData()
|
||||
self.form.optimizeEnabled.hide() # Has no independent QLabel object
|
||||
|
||||
if Algorithm == "OCL Dropcutter":
|
||||
self.form.cutPattern.hide()
|
||||
self.form.cutPattern_label.hide()
|
||||
self.form.boundaryAdjustment.hide()
|
||||
self.form.boundaryAdjustment_label.hide()
|
||||
self.form.stepOver.hide()
|
||||
self.form.stepOver_label.hide()
|
||||
self.form.sampleInterval.show()
|
||||
self.form.sampleInterval_label.show()
|
||||
elif Algorithm == "Experimental":
|
||||
self.form.cutPattern.show()
|
||||
self.form.boundaryAdjustment.show()
|
||||
self.form.cutPattern_label.show()
|
||||
self.form.boundaryAdjustment_label.show()
|
||||
if self.form.cutPattern.currentData() == "None":
|
||||
self.form.stepOver.hide()
|
||||
self.form.stepOver_label.hide()
|
||||
else:
|
||||
self.form.stepOver.show()
|
||||
self.form.stepOver_label.show()
|
||||
self.form.sampleInterval.hide()
|
||||
self.form.sampleInterval_label.hide()
|
||||
|
||||
def registerSignalHandlers(self, obj):
|
||||
self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility)
|
||||
self.form.cutPattern.currentIndexChanged.connect(self.updateVisibility)
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
"Waterline",
|
||||
PathWaterline.Create,
|
||||
TaskPanelOpPage,
|
||||
"Path_Waterline",
|
||||
QT_TRANSLATE_NOOP("Path_Waterline", "Waterline"),
|
||||
QT_TRANSLATE_NOOP("Path_Waterline", "Create a Waterline Operation from a model"),
|
||||
PathWaterline.SetupProperties,
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog("Loading PathWaterlineGui... done\n")
|
||||
Reference in New Issue
Block a user