/************************************************************************** * Copyright (c) 2022 Werner Mayer * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library 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 library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ #include #include #include #include #include #include #include #include "SketchWorkflow.h" #include "DlgActiveBody.h" #include "TaskFeaturePick.h" #include "Utils.h" #include "ViewProviderBody.h" #include "WorkflowManager.h" #include "ui_DlgReference.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace PartDesignGui; namespace { struct RejectException { }; struct WrongSelectionException { }; struct WrongSupportException { }; struct SupportNotPlanarException { }; struct MissingPlanesException { }; class SupportFaceValidator { public: explicit SupportFaceValidator(Gui::SelectionObject faceSelection) : faceSelection(faceSelection) {} void handleSelectedBody(PartDesign::Body* activeBody) { App::DocumentObject* object = faceSelection.getObject(); std::vector elements = faceSelection.getSubNames(); // In case the selected face belongs to the body then it means its // Display Mode Body is set to Tip. But the body face is not allowed // to be used as support because otherwise it would cause a cyclic // dependency. So, instead we use the tip object as reference. // https://forum.freecad.org/viewtopic.php?f=3&t=37448 if (object == activeBody) { App::DocumentObject* tip = activeBody->Tip.getValue(); if (tip && tip->isDerivedFrom() && elements.size() == 1) { Gui::SelectionChanges msg; msg.pDocName = faceSelection.getDocName(); msg.pObjectName = tip->getNameInDocument(); msg.pSubName = elements[0].c_str(); msg.pTypeName = tip->getTypeId().getName(); faceSelection = Gui::SelectionObject {msg}; // automatically switch to 'Through' mode setThroughModeOfBody(activeBody); } } } void throwIfInvalid() { App::DocumentObject* object = faceSelection.getObject(); std::vector elements = faceSelection.getSubNames(); Part::Feature* partobject = dynamic_cast(object); if (!partobject) { throw WrongSelectionException(); } if (elements.size() != 1) { throw WrongSelectionException(); } // get the selected sub shape (a Face) const Part::TopoShape& shape = partobject->Shape.getValue(); Part::TopoShape subshape(shape.getSubShape(elements[0].c_str())); if (subshape.isNull()) { throw WrongSupportException(); } if (!subshape.isPlanar(Attacher::AttachEnginePlane::planarPrecision())) { throw SupportNotPlanarException(); } } std::string getSupport() const { return faceSelection.getAsPropertyLinkSubString(); } App::DocumentObject* getObject() const { return faceSelection.getObject(); } private: void setThroughModeOfBody(PartDesign::Body* activeBody) { // automatically switch to 'Through' mode PartDesignGui::ViewProviderBody* vpBody = dynamic_cast( Gui::Application::Instance->getViewProvider(activeBody) ); if (vpBody) { vpBody->DisplayModeBody.setValue("Through"); } } private: mutable Gui::SelectionObject faceSelection; }; class SupportPlaneValidator { public: explicit SupportPlaneValidator(Gui::SelectionObject faceSelection) : faceSelection(faceSelection) {} std::string getSupport() const { return faceSelection.getAsPropertyLinkSubString(); } App::DocumentObject* getObject() const { return faceSelection.getObject(); } private: mutable Gui::SelectionObject faceSelection; }; class SketchPreselection { public: SketchPreselection( Gui::Document* guidocument, PartDesign::Body* activeBody, std::tuple filter ) : guidocument(guidocument) , activeBody(activeBody) , faceFilter(std::get<0>(filter)) , planeFilter(std::get<1>(filter)) , sketchFilter(std::get<2>(filter)) {} bool matches() { return faceFilter.match() || planeFilter.match() || sketchFilter.match(); } std::string getSupport() const { return supportString; } void createSupport() { createBodyOrThrow(); // get the selected object App::DocumentObject* selectedObject {}; if (faceFilter.match()) { Gui::SelectionObject faceSelObject = faceFilter.Result[0][0]; SupportFaceValidator validator {faceSelObject}; validator.handleSelectedBody(activeBody); validator.throwIfInvalid(); selectedObject = validator.getObject(); supportString = validator.getSupport(); } else if (planeFilter.match()) { SupportPlaneValidator validator(planeFilter.Result[0][0]); selectedObject = validator.getObject(); supportString = validator.getSupport(); } else { // For a sketch, the support is the object itself with no sub-element. Gui::SelectionObject sketchSelObject = sketchFilter.Result[0][0]; selectedObject = sketchSelObject.getObject(); supportString = sketchSelObject.getAsPropertyLinkSubString(); } handleIfSupportOutOfBody(selectedObject); } void createSketchOnSupport(const std::string& supportString) { // create Sketch on Face or Plane App::Document* appdocument = guidocument->getDocument(); std::string FeatName = appdocument->getUniqueObjectName("Sketch"); guidocument->openCommand(QT_TRANSLATE_NOOP("Command", "Sketch on Face")); FCMD_OBJ_CMD(activeBody, "newObject('Sketcher::SketchObject','" << FeatName << "')"); auto Feat = activeBody->getDocument()->getObject(FeatName.c_str()); FCMD_OBJ_CMD(Feat, "AttachmentSupport = " << supportString); if (sketchFilter.match()) { FCMD_OBJ_CMD( Feat, "MapMode = '" << Attacher::AttachEngine::getModeName(Attacher::mmObjectXY) << "'" ); } else { // For Face or Plane FCMD_OBJ_CMD( Feat, "MapMode = '" << Attacher::AttachEngine::getModeName(Attacher::mmFlatFace) << "'" ); } Gui::Command::updateActive(); PartDesignGui::setEdit(Feat, activeBody); } private: void createBodyOrThrow() { if (!activeBody) { activeBody = PartDesignGui::getBody(/* messageIfNot = */ true); if (activeBody) { tryAddNewBodyToActivePart(); } else { throw RejectException(); } } } void tryAddNewBodyToActivePart() { App::Part* activePart = PartDesignGui::getActivePart(); if (activePart) { activePart->addObject(activeBody); } } void handleIfSupportOutOfBody(App::DocumentObject* selectedObject) { if (!activeBody->hasObject(selectedObject)) { if (!selectedObject->isDerivedFrom(App::Plane::getClassTypeId())) { // TODO check here if the plane associated with right part/body (2015-09-01, Fat-Zer) // check the prerequisites for the selected objects // the user has to decide which option we should take if external references are used // TODO share this with UnifiedDatumCommand() (2015-10-20, Fat-Zer) QDialog dia(Gui::getMainWindow()); PartDesignGui::Ui_DlgReference dlg; dlg.setupUi(&dia); dia.setModal(true); int result = dia.exec(); if (result == QDialog::Rejected) { throw RejectException(); } if (!dlg.radioXRef->isChecked()) { guidocument->openCommand(QT_TRANSLATE_NOOP("Command", "Make copy")); auto copy = makeCopy(selectedObject, dlg.radioIndependent->isChecked()); supportString = supportFromCopy(copy); guidocument->commitCommand(); } } } } App::DocumentObject* makeCopy(App::DocumentObject* selectedObject, bool independent) { std::string sub; if (faceFilter.match()) { sub = faceFilter.Result[0][0].getSubNames()[0]; } auto copy = PartDesignGui::TaskFeaturePick::makeCopy(selectedObject, sub, independent); addToBodyOrPart(copy); return copy; } std::string supportFromCopy(App::DocumentObject* copy) { std::string supportString; if (planeFilter.match()) { supportString = Gui::Command::getObjectCmd(copy, "(", ",'')"); } else { // it is ensured that only a single face is selected, hence it must always be Face1 of // the shapebinder supportString = Gui::Command::getObjectCmd(copy, "(", ",'Face1')"); } return supportString; } void addToBodyOrPart(App::DocumentObject* object) { auto activePart = PartDesignGui::getPartFor(activeBody, false); if (activeBody) { activeBody->addObject(object); } else if (activePart) { activePart->addObject(object); } } private: Gui::Document* guidocument; PartDesign::Body* activeBody; Gui::SelectionFilter faceFilter; Gui::SelectionFilter planeFilter; Gui::SelectionFilter sketchFilter; std::string supportString; }; class PlaneFinder { public: PlaneFinder(App::Document* appdocument, PartDesign::Body* activeBody) : appdocument(appdocument) , activeBody(activeBody) {} std::vector getPlanes() const { return planes; } std::vector getStatus() const { return status; } unsigned countValidPlanes() const { return validPlaneCount; } void findBasePlanes() { try { tryFindBasePlanes(); } catch (const Base::Exception& ex) { Base::Console().error("%s\n", ex.what()); } } void findDatumPlanes() { App::GeoFeatureGroupExtension* geoGroup = getGroupExtensionOfBody(); const std::vector types = {PartDesign::Plane::getClassTypeId(), App::Plane::getClassTypeId()}; auto datumPlanes = appdocument->getObjectsOfType(types); for (auto plane : datumPlanes) { if (std::find(planes.begin(), planes.end(), plane) != planes.end()) { continue; // Skip if already in planes (for base planes) } planes.push_back(plane); // Check whether this plane belongs to the active body if (activeBody->hasObject(plane, true)) { if (!activeBody->isAfterInsertPoint(plane)) { validPlaneCount++; status.push_back(PartDesignGui::TaskFeaturePick::validFeature); } else { status.push_back(PartDesignGui::TaskFeaturePick::afterTip); } } else { PartDesign::Body* planeBody = PartDesign::Body::findBodyOf(plane); if (planeBody) { if ((geoGroup && geoGroup->hasObject(planeBody, true)) || !App::GeoFeatureGroupExtension::getGroupOfObject(planeBody)) { status.push_back(PartDesignGui::TaskFeaturePick::otherBody); } else { status.push_back(PartDesignGui::TaskFeaturePick::otherPart); } } else { if ((geoGroup && geoGroup->hasObject(plane, true)) || App::GeoFeatureGroupExtension::getGroupOfObject(plane)) { status.push_back(PartDesignGui::TaskFeaturePick::otherPart); } else { status.push_back(PartDesignGui::TaskFeaturePick::notInBody); } } } } } void findShapeBinderPlanes() { // Collect also shape binders consisting of a single planar face auto shapeBinders(appdocument->getObjectsOfType(PartDesign::ShapeBinder::getClassTypeId())); auto binders(appdocument->getObjectsOfType(PartDesign::SubShapeBinder::getClassTypeId())); shapeBinders.insert(shapeBinders.end(), binders.begin(), binders.end()); for (auto binder : shapeBinders) { // Check whether this plane belongs to the active body if (activeBody->hasObject(binder)) { Part::TopoShape shape = static_cast(binder)->Shape.getShape(); if (shape.isPlanar()) { if (!activeBody->isAfterInsertPoint(binder)) { validPlaneCount++; planes.push_back(binder); status.push_back(PartDesignGui::TaskFeaturePick::validFeature); } } } } } private: void tryFindBasePlanes() { auto* origin = activeBody->getOrigin(); for (auto plane : origin->planes()) { planes.push_back(plane); status.push_back(PartDesignGui::TaskFeaturePick::basePlane); validPlaneCount++; } } App::GeoFeatureGroupExtension* getGroupExtensionOfBody() const { App::GeoFeatureGroupExtension* geoGroup {nullptr}; if (activeBody) { auto group(App::GeoFeatureGroupExtension::getGroupOfObject(activeBody)); if (group) { geoGroup = group->getExtensionByType(); } } return geoGroup; } private: App::Document* appdocument; PartDesign::Body* activeBody; unsigned validPlaneCount = 0; std::vector planes; std::vector status; }; class SketchRequestSelection { public: SketchRequestSelection(Gui::Document* guidocument, PartDesign::Body* activeBody) : guidocument(guidocument) , activeBody(activeBody) {} void findSupport() { try { // Start command early, so undo will undo any Body creation guidocument->openCommand(QT_TRANSLATE_NOOP("Command", "New Sketch")); tryFindSupport(); } catch (const RejectException&) { guidocument->abortCommand(); throw; } catch (const MissingPlanesException&) { guidocument->abortCommand(); throw; } } private: void tryFindSupport() { createBodyOrThrow(); bool useAttachment = App::GetApplication() .GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/PartDesign" ) ->GetBool("NewSketchUseAttachmentDialog", false); if (useAttachment) { createSketchAndShowAttachment(); } else { findAndSelectPlane(); } } void createBodyOrThrow() { if (!activeBody) { App::Document* appdocument = guidocument->getDocument(); activeBody = PartDesignGui::makeBody(appdocument); if (activeBody) { tryAddNewBodyToActivePart(); } else { throw RejectException(); } } } void tryAddNewBodyToActivePart() { App::Part* activePart = PartDesignGui::getActivePart(); if (activePart) { activePart->addObject(activeBody); } } void setOriginTemporaryVisibility() { auto* origin = activeBody->getOrigin(); auto* vpo = dynamic_cast( Gui::Application::Instance->getViewProvider(origin) ); if (vpo) { vpo->setTemporaryVisibility(Gui::DatumElement::Planes | Gui::DatumElement::Axes); vpo->setPlaneLabelVisibility(true); } } void createSketchAndShowAttachment() { setOriginTemporaryVisibility(); // Create sketch App::Document* doc = activeBody->getDocument(); std::string FeatName = doc->getUniqueObjectName("Sketch"); FCMD_OBJ_CMD(activeBody, "newObject('Sketcher::SketchObject','" << FeatName << "')"); auto sketch = doc->getObject(FeatName.c_str()); PartDesign::Body* partDesignBody = activeBody; auto onAccept = [partDesignBody, sketch]() { resetOriginVisibility(partDesignBody); Gui::Selection().clearSelection(); PartDesignGui::setEdit(sketch, partDesignBody); }; auto onReject = [partDesignBody]() { resetOriginVisibility(partDesignBody); }; Gui::Selection().clearSelection(); // Open attachment dialog auto* vps = dynamic_cast( Gui::Application::Instance->getViewProvider(sketch) ); vps->showAttachmentEditor(onAccept, onReject); } static void resetOriginVisibility(PartDesign::Body* partDesignBody) { auto* origin = partDesignBody->getOrigin(); auto* vpo = dynamic_cast( Gui::Application::Instance->getViewProvider(origin) ); if (vpo) { vpo->resetTemporaryVisibility(); vpo->resetTemporarySize(); vpo->setPlaneLabelVisibility(false); } } void findAndSelectPlane() { App::Document* appdocument = guidocument->getDocument(); PlaneFinder planeFinder {appdocument, activeBody}; planeFinder.findBasePlanes(); planeFinder.findDatumPlanes(); planeFinder.findShapeBinderPlanes(); std::vector planes = planeFinder.getPlanes(); std::vector status = planeFinder.getStatus(); unsigned validPlaneCount = planeFinder.countValidPlanes(); for (auto& plane : planes) { auto* planeViewProvider = Gui::Application::Instance->getViewProvider(plane); // skip updating planes from coordinate systems if (!planeViewProvider || !planeViewProvider->getRole().empty()) { continue; } planeViewProvider->setLabelVisibility(true); planeViewProvider->setTemporaryScale( Gui::ViewParams::instance()->getDatumTemporaryScaleFactor() ); } // // Lambda definitions // App::Document* documentOfBody = appdocument; PartDesign::Body* partDesignBody = activeBody; auto restorePlaneVisibility = [planes]() { for (auto& plane : planes) { auto* planeViewProvider = Gui::Application::Instance->getViewProvider(plane); if (!planeViewProvider) { continue; } planeViewProvider->resetTemporarySize(); planeViewProvider->setLabelVisibility(false); } }; // Determines if user made a valid selection in dialog auto acceptFunction = [restorePlaneVisibility](const std::vector& features) -> bool { restorePlaneVisibility(); return !features.empty(); }; // Called by dialog when user hits "OK" and accepter returns true auto processFunction = [documentOfBody, partDesignBody](const std::vector& features) { SketchRequestSelection::createSketch(documentOfBody, partDesignBody, features); }; // Called by dialog for "Cancel", or "OK" if accepter returns false std::string docname = documentOfBody->getName(); auto rejectFunction = [docname, restorePlaneVisibility]() { restorePlaneVisibility(); Gui::Document* document = Gui::Application::Instance->getDocument(docname.c_str()); if (document) { document->abortCommand(); } }; // // End of lambda definitions // if (validPlaneCount == 0) { throw MissingPlanesException(); } else if (validPlaneCount == 1) { processFunction(planes); } else if (validPlaneCount > 1) { checkForShownDialog(); Gui::Selection().clearSelection(); // Show dialog and let user pick plane Gui::Control().showDialog(new PartDesignGui::TaskDlgFeaturePick( planes, status, acceptFunction, processFunction, true, rejectFunction )); } } void checkForShownDialog() { Gui::TaskView::TaskDialog* dlg = Gui::Control().activeDialog(); PartDesignGui::TaskDlgFeaturePick* pickDlg = qobject_cast(dlg); if (dlg && !pickDlg) { QMessageBox msgBox(Gui::getMainWindow()); msgBox.setText(QObject::tr("A dialog is already open in the task panel")); msgBox.setInformativeText(QObject::tr("Close this dialog?")); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::Yes); int ret = msgBox.exec(); if (ret == QMessageBox::Yes) { Gui::Control().closeDialog(); } else { throw RejectException(); } } if (dlg) { Gui::Control().closeDialog(); } } static void createSketch( App::Document* documentOfBody, PartDesign::Body* partDesignBody, const std::vector& features ) { // may happen when the user switched to an empty document while the // dialog is open if (features.empty()) { return; } std::string FeatName = documentOfBody->getUniqueObjectName("Sketch"); auto* plane = static_cast(features.front()); auto* lcs = plane->getLCS(); std::string supportString; if (lcs) { supportString = Gui::Command::getObjectCmd(lcs, "(") + ",['" + plane->getNameInDocument() + "'])"; } else { supportString = Gui::Command::getObjectCmd(plane, "(", ",[''])"); } App::Document* doc = partDesignBody->getDocument(); if (!doc->hasPendingTransaction()) { doc->openTransaction(QT_TRANSLATE_NOOP("Command", "New Sketch")); } FCMD_OBJ_CMD(partDesignBody, "newObject('Sketcher::SketchObject','" << FeatName << "')"); auto Feat = doc->getObject(FeatName.c_str()); FCMD_OBJ_CMD(Feat, "AttachmentSupport = " << supportString); FCMD_OBJ_CMD( Feat, "MapMode = '" << Attacher::AttachEngine::getModeName(Attacher::mmFlatFace) << "'" ); Gui::Command::updateActive(); // Make sure the AttachmentSupport's Placement property is updated PartDesignGui::setEdit(Feat, partDesignBody); } private: Gui::Document* guidocument; PartDesign::Body* activeBody; }; } // namespace SketchWorkflow::SketchWorkflow(Gui::Document* document) : guidocument(document) { appdocument = guidocument->getDocument(); } void SketchWorkflow::createSketch() { try { tryCreateSketch(); } catch (const RejectException&) { } catch (const WrongSelectionException&) { QMessageBox::warning( Gui::getMainWindow(), QObject::tr("Several sub-elements selected"), QObject::tr("Select a single face as support for a sketch!") ); } catch (const WrongSupportException&) { QMessageBox::warning( Gui::getMainWindow(), QObject::tr("No support face selected"), QObject::tr("Select a face as support for a sketch!") ); } catch (const SupportNotPlanarException&) { QMessageBox::warning( Gui::getMainWindow(), QObject::tr("No planar support"), QObject::tr("Need a planar face as support for a sketch!") ); } catch (const MissingPlanesException&) { QMessageBox::warning( Gui::getMainWindow(), QObject::tr("No valid planes in this document"), QObject::tr("Create a plane first or select a face to sketch on") ); } } void SketchWorkflow::tryCreateSketch() { auto result = shouldCreateBody(); auto shouldMakeBody = std::get<0>(result); activeBody = std::get<1>(result); if (shouldAbort(shouldMakeBody)) { return; } auto filters = getFilters(); SketchPreselection sketchOnFace {guidocument, activeBody, filters}; if (sketchOnFace.matches()) { // create Sketch on Face or Plane sketchOnFace.createSupport(); sketchOnFace.createSketchOnSupport(sketchOnFace.getSupport()); } else { SketchRequestSelection requestSelection {guidocument, activeBody}; requestSelection.findSupport(); } } std::tuple SketchWorkflow::shouldCreateBody() { auto shouldMakeBody {false}; // We need either an active Body, or for there to be no Body // objects (in which case, just make one) to make a new sketch. // If we are inside a link, we need to use its placement. App::DocumentObject* topParent; PartDesign::Body* pdBody = PartDesignGui::getBody(/* messageIfNot = */ false, true, true, &topParent); if (pdBody && topParent->isLink()) { auto* xLink = dynamic_cast(topParent); pdBody->Placement.setValue(xLink->Placement.getValue()); } if (!pdBody) { if (appdocument->countObjectsOfType() == 0) { shouldMakeBody = true; } else { PartDesignGui::DlgActiveBody dia(Gui::getMainWindow(), appdocument); if (dia.exec() == QDialog::Accepted) { pdBody = dia.getActiveBody(); } } } return std::make_tuple(shouldMakeBody, pdBody); } bool SketchWorkflow::shouldAbort(bool shouldMakeBody) const { return !shouldMakeBody && !activeBody; } std::tuple SketchWorkflow::getFilters() const { // Hint: // The behaviour of this command has changed with respect to a selected sketch: // It doesn't try any more to edit a selected sketch but always tries to create // a new sketch. // See https://forum.freecad.org/viewtopic.php?f=3&t=44070 Gui::SelectionFilter FaceFilter("SELECT Part::Feature SUBELEMENT Face COUNT 1"); Gui::SelectionFilter PlaneFilter("SELECT App::Plane COUNT 1", activeBody); Gui::SelectionFilter PlaneFilter2("SELECT PartDesign::Plane COUNT 1", activeBody); Gui::SelectionFilter SketchFilter("SELECT Part::Part2DObject COUNT 1", activeBody); if (PlaneFilter2.match()) { PlaneFilter = PlaneFilter2; } return std::make_tuple(FaceFilter, PlaneFilter, SketchFilter); }