/*************************************************************************** * Copyright (c) 2016 WandererFan * * Copyright (c) 2024 Benjamin Bræstrup Sayoc * * * * 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 #include #include # include # include # include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "DlgPageChooser.h" #include "DrawGuiUtil.h" #include "MDIViewPage.h" #include "QGIEdge.h" #include "QGIVertex.h" #include "QGIViewPart.h" #include "QGSPage.h" #include "ViewProviderPage.h" #include "Rez.h" using namespace TechDrawGui; using namespace TechDraw; using DU = DrawUtil; void DrawGuiUtil::loadArrowBox(QComboBox* qcb) { qcb->clear(); auto curStyleSheet = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/MainWindow") ->GetASCII("StyleSheet", "None"); int i = 0; for (; i < ArrowPropEnum::ArrowCount; i++) { qcb->addItem( QCoreApplication::translate("ArrowPropEnum", ArrowPropEnum::ArrowTypeEnums[i])); QIcon itemIcon(QString::fromUtf8(ArrowPropEnum::ArrowTypeIcons[i].c_str())); if (isStyleSheetDark(curStyleSheet)) { QColor textColor = Preferences::lightTextColor().asValue(); QSize iconSize(48, 48); QIcon itemUpdatedIcon(maskBlackPixels(itemIcon, iconSize, textColor)); qcb->setItemIcon(i, itemUpdatedIcon); } else { qcb->setItemIcon(i, itemIcon); } } } void DrawGuiUtil::loadBalloonShapeBox(QComboBox* qballooncb) { qballooncb->clear(); auto curStyleSheet = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/MainWindow") ->GetASCII("StyleSheet", "None"); int i = 0; for (; i < BalloonPropEnum::BalloonCount; i++) { qballooncb->addItem( QCoreApplication::translate("BalloonPropEnum", BalloonPropEnum::BalloonTypeEnums[i])); QIcon itemIcon(QString::fromUtf8(BalloonPropEnum::BalloonTypeIcons[i].c_str())); if (isStyleSheetDark(curStyleSheet)) { QColor textColor = Preferences::lightTextColor().asValue(); QSize iconSize(48, 48); QIcon itemUpdatedIcon(maskBlackPixels(itemIcon, iconSize, textColor)); qballooncb->setItemIcon(i, itemUpdatedIcon); } else { qballooncb->setItemIcon(i, itemIcon); } } } void DrawGuiUtil::loadMattingStyleBox(QComboBox* qmattingcb) { qmattingcb->clear(); auto curStyleSheet = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/MainWindow") ->GetASCII("StyleSheet", "None"); int i = 0; for (; i < MattingPropEnum::MattingCount; i++) { qmattingcb->addItem( QCoreApplication::translate("MattingPropEnum", MattingPropEnum::MattingTypeEnums[i])); QIcon itemIcon(QString::fromUtf8(MattingPropEnum::MattingTypeIcons[i].c_str())); if (isStyleSheetDark(curStyleSheet)) { QColor textColor = Preferences::lightTextColor().asValue(); QSize iconSize(48, 48); QIcon itemUpdatedIcon(maskBlackPixels(itemIcon, iconSize, textColor)); qmattingcb->setItemIcon(i, itemUpdatedIcon); } else { qmattingcb->setItemIcon(i, itemIcon); } } } void DrawGuiUtil::loadLineStandardsChoices(QComboBox* combo) { combo->clear(); std::vector choices = LineGenerator::getAvailableLineStandards(); for (auto& entry : choices) { QString qentry = QString::fromStdString(entry); combo->addItem(qentry); } } void DrawGuiUtil::loadLineStyleChoices(QComboBox* combo, LineGenerator* generator) { combo->clear(); std::vector choices; if (generator) { choices = generator->getLoadedDescriptions(); } else { choices = LineGenerator::getLineDescriptions(); } auto translationContext = LineName::currentTranslationContext(); int itemNumber {0}; for (auto& entry : choices) { QString qentry = QCoreApplication::translate(translationContext.c_str(), entry.c_str()); combo->addItem(qentry); if (generator) { combo->setItemIcon(itemNumber, iconForLine(itemNumber + 1, generator)); } itemNumber++; } } void DrawGuiUtil::loadLineGroupChoices(QComboBox* combo) { combo->clear(); std::string lgFileName = Preferences::lineGroupFile(); std::string lgRecord = LineGroup::getGroupNamesFromFile(lgFileName); // split collected groups std::stringstream ss(lgRecord); std::vector lgNames; while (std::getline(ss, lgRecord, ',')) { lgNames.push_back(QString::fromStdString(lgRecord)); } // fill the combobox with the found names for (auto& name : lgNames) { combo->addItem(name); } } //! make an icon that shows a sample of lineNumber in the current line standard QIcon DrawGuiUtil::iconForLine(size_t lineNumber, TechDraw::LineGenerator* generator) { // Base::Console().message("DGU::iconForLine(lineNumber: %d)\n", lineNumber); constexpr int iconSize {64}; constexpr int borderSize {4}; constexpr double iconLineWeight {1.0}; size_t lineCount {4}; double maxLineLength = iconSize - borderSize * 2.0; QBitmap bitmap {iconSize, iconSize}; bitmap.clear(); QPainter painter(&bitmap); QPen linePen = generator->getLinePen(lineNumber, iconLineWeight); linePen.setDashOffset(0.0); linePen.setCapStyle(Qt::FlatCap); linePen.setColor(Qt::color1); QSize lineIconSize(iconSize, iconSize); auto curStyleSheet = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/MainWindow") ->GetASCII("StyleSheet", "None"); QColor textColor{Qt::black}; if (isStyleSheetDark(curStyleSheet)) { textColor = Preferences::lightTextColor().asValue(); } // handle simple case of continuous line if (linePen.style() == Qt::SolidLine) { linePen.setWidthF(iconLineWeight * lineCount); painter.setPen(linePen); painter.drawLine(borderSize, iconSize / 2, iconSize - borderSize, iconSize / 2); if (isStyleSheetDark(curStyleSheet)) { QIcon lineItemIcon(bitmap); return QIcon(maskBlackPixels(lineItemIcon, lineIconSize, textColor)); } else { return QIcon(bitmap); } } // dashed line linePen.setWidthF(iconLineWeight); painter.setPen(linePen); double yHeight = (iconSize / 2) - (lineCount * iconLineWeight); size_t iLine = 0; // draw multiple lines to stretch the line vertically without horizontal // distortion for (; iLine < lineCount; iLine++) { painter.drawLine(borderSize, yHeight, maxLineLength, yHeight); yHeight += iconLineWeight; } if (isStyleSheetDark(curStyleSheet)) { QIcon lineItemIcon(bitmap); return QIcon(maskBlackPixels(lineItemIcon, lineIconSize, textColor)); } else { return QIcon(bitmap); } } //=========================================================================== // validate helper routines //=========================================================================== // find a page in Selection, Document or CurrentWindow. TechDraw::DrawPage* DrawGuiUtil::findPage(Gui::Command* cmd, bool findAny) { std::vector names; std::vector labels; auto docs = App::GetApplication().getDocuments(); if (findAny) { // find a page in any open document std::vector foundPageObjects; // no page found in the usual places, but we have been asked to search all // open documents for a page. auto docsAll = App::GetApplication().getDocuments(); for (auto& doc : docsAll) { auto docPages = doc->getObjectsOfType(TechDraw::DrawPage::getClassTypeId()); if (docPages.empty()) { // this open document has no TD pages continue; } foundPageObjects.insert(foundPageObjects.end(), docPages.begin(), docPages.end()); } if (foundPageObjects.empty()) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("No page found"), QObject::tr("No Drawing Pages available.")); return nullptr; } if (foundPageObjects.size() > 1) { // multiple pages available, ask for help for (auto obj : foundPageObjects) { std::string name = obj->getNameInDocument(); names.push_back(name); std::string label = obj->Label.getValue(); labels.push_back(label); } DlgPageChooser dlg(labels, names, Gui::getMainWindow()); if (dlg.exec() == QDialog::Accepted) { std::string selName = dlg.getSelection(); if (selName.empty()) { showNoPageMessage(); return nullptr; } App::Document* doc = cmd->getDocument(); return static_cast(doc->getObject(selName.c_str())); } } else { // only 1 page found return static_cast(foundPageObjects.front()); } } // check Selection for a page std::vector selPages = Gui::Command::getSelection().getObjectsOfType(TechDraw::DrawPage::getClassTypeId()); if (selPages.empty()) { // no page in selection, try this document auto docPages = cmd->getDocument()->getObjectsOfType(TechDraw::DrawPage::getClassTypeId()); if (docPages.empty()) { // we are only to look in this document, and there is no page in this document showNoPageMessage(); return nullptr; } if (docPages.size() > 1) { // multiple pages in document, use active page if there is one auto* w = Gui::getMainWindow(); auto* mv = w->activeWindow(); auto* mvp = qobject_cast(mv); if (mvp) { QGSPage* qp = mvp->getViewProviderPage()->getQGSPage(); return qp->getDrawPage(); } // none of pages in document is active, ask for help for (auto obj : docPages) { std::string name = obj->getNameInDocument(); names.push_back(name); std::string label = obj->Label.getValue(); labels.push_back(label); } DlgPageChooser dlg(labels, names, Gui::getMainWindow()); if (dlg.exec() == QDialog::Accepted) { std::string selName = dlg.getSelection(); if (selName.empty()) { showNoPageMessage(); return nullptr; } App::Document* doc = cmd->getDocument(); return static_cast(doc->getObject(selName.c_str())); } return nullptr; } // only 1 page in document - use it return static_cast(docPages.front()); } if (selPages.size() > 1) { // multiple pages in selection for (auto obj : selPages) { std::string name = obj->getNameInDocument(); names.push_back(name); std::string label = obj->Label.getValue(); labels.push_back(label); } DlgPageChooser dlg(labels, names, Gui::getMainWindow()); if (dlg.exec() == QDialog::Accepted) { std::string selName = dlg.getSelection(); if (selName.empty()) { showNoPageMessage(); return nullptr; } App::Document* doc = cmd->getDocument(); return static_cast(doc->getObject(selName.c_str())); } } else { // exactly 1 page in selection, use it return static_cast(selPages.front()); } return nullptr; } void DrawGuiUtil::showNoPageMessage() { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("No page selected"), QObject::tr("This function needs a page.")); } bool DrawGuiUtil::isDraftObject(App::DocumentObject* obj) { bool result = false; App::PropertyPythonObject* proxy = dynamic_cast(obj->getPropertyByName("Proxy")); if (proxy) { // if no proxy, can not be Draft obj // if has proxy, might be Draft obj std::stringstream ss; Py::Object proxyObj = proxy->getValue(); Base::PyGILStateLocker lock; try { if (proxyObj.hasAttr("__module__")) { Py::String mod(proxyObj.getAttr("__module__")); ss << (std::string)mod; if (ss.str().find("Draft") != std::string::npos) { result = true; } else if (ss.str().find("draft") != std::string::npos) { result = true; } } } catch (Py::Exception&) { Base::PyException e; // extract the Python error text e.reportException(); result = false; } } return result; } bool DrawGuiUtil::isArchObject(App::DocumentObject* obj) { bool result = false; App::PropertyPythonObject* proxy = dynamic_cast(obj->getPropertyByName("Proxy")); if (proxy) { // if no proxy, can not be Arch obj // if has proxy, might be Arch obj Py::Object proxyObj = proxy->getValue(); std::stringstream ss; Base::PyGILStateLocker lock; try { if (proxyObj.hasAttr("__module__")) { Py::String mod(proxyObj.getAttr("__module__")); ss << (std::string)mod; // does this have to be an ArchSection, or can it be any Arch object? if (ss.str().find("Arch") != std::string::npos) { result = true; } } } catch (Py::Exception&) { Base::PyException e; // extract the Python error text e.reportException(); result = false; } } return result; } bool DrawGuiUtil::isArchSection(App::DocumentObject* obj) { bool result = false; App::PropertyPythonObject* proxy = dynamic_cast(obj->getPropertyByName("Proxy")); if (proxy) { // if no proxy, can not be Arch obj // if has proxy, might be Arch obj Py::Object proxyObj = proxy->getValue(); std::stringstream ss; Base::PyGILStateLocker lock; try { if (proxyObj.hasAttr("__module__")) { Py::String mod(proxyObj.getAttr("__module__")); ss << (std::string)mod; // does this have to be an ArchSection, or can it be other Arch objects? if (ss.str().find("ArchSectionPlane") != std::string::npos) { result = true; } } } catch (Py::Exception&) { Base::PyException e; // extract the Python error text e.reportException(); result = false; } } return result; } bool DrawGuiUtil::needPage(Gui::Command* cmd, bool findAny) { if (findAny) { // look for any page in any open document auto docsAll = App::GetApplication().getDocuments(); for (auto& doc : docsAll) { auto docPages = doc->getObjectsOfType(TechDraw::DrawPage::getClassTypeId()); if (docPages.empty()) { // this open document has no TD pages continue; } else { // found at least 1 page return true; } } // did not find any pages return false; } // need a Document and a Page if (cmd->hasActiveDocument()) { auto drawPageType(TechDraw::DrawPage::getClassTypeId()); auto selPages = cmd->getDocument()->getObjectsOfType(drawPageType); return !selPages.empty(); } return false; } bool DrawGuiUtil::needView(Gui::Command* cmd, bool partOnly) { bool haveView = false; if (cmd->hasActiveDocument()) { if (partOnly) { auto drawPartType(TechDraw::DrawViewPart::getClassTypeId()); auto selParts = cmd->getDocument()->getObjectsOfType(drawPartType); if (!selParts.empty()) { haveView = true; } } else { auto drawViewType(TechDraw::DrawView::getClassTypeId()); auto selParts = cmd->getDocument()->getObjectsOfType(drawViewType); if (!selParts.empty()) { haveView = true; } } } return haveView; } void DrawGuiUtil::dumpRectF(const char* text, const QRectF& r) { Base::Console().message("DUMP - dumpRectF - %s\n", text); double left = r.left(); double right = r.right(); double top = r.top(); double bottom = r.bottom(); Base::Console().message("Extents: L: %.3f, R: %.3f, T: %.3f, B: %.3f\n", left, right, top, bottom); Base::Console().message("Size: W: %.3f H: %.3f\n", r.width(), r.height()); Base::Console().message("Centre: (%.3f, %.3f)\n", r.center().x(), r.center().y()); } void DrawGuiUtil::dumpPointF(const char* text, const QPointF& p) { Base::Console().message("DUMP - dumpPointF - %s\n", text); Base::Console().message("Point: (%.3f, %.3f)\n", p.x(), p.y()); } std::pair DrawGuiUtil::get3DDirAndRot() { std::pair result; Base::Vector3d viewDir(0.0, -1.0, 0.0); // default to front Base::Vector3d viewUp(0.0, 0.0, 1.0); // default to top Base::Vector3d viewRight(1.0, 0.0, 0.0); // default to right std::list mdis = Gui::Application::Instance->activeDocument()->getMDIViews(); Gui::View3DInventor* view; Gui::View3DInventorViewer* viewer = nullptr; for (auto& m : mdis) { // find the 3D viewer view = dynamic_cast(m); if (view) { viewer = view->getViewer(); break; } } if (!viewer) { return std::make_pair(viewDir, viewRight); } // Coin is giving us a values like 0.000000134439 instead of 0.000000000000. // This small difference caused circles to be projected as ellipses among other // problems. // Since SbVec3f is single precision floating point, it is only good to 6-9 // significant decimal digits, and the rest of TechDraw works with doubles // that are good to 15-18 significant decimal digits. // But. When a float is promoted to double the value is supposed to be unchanged! // So where do the garbage digits come from??? // In any case, if we restrict directions to 6 digits, we avoid the problem. int digits(6); SbVec3f dvec = viewer->getViewDirection(); double dvecX = roundToDigits(dvec[0], digits); double dvecY = roundToDigits(dvec[1], digits); double dvecZ = roundToDigits(dvec[2], digits); viewDir = Base::Vector3d(dvecX, dvecY, dvecZ); viewDir = viewDir * (-1.0); // Inventor dir is opposite TD projection dir SbVec3f upvec = viewer->getUpDirection(); double upvecX = roundToDigits(upvec[0], digits); double upvecY = roundToDigits(upvec[1], digits); double upvecZ = roundToDigits(upvec[2], digits); viewUp = Base::Vector3d(upvecX, upvecY, upvecZ); Base::Vector3d right = viewUp.Cross(viewDir); result = std::make_pair(viewDir, right); return result; } std::pair DrawGuiUtil::getProjDirFromFace(App::DocumentObject* obj, std::string faceName) { std::pair d3Dirs = get3DDirAndRot(); std::pair dirs; dirs.first = Base::Vector3d(0.0, 0.0, 1.0); // set a default dirs.second = Base::Vector3d(1.0, 0.0, 0.0); Base::Vector3d projDir, rotVec; projDir = d3Dirs.first; rotVec = d3Dirs.second; auto ts = Part::Feature::getShape(obj, Part::ShapeOption::NeedSubElement | Part::ShapeOption::ResolveLink | Part::ShapeOption::Transform, faceName.c_str()); if (ts.IsNull() || ts.ShapeType() != TopAbs_FACE) { Base::Console().warning("getProjDirFromFace(%s) is not a Face\n", faceName.c_str()); return dirs; } const TopoDS_Face& face = TopoDS::Face(ts); TopAbs_Orientation orient = face.Orientation(); BRepAdaptor_Surface adapt(face); double u1 = adapt.FirstUParameter(); double u2 = adapt.LastUParameter(); double v1 = adapt.FirstVParameter(); double v2 = adapt.LastVParameter(); double uMid = (u1 + u2) / 2.0; double vMid = (v1 + v2) / 2.0; BRepLProp_SLProps props(adapt, uMid, vMid, 2, Precision::Confusion()); if (props.IsNormalDefined()) { gp_Dir vec = props.Normal(); projDir = Base::Vector3d(vec.X(), vec.Y(), vec.Z()); if (orient != TopAbs_FORWARD) { projDir = projDir * (-1.0); } } return std::make_pair(projDir, rotVec); } // converts original value to one with only digits significant figures double DrawGuiUtil::roundToDigits(double original, int digits) { double factor = std::pow(10.0, digits); double temp = original * factor; double rounded = std::round(temp); temp = rounded / factor; return temp; } // Returns true if the item or any of its descendants is selected bool DrawGuiUtil::isSelectedInTree(QGraphicsItem* item) { if (item) { if (item->isSelected()) { return true; } for (QGraphicsItem* child : item->childItems()) { if (isSelectedInTree(child)) { return true; } } } return false; } // Selects or deselects the item and all its descendants void DrawGuiUtil::setSelectedTree(QGraphicsItem* item, bool selected) { if (item) { item->setSelected(selected); for (QGraphicsItem* child : item->childItems()) { setSelectedTree(child, selected); } } } //! convert point from scene coords to mm and conventional Y axis (page coords). Base::Vector3d DrawGuiUtil::fromSceneCoords(const Base::Vector3d& sceneCoord, bool invert) { Base::Vector3d result; if (invert) { result = Rez::appX(DU::invertY(sceneCoord)); } else { result = Rez::appX(sceneCoord); } return Rez::appX(DU::invertY(sceneCoord)); } //! convert point from printed page coords to scene units (Rez(mm) and inverted Y axis (scene coords) Base::Vector3d DrawGuiUtil::toSceneCoords(const Base::Vector3d& pageCoord, bool invert) { Base::Vector3d result; if (invert) { result = Rez::guiX(DU::invertY(pageCoord)); } else { result = Rez::guiX(pageCoord); } return result; } //! convert unscaled, unrotated point to scaled, rotated view coordinates Base::Vector3d DrawGuiUtil::toGuiPoint(DrawView* obj, const Base::Vector3d& toConvert) { Base::Vector3d result{toConvert}; auto rotDegrees = obj->Rotation.getValue(); if (rotDegrees != 0.0) { result.RotateZ(Base::toRadians(rotDegrees)); } result *= obj->getScale(); result = DU::invertY(result); result = Rez::guiX(result); return result; } //! true if targetObj is in the selection list bool DrawGuiUtil::findObjectInSelection(const std::vector& selection, const App::DocumentObject& targetObject) { for (auto& selObj : selection) { if (&targetObject == selObj.getObject()) { return true; } } return false; } std::vector DrawGuiUtil::getSubsForSelectedObject(const std::vector& selection, App::DocumentObject* selectedObj) { for (auto& selObj : selection) { if (selectedObj == selObj.getObject()) { return selObj.getSubNames(); } } return {}; } bool DrawGuiUtil::isStyleSheetDark(std::string curStyleSheet) { if (curStyleSheet.find("dark") != std::string::npos || curStyleSheet.find("Dark") != std::string::npos) { return true; } return false; } QIcon DrawGuiUtil::maskBlackPixels(QIcon itemIcon, QSize iconSize, QColor textColor) { QPixmap originalPix = itemIcon.pixmap(iconSize, QIcon::Mode::Normal, QIcon::State::On); QPixmap filler(iconSize); filler.fill(QColor(textColor)); filler.setMask(originalPix.createMaskFromColor(Qt::black, Qt::MaskOutColor)); return filler; } void DrawGuiUtil::rotateToAlign(const QGIEdge* edge, const Base::Vector2d& direction) { QGIViewPart* view = static_cast(edge->parentItem()); DrawViewPart* dvp = static_cast(view->getViewObject()); BaseGeomPtr bg = dvp->getEdgeGeometry().at(edge->getProjIndex()); std::vector endPoints = bg->findEndPoints(); Base::Vector3d oldDirection3d = endPoints.at(0) - endPoints.at(1); Base::Vector2d oldDirection2d(oldDirection3d.x, oldDirection3d.y); rotateToAlign(dvp, oldDirection2d, direction); } //! The view of p1 and p2 will be rotated to make p1 and p2 aligned with direction (for instance horizontalle aligned) void DrawGuiUtil::rotateToAlign(const QGIVertex* p1, const QGIVertex* p2, const Base::Vector2d& direction) { QGIViewPart* view = static_cast(p1->parentItem()); if(view != static_cast(p2->parentItem())) { Base::Console().error("Vertexes have to be from the same view!"); } Base::Vector2d oldDirection = p2->vector2dBetweenPoints(p1); DrawViewPart* dvp = static_cast(view->getViewObject()); rotateToAlign(dvp, oldDirection, direction); } void DrawGuiUtil::rotateToAlign(DrawViewPart* view, const Base::Vector2d& oldDirection, const Base::Vector2d& newDirection) { // If pointing counterclockwise, we need to rotate clockwise // If pointing clockwise, we need to rotate counter clockwise int cw = 1; if(newDirection.Angle() > oldDirection.Angle()) { cw = -1; } double toRotate = newDirection.GetAngle(oldDirection); // Radians to degrees toRotate = Base::toDegrees(toRotate); // Rotate least amount possible if(toRotate > 90) { // Instead of rotating 145 degrees to match direction // we only rotate -35 degrees toRotate = toRotate - 180; } else if(toRotate < -90) { toRotate = toRotate + 180; } double oldRotation = view->Rotation.getValue(); view->Rotation.setValue(oldRotation + toRotate * cw); }