/*************************************************************************** * Copyright (c) 2013 Luke Parry * * * * 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 "PreCompiled.h" #ifndef _PreComp_ # include # include # include # include # include # include # include # include #endif #include #include #include #include #include "PreferencesGui.h" #include "QGIFace.h" #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) : m_hideSvgTiles(false), projIndex(index), m_hatchRotation(0.0) { isHatched(false); setFlag(QGraphicsItem::ItemClipsChildrenToShape, true); setStyle(Qt::NoPen); //don't draw face lines, just fill for debugging //setStyle(Qt::DashLine); m_geomColor = PreferencesGui::getAccessibleQColor(QColor(Qt::black)); m_styleCurrent = Qt::NoPen; m_pen.setStyle(m_styleCurrent); setLineWeight(0.0); //0 = cosmetic m_texture = QPixmap(); //empty texture m_svgHatchArea = new QGCustomRect(); m_svgHatchArea->setParentItem(this); 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", COLWHITE))) .asValue(); m_colDefFill.setAlpha(Preferences::getPreferenceGroup("Colors")->GetBool("ClearFace", false) ? ALPHALOW : ALPHAHIGH); m_fillDef = Qt::SolidPattern; m_fillSelect = Qt::SolidPattern; setFillMode(NoFill); if (m_colDefFill.alpha() > 0) { setFillMode(PlainFill); } setFill(m_colDefFill, m_fillDef); m_sharedRender = new QSvgRenderer(); m_patMaker = new PATPathMaker(this, 1.0, 1.0); } QGIFace::~QGIFace() { delete m_sharedRender; delete m_patMaker; } /// redraw this face 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 setFlag(QGraphicsItem::ItemClipsChildrenToShape, false); if (!m_lineSets.empty()) { m_brush.setTexture(QPixmap()); m_fillStyleCurrent = m_fillDef; m_fillNormal = m_fillStyleCurrent; for (auto& ls: m_lineSets) { lineSetToFillItems(ls); } } } else if (m_mode == SvgFill) { m_brush.setTexture(QPixmap()); m_fillNormal = m_fillDef; m_fillStyleCurrent = m_fillNormal; setFlag(QGraphicsItem::ItemClipsChildrenToShape,true); loadSvgHatch(m_fileSpec); 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); } else if (m_mode == PlainFill) { setFill(m_colNormalFill, m_fillNormal); } } show(); } /// show the face style & colour in normal configuration void QGIFace::setPrettyNormal() { // Base::Console().Message("QGIF::setPrettyNormal() - hatched: %d\n", isHatched()); if (isHatched() && (m_mode == BitmapFill) ) { //hatch with bitmap fill m_fillStyleCurrent = Qt::TexturePattern; m_brush.setTexture(m_texture); } else { m_brush.setTexture(QPixmap()); } QGIPrimPath::setPrettyNormal(); } /// show the face style & colour in pre-select configuration void QGIFace::setPrettyPre() { // Base::Console().Message("QGIF::setPrettyPre()\n"); m_fillStyleCurrent = Qt::SolidPattern; m_brush.setTexture(QPixmap()); QGIPrimPath::setPrettyPre(); } /// show the face style & colour in selected configuration void QGIFace::setPrettySel() { // Base::Console().Message("QGIF::setPrettySel()\n"); m_fillStyleCurrent = Qt::SolidPattern; m_brush.setTexture(QPixmap()); QGIPrimPath::setPrettySel(); } /// show or hide the edges of this face. Usually just for debugging void QGIFace::setDrawEdges(bool state) { // Base::Console().Message("QGIF::setDrawEdges(%d)\n", b); if (state) { setStyle(Qt::DashLine); } else { setStyle(Qt::NoPen); //don't draw face lines, just fill } } void QGIFace::setHatchFile(std::string fileSpec) { m_fileSpec = fileSpec; } /// get the .svg file to use for hatching this face void QGIFace::loadSvgHatch(std::string fileSpec) { QString qfs(QString::fromUtf8(fileSpec.data(), fileSpec.size())); 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 = 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 QByteArray pattern("stroke:"); QByteArrayMatcher matcher(pattern); int pos = 0; if (matcher.indexIn(m_svgXML, pos) != -1) { SVGCOLPREFIX = "stroke:"; // declaration part of a style="" statement } else { SVGCOLPREFIX = "stroke=\""; // declaration of its own } } void QGIFace::setFillMode(QGIFace::fillMode mode) { m_mode = mode; if ((m_mode == NoFill) || (m_mode == PlainFill)) { isHatched(false); } else { isHatched(true); } } /// update the outline of this face void QGIFace::setOutline(const QPainterPath & path) { m_outline = path; } /// remove the PAT hatch lines void QGIFace::clearLineSets() { m_dashSpecs.clear(); clearFillItems(); } /// add PAT hatch line set void QGIFace::addLineSet(LineSet& ls) { m_lineSets.push_back(ls); } /// convert the PAT line set to QGraphicsPathItems void QGIFace::lineSetToFillItems(LineSet& ls) { m_patMaker->setLineWidth(Rez::guiX(m_geomWeight)); m_patMaker->setScale(m_fillScale); m_patMaker->setPen(setGeomPen()); m_patMaker->lineSetToFillItems(ls); } QPen QGIFace::setGeomPen() { QPen result; result.setWidthF(Rez::guiX(m_geomWeight)); result.setColor(m_geomColor); result.setStyle(Qt::SolidLine); return result; } //! get zoom level (scale) from QGraphicsView // not used currently double QGIFace::getXForm() { //try to keep the pattern the same when View scales 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; } /// remove the children that make up a PAT fill void QGIFace::clearFillItems() { 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) // NOLINT readability-identifier-length { QGICMark* cmItem = new QGICMark(-1); cmItem->setParentItem(this); cmItem->setPos(x, y); cmItem->setThick(1.0); cmItem->setSize(40.0); cmItem->setZValue(ZVALUE::VERTEX); } /// make an array of svg tiles to cover this face void QGIFace::buildSvgHatch() { // Base::Console().Message("QGIF::buildSvgHatch() - offset: %s\n", DrawUtil::formatVector(getHatchOffset()).c_str()); 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; m_svgHatchArea->setRect(0., 0., overlayWidth,-overlayHeight); m_svgHatchArea->centerAt(faceCenter); 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"); return; } long int tileCount = 0; for (int iw = 0; iw < int(tilesWide); iw++) { for (int ih = 0; ih < int(tilesHigh); ih++) { QGCustomSvg* tile = new QGCustomSvg(); tile->setScale(m_fillScale); tile->setSharedRenderer(m_sharedRender); tile->setParentItem(m_svgHatchArea); tile->setPos(iw*wTile + getHatchOffset().x, -overlayWidth + ih*hTile + getHatchOffset().y); tileCount++; if (tileCount > m_maxTile) { Base::Console().Warning("SVG tile count exceeded: %ld. Change hatch scale or raise limit.\n", tileCount); break; } } if (tileCount > m_maxTile) { break; } } QPointF faceCenterToMRect = mapToItem(m_svgHatchArea, faceCenter); m_svgHatchArea->setTransformOriginPoint(faceCenterToMRect); m_svgHatchArea->setRotation(m_hatchRotation); } void QGIFace::clearSvg() { hideSvg(true); } //! 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() { 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; } 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 color) { m_svgCol = color.asHexString(); m_geomColor = color.asValue(); } void QGIFace::setHatchScale(double scale) { 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 state) { 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) const { QPixmap pix; QString qfs(QString::fromUtf8(fileSpec.data(), fileSpec.size())); QFile file(qfs); if (!file.open(QFile::ReadOnly)) { Base::Console().Error("QGIFace could not read %s\n", fileSpec.c_str()); return pix; } QByteArray bytes = file.readAll(); pix.loadFromData(bytes); if (m_hatchRotation != 0.0) { QTransform rotator; rotator.rotate(m_hatchRotation); pix = pix.transformed(rotator); } return pix; } void QGIFace::setLineWeight(double weight) { m_geomWeight = weight; } void QGIFace::getParameters() { m_maxSeg = Preferences::getPreferenceGroup("PAT")->GetInt("MaxSeg", MAXSEGMENT); m_maxTile = Preferences::getPreferenceGroup("Decorations")->GetInt("MaxSVGTile", MAXTILES); } QRectF QGIFace::boundingRect() const { return shape().controlPointRect(); } QPainterPath QGIFace::shape() const { return path(); } bool QGIFace::exporting() const { auto tdScene = dynamic_cast(scene()); if (!tdScene) { return false; } return tdScene->getExportingSvg(); }