From cea3370a248360e71c489356c574b602537c78fc Mon Sep 17 00:00:00 2001 From: wandererfan Date: Wed, 10 Apr 2024 17:28:32 -0400 Subject: [PATCH] [TD]fix regression of svg hatch during export - last worked in v0.20 - svg hatch tile field is not cropped on export to svg. svg tiles are replaced by pixmap tiles for export. --- src/Mod/TechDraw/Gui/QGIFace.cpp | 235 ++++++++++++++++++++++--------- src/Mod/TechDraw/Gui/QGIFace.h | 85 ++++++----- src/Mod/TechDraw/Gui/QGSPage.cpp | 2 + src/Mod/TechDraw/Gui/QGSPage.h | 3 + 4 files changed, 219 insertions(+), 106 deletions(-) diff --git a/src/Mod/TechDraw/Gui/QGIFace.cpp b/src/Mod/TechDraw/Gui/QGIFace.cpp index 5bf90fbdeb..dcc6850834 100644 --- a/src/Mod/TechDraw/Gui/QGIFace.cpp +++ b/src/Mod/TechDraw/Gui/QGIFace.cpp @@ -43,21 +43,23 @@ #include #include "QGCustomRect.h" #include "QGCustomSvg.h" +#include "QGCustomImage.h" #include "QGICMark.h" #include "QGIPrimPath.h" +#include "QGSPage.h" #include "Rez.h" #include "ZVALUE.h" using namespace TechDrawGui; using namespace TechDraw; +using DU = DrawUtil; + QGIFace::QGIFace(int index) : - projIndex(index), m_hideSvgTiles(false), + projIndex(index), m_hatchRotation(0.0) { - m_segCount = 0; -// setFillMode(NoFill); isHatched(false); setFlag(QGraphicsItem::ItemClipsChildrenToShape, true); @@ -68,26 +70,27 @@ QGIFace::QGIFace(int index) : m_pen.setStyle(m_styleCurrent); setLineWeight(0.0); //0 = cosmetic - setPrettyNormal(); m_texture = QPixmap(); //empty texture m_svgHatchArea = new QGCustomRect(); m_svgHatchArea->setParentItem(this); - - m_svgCol = SVGCOLDEFAULT; - m_fillScale = 1.0; + m_imageSvgHatchArea = new QGCustomImage(); + m_imageSvgHatchArea->setParentItem(this); getParameters(); // set up style & colour defaults - m_colDefFill = App::Color(static_cast(Preferences::getPreferenceGroup("Colors")->GetUnsigned("FaceColor", 0xFFFFFF))) + m_colDefFill = App::Color(static_cast(Preferences::getPreferenceGroup("Colors")->GetUnsigned("FaceColor", COLWHITE))) .asValue(); - m_colDefFill.setAlpha(Preferences::getPreferenceGroup("Colors")->GetBool("ClearFace", false) ? 0 : 255); + m_colDefFill.setAlpha(Preferences::getPreferenceGroup("Colors")->GetBool("ClearFace", false) ? ALPHALOW : ALPHAHIGH); m_fillDef = Qt::SolidPattern; m_fillSelect = Qt::SolidPattern; - setFillMode(m_colDefFill.alpha() ? PlainFill : NoFill); + setFillMode(NoFill); + if (m_colDefFill.alpha() > 0) { + setFillMode(PlainFill); + } setFill(m_colDefFill, m_fillDef); m_sharedRender = new QSvgRenderer(); @@ -106,6 +109,9 @@ void QGIFace::draw() // Base::Console().Message("QGIF::draw - pen style: %d\n", m_pen.style()); setPath(m_outline); //Face boundary + m_svgHatchArea->hide(); + m_imageSvgHatchArea->hide(); + if (isHatched()) { if (m_mode == GeomHatchFill) { //GeomHatch does not appear in pdf if clipping is set to true @@ -118,29 +124,28 @@ void QGIFace::draw() lineSetToFillItems(ls); } } - m_svgHatchArea->hide(); } else if (m_mode == SvgFill) { m_brush.setTexture(QPixmap()); m_fillNormal = m_fillDef; m_fillStyleCurrent = m_fillNormal; + setFlag(QGraphicsItem::ItemClipsChildrenToShape,true); loadSvgHatch(m_fileSpec); - //SVG tiles need to be clipped - setFlag(QGraphicsItem::ItemClipsChildrenToShape, true); - buildSvgHatch(); - m_svgHatchArea->show(); + if (exporting()) { + buildPixHatch(); + m_imageSvgHatchArea->show(); + } else { + buildSvgHatch(); + m_svgHatchArea->show(); + } } else if (m_mode == BitmapFill) { m_fillStyleCurrent = Qt::TexturePattern; m_texture = textureFromBitmap(m_fileSpec); m_brush.setTexture(m_texture); - m_svgHatchArea->hide(); } else if (m_mode == PlainFill) { setFill(m_colNormalFill, m_fillNormal); - m_svgHatchArea->hide(); } - } else { - // face is not hatched - m_svgHatchArea->hide(); } + show(); } @@ -174,9 +179,9 @@ void QGIFace::setPrettySel() { } /// show or hide the edges of this face. Usually just for debugging -void QGIFace::setDrawEdges(bool b) { +void QGIFace::setDrawEdges(bool state) { // Base::Console().Message("QGIF::setDrawEdges(%d)\n", b); - if (b) { + if (state) { setStyle(Qt::DashLine); } else { setStyle(Qt::NoPen); //don't draw face lines, just fill @@ -192,12 +197,12 @@ void QGIFace::setHatchFile(std::string fileSpec) void QGIFace::loadSvgHatch(std::string fileSpec) { QString qfs(QString::fromUtf8(fileSpec.data(), fileSpec.size())); - QFile f(qfs); - if (!f.open(QFile::ReadOnly | QFile::Text)) { + QFile file(qfs); + if (!file.open(QFile::ReadOnly | QFile::Text)) { Base::Console().Error("QGIFace could not read %s\n", fileSpec.c_str()); return; } - m_svgXML = f.readAll(); + m_svgXML = file.readAll(); // search in the file for the "stroke" specification in order to find out what declaration style is used // this is necessary to apply a color set by the user to the SVG @@ -211,9 +216,9 @@ void QGIFace::loadSvgHatch(std::string fileSpec) } } -void QGIFace::setFillMode(QGIFace::fillMode m) +void QGIFace::setFillMode(QGIFace::fillMode mode) { - m_mode = m; + m_mode = mode; if ((m_mode == NoFill) || (m_mode == PlainFill)) { isHatched(false); @@ -265,13 +270,13 @@ QPen QGIFace::setGeomPen() double QGIFace::getXForm() { //try to keep the pattern the same when View scales - auto s = scene(); - if (s) { - auto vs = s->views(); //ptrs to views - if (!vs.empty()) { - auto v = vs.at(0); - auto i = v->transform().inverted(); - return i.m11(); + auto ourScene = scene(); + if (ourScene) { + auto viewsAll = ourScene->views(); //ptrs to views + if (!viewsAll.empty()) { + auto view = viewsAll.at(0); + auto iView = view->transform().inverted(); + return iView.m11(); } } return 1.0; @@ -280,15 +285,15 @@ double QGIFace::getXForm() /// remove the children that make up a PAT fill void QGIFace::clearFillItems() { - for (auto& f: m_fillItems) { - f->setParentItem(nullptr); - this->scene()->removeItem(f); - delete f; + for (auto& fill: m_fillItems) { + fill->setParentItem(nullptr); + this->scene()->removeItem(fill); + delete fill; } } /// debugging tool draws a mark at a position on this face -void QGIFace::makeMark(double x, double y) +void QGIFace::makeMark(double x, double y) // NOLINT readability-identifier-length { QGICMark* cmItem = new QGICMark(-1); cmItem->setParentItem(this); @@ -315,9 +320,9 @@ void QGIFace::buildSvgHatch() double overlayHeight = tilesHigh * hTile; m_svgHatchArea->setRect(0., 0., overlayWidth,-overlayHeight); m_svgHatchArea->centerAt(faceCenter); - QByteArray before, after; - before = QString::fromStdString(SVGCOLPREFIX + SVGCOLDEFAULT).toUtf8(); - after = QString::fromStdString(SVGCOLPREFIX + m_svgCol).toUtf8(); + + QByteArray before = QString::fromStdString(SVGCOLPREFIX + SVGCOLDEFAULT).toUtf8(); + QByteArray after = QString::fromStdString(SVGCOLPREFIX + m_svgCol).toUtf8(); QByteArray colorXML = m_svgXML.replace(before, after); if (!m_sharedRender->load(colorXML)) { Base::Console().Message("QGIF::buildSvgHatch - failed to load svg string\n"); @@ -352,56 +357,138 @@ void QGIFace::clearSvg() hideSvg(true); } -//this isn't used currently -QPixmap QGIFace::textureFromSvg(std::string fileSpec) +//! similar to svg hatch, but using pixmaps. we do this because QGraphicsSvgItems are not clipped +//! when we export the scene to svg, but pixmaps are clipped. +void QGIFace::buildPixHatch() { - QString qs(QString::fromStdString(fileSpec)); - QFileInfo ffi(qs); - if (!ffi.isReadable()) { - return QPixmap(); + double wTile = SVGSIZEW * m_fillScale; + double hTile = SVGSIZEH * m_fillScale; + double faceWidth = m_outline.boundingRect().width(); + double faceHeight = m_outline.boundingRect().height(); + double faceOverlaySize = Preferences::svgHatchFactor() * std::max(faceWidth, faceHeight); + QPointF faceCenter = m_outline.boundingRect().center(); + double tilesWide = ceil(faceOverlaySize / wTile); + double tilesHigh = ceil(faceOverlaySize / hTile); + + double overlayWidth = tilesWide * wTile; + double overlayHeight = tilesHigh * hTile; + + // handle color by brute force find & replace + QByteArray before = QString::fromStdString(SVGCOLPREFIX + SVGCOLDEFAULT).toUtf8(); + QByteArray after = QString::fromStdString(SVGCOLPREFIX + m_svgCol).toUtf8(); + QByteArray colorXML = m_svgXML.replace(before,after); + + + // TODO: there is a lot of switching back and forth between svg, QPixmap and QImage here that I + // don't really understand. + // render svg tile onto a QImage + if (!m_sharedRender->load(colorXML)) { + Base::Console().Message("QGIF::buildSvgHatch - failed to load svg string\n"); + return; } - QSvgRenderer renderer(qs); - QPixmap pixMap(renderer.defaultSize()); - pixMap.fill(Qt::white); //try Qt::transparent? - QPainter painter(&pixMap); - renderer.render(&painter); //svg texture -> bitmap - return pixMap.scaled(m_fillScale, m_fillScale); + + QImage svgImage(SVGSIZEW, SVGSIZEH, QImage::Format_ARGB32); + svgImage.fill(Qt::transparent); + QPainter painter(&svgImage); + if (svgImage.isNull()) { + Base::Console().Error("QGIF::buildPixHatch - svgImage is null\n"); + return; + } + + m_sharedRender->render(&painter); + + // convert the QImage into a QPixmap + QPixmap tilePixmap(SVGSIZEW, SVGSIZEH); + tilePixmap = QPixmap::fromImage(svgImage); + tilePixmap = tilePixmap.scaled(wTile, hTile); + if (tilePixmap.isNull()) { + Base::Console().Error("QGIF::buildPixHatch - tilePixmap is null\n"); + return; + } + + // layout a field of bitmap tiles big enough to cover this face onto a Qimage + QImage tileField(overlayWidth, overlayHeight, QImage::Format_ARGB32); + QPointF fieldCenter(overlayWidth / 2.0, overlayHeight / 2.0); + + tileField.fill(Qt::transparent); + QPainter painter2(&tileField); + QPainter::RenderHints hints = painter2.renderHints(); + hints = hints & QPainter::Antialiasing; + painter2.setRenderHints(hints); + QPainterPath clipper = path(); + QPointF offset = (fieldCenter - faceCenter); + clipper.translate(offset); + painter2.setClipPath(clipper); + + long int tileCount = 0; + for (int iw = 0; iw < int(tilesWide); iw++) { + for (int ih = 0; ih < int(tilesHigh); ih++) { + painter2.drawPixmap(QRectF(iw * wTile, ih * hTile, wTile, hTile), //target rect + tilePixmap, + QRectF(0, 0, wTile, hTile)); //source rect + tileCount++; + if (tileCount > m_maxTile) { + Base::Console().Warning("Pixmap tile count exceeded: %ld\n",tileCount); + break; + } + } + if (tileCount > m_maxTile) { + break; + } + } + + // turn the QImage field into a pixmap + QPixmap fieldPixmap(overlayWidth, overlayHeight); + fieldPixmap = QPixmap::fromImage(tileField); + + // TODO: figure out how to rotate the pixmap without it looking terrible - far worse than the unrotated pixmap. svg hatch exported to svg will not be shown rotated + // QTransform xFormPixmap; + // xFormPixmap.rotate(m_hatchRotation); + // xFormPixmap.translate(getHatchOffset().x, getHatchOffset().y); + // m_imageSvgHatchArea->load(fieldPixmap.transformed(xFormPixmap)); + + QPixmap nothing; + m_imageSvgHatchArea->setPixmap(nothing); + m_imageSvgHatchArea->load(fieldPixmap); + m_imageSvgHatchArea->centerAt(faceCenter); + } -void QGIFace::setHatchColor(App::Color c) + +void QGIFace::setHatchColor(App::Color color) { - m_svgCol = c.asHexString(); - m_geomColor = c.asValue(); + m_svgCol = color.asHexString(); + m_geomColor = color.asValue(); } -void QGIFace::setHatchScale(double s) +void QGIFace::setHatchScale(double scale) { - m_fillScale = s; + m_fillScale = scale; } /// turn svg tiles on or off. QtSvg does not handle clipping, /// so we must be able to turn the hatching on/off when exporting a face with an /// svg hatch. Otherwise the full tile pattern is shown in the export. /// NOTE: there appears to have been a change in Qt that it now clips svg items -void QGIFace::hideSvg(bool b) +void QGIFace::hideSvg(bool state) { - m_hideSvgTiles = b; + m_hideSvgTiles = state; } /// create a QPixmap from a bitmap file. The QPixmap will be used as a QBrush /// texture. -QPixmap QGIFace::textureFromBitmap(std::string fileSpec) +QPixmap QGIFace::textureFromBitmap(std::string fileSpec) const { QPixmap pix; QString qfs(QString::fromUtf8(fileSpec.data(), fileSpec.size())); - QFile f(qfs); - if (!f.open(QFile::ReadOnly)) { + QFile file(qfs); + if (!file.open(QFile::ReadOnly)) { Base::Console().Error("QGIFace could not read %s\n", fileSpec.c_str()); return pix; } - QByteArray bytes = f.readAll(); + QByteArray bytes = file.readAll(); pix.loadFromData(bytes); if (m_hatchRotation != 0.0) { QTransform rotator; @@ -411,14 +498,14 @@ QPixmap QGIFace::textureFromBitmap(std::string fileSpec) return pix; } -void QGIFace::setLineWeight(double w) { - m_geomWeight = w; +void QGIFace::setLineWeight(double weight) { + m_geomWeight = weight; } void QGIFace::getParameters() { - m_maxSeg = Preferences::getPreferenceGroup("PAT")->GetInt("MaxSeg", 10000l); - m_maxTile = Preferences::getPreferenceGroup("Decorations")->GetInt("MaxSVGTile", 10000l); + m_maxSeg = Preferences::getPreferenceGroup("PAT")->GetInt("MaxSeg", MAXSEGMENT); + m_maxTile = Preferences::getPreferenceGroup("Decorations")->GetInt("MaxSVGTile", MAXTILES); } QRectF QGIFace::boundingRect() const @@ -430,3 +517,13 @@ QPainterPath QGIFace::shape() const { return path(); } + +bool QGIFace::exporting() const +{ + auto tdScene = dynamic_cast(scene()); + if (!tdScene) { + return false; + } + return tdScene->getExportingSvg(); +} + diff --git a/src/Mod/TechDraw/Gui/QGIFace.h b/src/Mod/TechDraw/Gui/QGIFace.h index 18323799b5..b0c8c3d317 100644 --- a/src/Mod/TechDraw/Gui/QGIFace.h +++ b/src/Mod/TechDraw/Gui/QGIFace.h @@ -43,9 +43,14 @@ class QGCustomSvg; class QGCustomRect; class QGCustomImage; - const double SVGSIZEW = 64.0; //width and height of standard FC SVG pattern - const double SVGSIZEH = 64.0; - const std::string SVGCOLDEFAULT = "#000000"; + constexpr int SVGSIZEW{64}; //width and height of standard FC SVG pattern + constexpr int SVGSIZEH{64}; + constexpr uint32_t COLWHITE{0xfffff}; // white + constexpr int ALPHALOW{0}; + constexpr int ALPHAHIGH{255}; + constexpr long int MAXSEGMENT{10000L}; + constexpr long int MAXTILES{10000L}; + const std::string SVGCOLDEFAULT = "#000000"; // black class QGIFace : public QGIPrimPath { @@ -68,35 +73,37 @@ public: PlainFill }; std::string SVGCOLPREFIX = ""; // will be determined on runtime - int getProjIndex() const { return projIndex; } void draw(); void setPrettyNormal() override; void setPrettyPre() override; void setPrettySel() override; - void setDrawEdges(bool b); + void setDrawEdges(bool state); virtual void setOutline(const QPainterPath& path); //shared fill parms - void isHatched(bool s) {m_isHatched = s; } + void isHatched(bool state) {m_isHatched = state; } bool isHatched() {return m_isHatched;} - void setFillMode(fillMode m); + void setFillMode(fillMode mode); //general hatch parms & methods - void setHatchColor(App::Color c); - void setHatchScale(double s); + void setHatchColor(App::Color color); + void setHatchScale(double scale); //svg fill parms & methods void setHatchFile(std::string fileSpec); void loadSvgHatch(std::string fileSpec); void buildSvgHatch(); - void hideSvg(bool b); + void hideSvg(bool state); void clearSvg(); + //tiled pixmap fill from svg + void buildPixHatch(); + //PAT fill parms & methods - void setGeomHatchWeight(double w) { m_geomWeight = w; } - void setLineWeight(double w); + void setGeomHatchWeight(double weight) { m_geomWeight = weight; } + void setLineWeight(double weight); void clearLineSets(); void addLineSet(TechDraw::LineSet& ls); @@ -104,13 +111,10 @@ public: void lineSetToFillItems(TechDraw::LineSet& ls); QGraphicsPathItem* geomToLine(TechDraw::BaseGeomPtr base, TechDraw::LineSet& ls); -// QGraphicsPathItem* geomToOffsetLine(TechDraw::BaseGeomPtr base, double offset, const TechDraw::LineSet& ls); QGraphicsPathItem* geomToStubbyLine(TechDraw::BaseGeomPtr base, double offset, TechDraw::LineSet& ls); QGraphicsPathItem* lineFromPoints(Base::Vector3d start, Base::Vector3d end, TechDraw::DashSpec ds); - //bitmap texture fill parms method - QPixmap textureFromBitmap(std::string fileSpec); - QPixmap textureFromSvg(std::string fillSpec); + QPixmap textureFromBitmap(std::string fileSpec) const; //Qt uses clockwise degrees void setHatchRotation(double degrees) { m_hatchRotation = -degrees; } @@ -120,39 +124,46 @@ public: Base::Vector3d getHatchOffset() { return m_hatchOffset; } protected: - void makeMark(double x, double y); + void makeMark(double x, double y); // NOLINT readability-identifier-length double getXForm(); void getParameters(); std::vector offsetDash(const std::vector dv, const double offset); QPainterPath dashedPPath(const std::vector dv, const Base::Vector3d start, const Base::Vector3d end); double dashRemain(const std::vector dv, const double offset); - double calcOffset(TechDraw::BaseGeomPtr g, TechDraw::LineSet ls); - int projIndex; //index of face in Projection. -1 for SectionFace. - QGCustomRect* m_svgHatchArea; + double calcOffset(TechDraw::BaseGeomPtr geom, TechDraw::LineSet ls); - QByteArray m_svgXML; - std::string m_svgCol; - std::string m_fileSpec; //for svg & bitmaps - - double m_fillScale; - bool m_isHatched; - QGIFace::fillMode m_mode; QPen setGeomPen(); - std::vector decodeDashSpec(TechDraw::DashSpec d); - std::vector m_fillItems; - std::vector m_lineSets; - std::vector m_dashSpecs; - long int m_segCount; - long int m_maxSeg; - long int m_maxTile; + std::vector decodeDashSpec(TechDraw::DashSpec dash); - bool m_hideSvgTiles; bool multiselectEligible() override { return true; } + bool exporting() const; + + private: + std::vector m_fillItems; + std::vector m_lineSets; + std::vector m_dashSpecs; + long int m_segCount{0}; + long int m_maxSeg{0}; + long int m_maxTile{0}; + + bool m_hideSvgTiles{false}; + int projIndex; //index of face in Projection. -1 for SectionFace. + + QGCustomRect* m_svgHatchArea; + QGCustomImage* m_imageSvgHatchArea; + + QByteArray m_svgXML; + std::string m_svgCol{SVGCOLDEFAULT}; + std::string m_fileSpec; //for svg & bitmaps + + double m_fillScale{1.0}; + bool m_isHatched{false}; + QGIFace::fillMode m_mode; QPixmap m_texture; // QPainterPath m_outline; // @@ -160,11 +171,11 @@ private: QPainterPath m_geomhatch; //crosshatch fill lines QColor m_geomColor; //color for crosshatch lines - double m_geomWeight; //lineweight for crosshatch lines + double m_geomWeight{0.5}; //lineweight for crosshatch lines QColor m_defFaceColor; - double m_hatchRotation; + double m_hatchRotation{0.0}; Base::Vector3d m_hatchOffset; QSvgRenderer *m_sharedRender; diff --git a/src/Mod/TechDraw/Gui/QGSPage.cpp b/src/Mod/TechDraw/Gui/QGSPage.cpp index c7c32bc34d..176b9ca67b 100644 --- a/src/Mod/TechDraw/Gui/QGSPage.cpp +++ b/src/Mod/TechDraw/Gui/QGSPage.cpp @@ -930,8 +930,10 @@ void QGSPage::redraw1View(TechDraw::DrawView* dView) // RichTextAnno needs to know when it is rendering an Svg as the font size // is handled differently in Svg compared to the screen or Pdf. +// Also true of QGraphicsSvgItems. void QGSPage::setExportingSvg(bool enable) { + m_exportingSvg = enable; QList sceneItems = items(); for (auto& qgi : sceneItems) { QGIRichAnno* qgiRTA = dynamic_cast(qgi); diff --git a/src/Mod/TechDraw/Gui/QGSPage.h b/src/Mod/TechDraw/Gui/QGSPage.h index f15280ba2a..7d46879bb1 100644 --- a/src/Mod/TechDraw/Gui/QGSPage.h +++ b/src/Mod/TechDraw/Gui/QGSPage.h @@ -129,6 +129,7 @@ public: TechDraw::DrawPage* getDrawPage(); void setExportingSvg(bool enable); + bool getExportingSvg() { return m_exportingSvg; } virtual void refreshViews(); /// Renders the page to SVG with filename. @@ -145,6 +146,8 @@ protected: private: QGITemplate* pageTemplate; ViewProviderPage* m_vpPage; + + bool m_exportingSvg{false}; }; }// namespace TechDrawGui