Merge pull request #9782 from davidgilkaufman/restMachining

Rest Machining for Path and Path3D Operations
This commit is contained in:
sliptonic
2023-08-21 11:13:01 -05:00
committed by GitHub
11 changed files with 283 additions and 6 deletions

View File

@@ -496,6 +496,79 @@ void Area::add(const TopoDS_Shape& shape, short op) {
myShapes.emplace_back(op, shape);
}
std::shared_ptr<Area> 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<Area> clearedArea = make_shared<Area>(*this);
clearedArea->myArea.reset(new CArea(prevCleared));
return clearedArea;
}
std::shared_ptr<Area> Area::getRestArea(std::vector<std::shared_ptr<Area>> 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<Area> 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<Area> restArea = make_shared<Area>(&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) {

View File

@@ -242,6 +242,10 @@ public:
const std::vector<double>& heights = std::vector<double>(),
const TopoDS_Shape& plane = TopoDS_Shape());
std::shared_ptr<Area> getClearedArea(double tipDiameter, double diameter);
std::shared_ptr<Area> getRestArea(std::vector<std::shared_ptr<Area>> clearedAreas, double diameter);
TopoDS_Shape toTopoShape();
/** Config this Area object */
void setParams(const AreaParams& params);

View File

@@ -62,6 +62,21 @@ same algorithm</UserDocu>
<UserDocu></UserDocu>
</Documentation>
</Methode>
<Methode Name="getClearedArea" Keyword="true">
<Documentation>
<UserDocu></UserDocu>
</Documentation>
</Methode>
<Methode Name="getRestArea" Keyword="true">
<Documentation>
<UserDocu></UserDocu>
</Documentation>
</Methode>
<Methode Name="toTopoShape" Keyword="true">
<Documentation>
<UserDocu></UserDocu>
</Documentation>
</Methode>
<Methode Name="setParams" Keyword="true">
<Documentation>
<UserDocu></UserDocu>

View File

@@ -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<PyCFunction>(reinterpret_cast<void (*) ()>(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<Area> 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<std::shared_ptr<Area>> 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<Area>(*static_cast<AreaPy*>(item)->getAreaPtr(), true));
}
} else {
PyErr_SetString(PyExc_TypeError, "clearedAreas must be of type list of AreaPy");
return nullptr;
}
std::shared_ptr<Area> 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;

View File

@@ -245,14 +245,24 @@ The latter can be used to face of the entire stock area to ensure uniform height
</property>
</widget>
</item>
<item row="2" column="1">
<item row="2" column="1">
<widget class="QCheckBox" name="minTravel">
<property name="text">
<string>Min Travel</string>
</property>
</widget>
</item>
</layout>
<item row="3" column="0">
<widget class="QCheckBox" name="useRestMachining">
<property name="toolTip">
<string>Check to skip machining regions that have already been cleared by previous operations</string>
</property>
<property name="text">
<string>Use Rest Machining</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="4" column="0">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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