diff --git a/src/Mod/Path/App/Area.cpp b/src/Mod/Path/App/Area.cpp
index 15ac2c8507..8332036c2e 100644
--- a/src/Mod/Path/App/Area.cpp
+++ b/src/Mod/Path/App/Area.cpp
@@ -496,6 +496,79 @@ void Area::add(const TopoDS_Shape& shape, short op) {
myShapes.emplace_back(op, shape);
}
+std::shared_ptr Area::getClearedArea(double tipDiameter, double diameter) {
+ build();
+#define AREA_MY(_param) myParams.PARAM_FNAME(_param)
+ PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_OFFSET_CONF);
+ PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_CLIPPER_FILL);
+
+ // Do not fit arcs after these offsets; it introduces unnecessary approximation error, and all off
+ // those arcs will be converted back to segments again for clipper differencing in getRestArea anyway
+ CAreaConfig conf(myParams, /*no_fit_arcs*/ true);
+
+ const double roundPrecision = myParams.Accuracy;
+ const double buffer = 2 * roundPrecision;
+
+ // A = myArea
+ // prevCenters = offset(A, -rTip)
+ const double rTip = tipDiameter / 2.;
+ CArea prevCenter(*myArea);
+ prevCenter.OffsetWithClipper(-rTip, JoinType, EndType, myParams.MiterLimit, roundPrecision);
+
+ // prevCleared = offset(prevCenter, r).
+ CArea prevCleared(prevCenter);
+ prevCleared.OffsetWithClipper(diameter / 2. + buffer, JoinType, EndType, myParams.MiterLimit, roundPrecision);
+
+ std::shared_ptr clearedArea = make_shared(*this);
+ clearedArea->myArea.reset(new CArea(prevCleared));
+
+ return clearedArea;
+}
+
+std::shared_ptr Area::getRestArea(std::vector> clearedAreas, double diameter) {
+ build();
+ PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_OFFSET_CONF);
+ PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_CLIPPER_FILL);
+
+ const double roundPrecision = myParams.Accuracy;
+ const double buffer = 2 * roundPrecision;
+
+ // transform all clearedAreas into our workplane
+ Area clearedAreasInPlane(&myParams);
+ clearedAreasInPlane.myArea.reset(new CArea());
+ for (std::shared_ptr clearedArea : clearedAreas) {
+ gp_Trsf trsf = clearedArea->myTrsf;
+ trsf.Invert();
+ trsf.SetTranslationPart(gp_Vec{trsf.TranslationPart().X(), trsf.TranslationPart().Y(), -myTrsf.TranslationPart().Z()}); // discard z-height of cleared workplane, set to myWorkPlane's height
+ TopoDS_Shape clearedShape = Area::toShape(*clearedArea->myArea, false, &trsf);
+ Area::addShape(*(clearedAreasInPlane.myArea), clearedShape, &myTrsf, .01 /*default value*/,
+ &myWorkPlane);
+ }
+
+ // remaining = A - prevCleared
+ CArea remaining(*myArea);
+ remaining.Clip(toClipperOp(Area::OperationDifference), &*(clearedAreasInPlane.myArea), SubjectFill, ClipFill);
+
+ // rest = intersect(A, offset(remaining, dTool))
+ CArea restCArea(remaining);
+ restCArea.OffsetWithClipper(diameter + buffer, JoinType, EndType, myParams.MiterLimit, roundPrecision);
+ restCArea.Clip(toClipperOp(Area::OperationIntersection), &*myArea, SubjectFill, ClipFill);
+
+ gp_Trsf trsf(myTrsf.Inverted());
+ TopoDS_Shape restShape = Area::toShape(restCArea, false, &trsf);
+ std::shared_ptr restArea = make_shared(&myParams);
+ restArea->add(restShape, OperationCompound);
+
+ return restArea;
+}
+
+TopoDS_Shape Area::toTopoShape()
+{
+ build();
+ gp_Trsf trsf = myTrsf.Inverted();
+ return toShape(*myArea, false, &trsf);
+}
+
void Area::setParams(const AreaParams& params) {
#define AREA_SRC(_param) params.PARAM_FNAME(_param)
@@ -1624,7 +1697,6 @@ void Area::build() {
if (myShapes.empty())
throw Base::ValueError("no shape added");
-#define AREA_MY(_param) myParams.PARAM_FNAME(_param)
PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_CLIPPER_FILL);
if (myHaveSolid && myParams.SectionCount) {
diff --git a/src/Mod/Path/App/Area.h b/src/Mod/Path/App/Area.h
index 614adfafa7..5b192ae08a 100644
--- a/src/Mod/Path/App/Area.h
+++ b/src/Mod/Path/App/Area.h
@@ -242,6 +242,10 @@ public:
const std::vector& heights = std::vector(),
const TopoDS_Shape& plane = TopoDS_Shape());
+ std::shared_ptr getClearedArea(double tipDiameter, double diameter);
+ std::shared_ptr getRestArea(std::vector> clearedAreas, double diameter);
+ TopoDS_Shape toTopoShape();
+
/** Config this Area object */
void setParams(const AreaParams& params);
diff --git a/src/Mod/Path/App/AreaPy.xml b/src/Mod/Path/App/AreaPy.xml
index b5378f4344..6b9c1c6c1e 100644
--- a/src/Mod/Path/App/AreaPy.xml
+++ b/src/Mod/Path/App/AreaPy.xml
@@ -62,6 +62,21 @@ same algorithm
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/Path/App/AreaPyImp.cpp b/src/Mod/Path/App/AreaPyImp.cpp
index 42676f7e17..09be42d966 100644
--- a/src/Mod/Path/App/AreaPyImp.cpp
+++ b/src/Mod/Path/App/AreaPyImp.cpp
@@ -145,6 +145,22 @@ static const PyMethodDef areaOverrides[] = {
"\n* plane (None): optional shape to specify a section plane. If not give, the current workplane\n"
"of this Area is used if section mode is 'Workplane'.",
},
+ {
+ "getClearedArea",nullptr,0,
+ "getClearedArea(tipDiameter, diameter):\n"
+ "Gets the area cleared when a tool maximally clears this area. This method assumes a tool tip diameter 'tipDiameter' traces the full area, and that (perhaps at a different height on the tool) this clears a different region with tool diameter 'diameter'.\n",
+ },
+ {
+ "getRestArea",nullptr,0,
+ "getRestArea(clearedAreas, diameter):\n"
+ "Rest machining: gets the area left to be machined, assuming some of this area has already been cleared previous tool paths.\n"
+ "clearedAreas: the regions already cleared.\n"
+ "diameter: the tool diameter that finishes clearing this area.\n",
+ },
+ {
+ "toTopoShape",nullptr,0,
+ "toTopoShape():\n"
+ },
{
"setDefaultParams",reinterpret_cast(reinterpret_cast(areaSetParams)), METH_VARARGS|METH_KEYWORDS|METH_STATIC,
"setDefaultParams(key=value...):\n"
@@ -383,6 +399,55 @@ PyObject* AreaPy::makeSections(PyObject *args, PyObject *keywds)
} PY_CATCH_OCC
}
+PyObject* AreaPy::getClearedArea(PyObject *args, PyObject *keywds)
+{
+ PY_TRY {
+ double tipDiameter, diameter;
+ if (!PyArg_ParseTuple(args, "dd", &tipDiameter, &diameter))
+ return nullptr;
+ std::shared_ptr clearedArea = getAreaPtr()->getClearedArea(tipDiameter, diameter);
+ auto pyClearedArea = Py::asObject(new AreaPy(new Area(*clearedArea, true)));
+ return Py::new_reference_to(pyClearedArea);
+ } PY_CATCH_OCC
+}
+
+PyObject* AreaPy::getRestArea(PyObject *args, PyObject *keywds)
+{
+ PY_TRY {
+ PyObject *pyClearedAreas;
+ std::vector> clearedAreas;
+ double diameter;
+ if (!PyArg_ParseTuple(args, "Od", &pyClearedAreas, &diameter))
+ return nullptr;
+ if (pyClearedAreas && PyObject_TypeCheck(pyClearedAreas, &PyList_Type)) {
+ Py::Sequence clearedAreasSeq(pyClearedAreas);
+ clearedAreas.reserve(clearedAreasSeq.size());
+ for (Py::Sequence::iterator it = clearedAreasSeq.begin(); it != clearedAreasSeq.end(); ++it) {
+ PyObject *item = (*it).ptr();
+ if (!PyObject_TypeCheck(item, &(AreaPy::Type))) {
+ PyErr_SetString(PyExc_TypeError, "cleared areas must only contain AreaPy type");
+ return nullptr;
+ }
+ clearedAreas.push_back(std::make_shared(*static_cast(item)->getAreaPtr(), true));
+ }
+ } else {
+ PyErr_SetString(PyExc_TypeError, "clearedAreas must be of type list of AreaPy");
+ return nullptr;
+ }
+
+ std::shared_ptr restArea = getAreaPtr()->getRestArea(clearedAreas, diameter);
+ auto pyRestArea = Py::asObject(new AreaPy(new Area(*restArea, true)));
+ return Py::new_reference_to(pyRestArea);
+ } PY_CATCH_OCC
+}
+
+PyObject* AreaPy::toTopoShape(PyObject *args, PyObject *keywds)
+{
+ PY_TRY {
+ return Py::new_reference_to(Part::shape2pyshape(getAreaPtr()->toTopoShape()));
+ } PY_CATCH_OCC
+}
+
PyObject* AreaPy::setDefaultParams(PyObject *, PyObject *)
{
return nullptr;
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpPocketFullEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpPocketFullEdit.ui
index 1f7bd6e8c3..aadfbf1eac 100644
--- a/src/Mod/Path/Gui/Resources/panels/PageOpPocketFullEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpPocketFullEdit.ui
@@ -245,14 +245,24 @@ The latter can be used to face of the entire stock area to ensure uniform height
- -
+
-
Min Travel
-
+ -
+
+
+ Check to skip machining regions that have already been cleared by previous operations
+
+
+ Use Rest Machining
+
+
+
+
-
diff --git a/src/Mod/Path/Path/Op/Area.py b/src/Mod/Path/Path/Op/Area.py
index b249372161..1ef2044a8f 100644
--- a/src/Mod/Path/Path/Op/Area.py
+++ b/src/Mod/Path/Path/Op/Area.py
@@ -38,7 +38,7 @@ __title__ = "Base class for PathArea based operations."
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecad.org"
__doc__ = "Base class and properties for Path.Area based operations."
-__contributors__ = "russ4262 (Russell Johnson)"
+__contributors__ = "russ4262 (Russell Johnson) davidgilkaufman (David Kaufman)"
if False:
@@ -236,6 +236,47 @@ class ObjectOp(PathOp.ObjectOp):
mode=0, project=self.areaOpUseProjection(obj), heights=heights
)
Path.Log.debug("sections = %s" % sections)
+
+ # Rest machining
+ self.sectionShapes = self.sectionShapes + [section.toTopoShape() for section in sections]
+ if hasattr(obj, "UseRestMachining") and obj.UseRestMachining:
+ # Loop through prior operations
+ clearedAreas = []
+ foundSelf = False
+ for op in self.job.Operations.Group:
+ if foundSelf:
+ break
+ oplist = [op] + op.OutListRecursive
+ oplist = list(filter(lambda op: hasattr(op, "Active"), oplist))
+ for op in oplist:
+ if op.Proxy == self:
+ # Ignore self, and all later operations
+ foundSelf = True
+ break
+ if hasattr(op, "RestMachiningRegions") and op.Active:
+ if hasattr(op, "RestMachiningRegionsNeedRecompute") and op.RestMachiningRegionsNeedRecompute:
+ Path.Log.warning(
+ translate("PathAreaOp", "Previous operation %s is required for rest machining, but it has no stored rest machining metadata. Recomputing to generate this metadata...") % op.Label
+ )
+ op.recompute()
+
+ tool = op.Proxy.tool if hasattr(op.Proxy, "tool") else op.ToolController.Proxy.getTool(op.ToolController)
+ diameter = tool.Diameter.getValueAs("mm")
+ def shapeToArea(shape):
+ area = Path.Area()
+ area.setPlane(PathUtils.makeWorkplane(shape))
+ area.add(shape)
+ return area
+ opClearedAreas = [shapeToArea(pa).getClearedArea(diameter, diameter) for pa in op.RestMachiningRegions.SubShapes]
+ clearedAreas.extend(opClearedAreas)
+ restSections = []
+ for section in sections:
+ z = section.getShape().BoundBox.ZMin
+ sectionClearedAreas = [a for a in clearedAreas if a.getShape().BoundBox.ZMax <= z]
+ restSection = section.getRestArea(sectionClearedAreas, self.tool.Diameter.getValueAs("mm"))
+ restSections.append(restSection)
+ sections = restSections
+
shapelist = [sec.getShape() for sec in sections]
Path.Log.debug("shapelist = %s" % shapelist)
@@ -388,6 +429,7 @@ class ObjectOp(PathOp.ObjectOp):
shapes = [j["shape"] for j in locations]
sims = []
+ self.sectionShapes = []
for shape, isHole, sub in shapes:
profileEdgesIsOpen = False
@@ -435,6 +477,10 @@ class ObjectOp(PathOp.ObjectOp):
)
)
+ if hasattr(obj, "RestMachiningRegions"):
+ obj.RestMachiningRegions = Part.makeCompound(self.sectionShapes)
+ if hasattr(obj, "RestMachiningRegionsNeedRecompute"):
+ obj.RestMachiningRegionsNeedRecompute = False
Path.Log.debug("obj.Name: " + str(obj.Name) + "\n\n")
return sims
diff --git a/src/Mod/Path/Path/Op/Gui/Pocket.py b/src/Mod/Path/Path/Op/Gui/Pocket.py
index 7e7547eb3f..2890dfd41f 100644
--- a/src/Mod/Path/Path/Op/Gui/Pocket.py
+++ b/src/Mod/Path/Path/Op/Gui/Pocket.py
@@ -46,7 +46,7 @@ class TaskPanelOpPage(PathPocketBaseGui.TaskPanelOpPage):
def pocketFeatures(self):
"""pocketFeatures() ... return FeaturePocket (see PathPocketBaseGui)"""
- return PathPocketBaseGui.FeaturePocket
+ return PathPocketBaseGui.FeaturePocket | PathPocketBaseGui.FeatureRestMachining
Command = PathOpGui.SetupOperation(
diff --git a/src/Mod/Path/Path/Op/Gui/PocketBase.py b/src/Mod/Path/Path/Op/Gui/PocketBase.py
index 891520a997..b45410242f 100644
--- a/src/Mod/Path/Path/Op/Gui/PocketBase.py
+++ b/src/Mod/Path/Path/Op/Gui/PocketBase.py
@@ -44,6 +44,7 @@ translate = FreeCAD.Qt.translate
FeaturePocket = 0x01
FeatureFacing = 0x02
FeatureOutline = 0x04
+FeatureRestMachining = 0x08
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
@@ -89,6 +90,9 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
if not (FeatureOutline & self.pocketFeatures()):
form.useOutline.hide()
+ if not (FeatureRestMachining & self.pocketFeatures()):
+ form.useRestMachining.hide()
+
# if True:
# # currently doesn't have an effect or is experimental
# form.minTravel.hide()
@@ -131,6 +135,9 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
if obj.UseStartPoint != self.form.useStartPoint.isChecked():
obj.UseStartPoint = self.form.useStartPoint.isChecked()
+ if obj.UseRestMachining != self.form.useRestMachining.isChecked():
+ obj.UseRestMachining = self.form.useRestMachining.isChecked()
+
if FeatureOutline & self.pocketFeatures():
if obj.UseOutline != self.form.useOutline.isChecked():
obj.UseOutline = self.form.useOutline.isChecked()
@@ -155,6 +162,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
).UserString
)
self.form.useStartPoint.setChecked(obj.UseStartPoint)
+ self.form.useRestMachining.setChecked(obj.UseRestMachining)
if FeatureOutline & self.pocketFeatures():
self.form.useOutline.setChecked(obj.UseOutline)
@@ -186,6 +194,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
signals.append(self.form.toolController.currentIndexChanged)
signals.append(self.form.extraOffset.editingFinished)
signals.append(self.form.useStartPoint.clicked)
+ signals.append(self.form.useRestMachining.clicked)
signals.append(self.form.useOutline.clicked)
signals.append(self.form.minTravel.clicked)
signals.append(self.form.coolantController.currentIndexChanged)
diff --git a/src/Mod/Path/Path/Op/Gui/PocketShape.py b/src/Mod/Path/Path/Op/Gui/PocketShape.py
index 784c9fd94b..f7f7ac4e3d 100644
--- a/src/Mod/Path/Path/Op/Gui/PocketShape.py
+++ b/src/Mod/Path/Path/Op/Gui/PocketShape.py
@@ -52,7 +52,7 @@ class TaskPanelOpPage(PathPocketBaseGui.TaskPanelOpPage):
def pocketFeatures(self):
"""pocketFeatures() ... return FeaturePocket (see PathPocketBaseGui)"""
- return PathPocketBaseGui.FeaturePocket | PathPocketBaseGui.FeatureOutline
+ return PathPocketBaseGui.FeaturePocket | PathPocketBaseGui.FeatureOutline | PathPocketBaseGui.FeatureRestMachining
def taskPanelBaseLocationPage(self, obj, features):
if not hasattr(self, "extensionsPanel"):
diff --git a/src/Mod/Path/Path/Op/Pocket.py b/src/Mod/Path/Path/Op/Pocket.py
index e4dec55348..76dd910086 100644
--- a/src/Mod/Path/Path/Op/Pocket.py
+++ b/src/Mod/Path/Path/Op/Pocket.py
@@ -135,6 +135,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
def opOnDocumentRestored(self, obj):
"""opOnDocumentRestored(obj) ... adds the properties if they doesn't exist."""
+ super().opOnDocumentRestored(obj)
self.initPocketOp(obj)
def pocketInvertExtraOffset(self):
diff --git a/src/Mod/Path/Path/Op/PocketBase.py b/src/Mod/Path/Path/Op/PocketBase.py
index 9073fdebca..829dd865c3 100644
--- a/src/Mod/Path/Path/Op/PocketBase.py
+++ b/src/Mod/Path/Path/Op/PocketBase.py
@@ -185,6 +185,25 @@ class ObjectPocket(PathAreaOp.ObjectOp):
"Last Stepover Radius. If 0, 50% of cutter is used. Tuning this can be used to improve stepover for some shapes",
),
)
+ obj.addProperty(
+ "App::PropertyBool",
+ "UseRestMachining",
+ "Pocket",
+ QT_TRANSLATE_NOOP(
+ "App::Property",
+ "Skips machining regions that have already been cleared by previous operations.",
+ ),
+ )
+ obj.addProperty(
+ "Part::PropertyPartShape",
+ "RestMachiningRegions",
+ "Pocket",
+ QT_TRANSLATE_NOOP(
+ "App::Property",
+ "The areas cleared by this operation, one area per height, stored as a compound part. Used internally for rest machining.",
+ ),
+ )
+ obj.setEditorMode("RestMachiningRegions", 2) # hide
for n in self.pocketPropertyEnumerations():
setattr(obj, n[0], n[1])
@@ -246,6 +265,42 @@ class ObjectPocket(PathAreaOp.ObjectOp):
),
)
obj.PocketLastStepOver = 0
+
+ if not hasattr(obj, "UseRestMachining"):
+ obj.addProperty(
+ "App::PropertyBool",
+ "UseRestMachining",
+ "Pocket",
+ QT_TRANSLATE_NOOP(
+ "App::Property",
+ "Skips machining regions that have already been cleared by previous operations.",
+ ),
+ )
+
+ if not hasattr(obj, "RestMachiningRegions"):
+ obj.addProperty(
+ "Part::PropertyPartShape",
+ "RestMachiningRegions",
+ "Pocket",
+ QT_TRANSLATE_NOOP(
+ "App::Property",
+ "The areas cleared by this operation, one area per height, stored as a compound part. Used internally for rest machining.",
+ ),
+ )
+ obj.setEditorMode("RestMachiningRegions", 2) # hide
+
+ obj.addProperty(
+ "App::PropertyBool",
+ "RestMachiningRegionsNeedRecompute",
+ "Pocket",
+ QT_TRANSLATE_NOOP(
+ "App::Property",
+ "Flag to indicate that the rest machining regions have never been computed, and must be recomputed before being used.",
+ ),
+ )
+ obj.setEditorMode("RestMachiningRegionsNeedRecompute", 2) # hide
+ obj.RestMachiningRegionsNeedRecompute = True
+
Path.Log.track()
def areaOpPathParams(self, obj, isHole):