Moved all Path operations with model and gui into Path.Op module

This commit is contained in:
Markus Lampert
2022-08-11 23:53:33 -07:00
parent 4b205943bc
commit 6da2c338df
65 changed files with 261 additions and 259 deletions

View File

@@ -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

View File

@@ -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 = []

View File

@@ -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)

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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)"

View File

@@ -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)"

View File

@@ -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)"

View File

@@ -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)"

View File

@@ -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)"

View File

@@ -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)"

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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()")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")