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