File indexing completed on 2024-05-12 04:35:07

0001 /* This file is part of the TikZKit project.
0002  *
0003  * Copyright (C) 2013 Dominik Haumann <dhaumann@kde.org>
0004  *
0005  * This library is free software; you can redistribute it and/or modify
0006  * it under the terms of the GNU Library General Public License as published
0007  * by the Free Software Foundation, either version 2 of the License, or
0008  * (at your option) any later version.
0009  *
0010  * This library is distributed in the hope that it will be useful,
0011  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0012  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013  * GNU Library General Public License for more details.
0014  *
0015  * You should have received a copy of the GNU Library General Public License
0016  * along with this library; see the file COPYING.LIB.  If not, see
0017  * <http://www.gnu.org/licenses/>.
0018  */
0019 
0020 #include "TikzExportVisitor.h"
0021 
0022 #include "Document.h"
0023 #include "Node.h"
0024 #include "Path.h"
0025 #include "EllipsePath.h"
0026 #include "EdgePath.h"
0027 #include "Style.h"
0028 
0029 #include <QStringList>
0030 #include <QTextStream>
0031 #include <QMetaProperty>
0032 #include <QFile>
0033 #include <QDebug>
0034 #include <QHash>
0035 
0036 namespace tikz {
0037 namespace core {
0038 
0039 static QColor mixColor(const QColor & c1, const QColor & c2, qreal interp)
0040 {
0041     if (! c1.isValid() || ! c2.isValid()) {
0042         return QColor();
0043     }
0044 
0045     const auto p = interp;
0046     const auto q = 1 - interp;
0047     return QColor(qRound(p * c1.red() + q * c2.red()),
0048                   qRound(p * c1.green() + q * c2.green()),
0049                   qRound(p * c1.blue() + q * c2.blue()));
0050 }
0051 
0052 static QString colorToString(const QColor & color)
0053 {
0054     static QHash<QRgb, QString> colorMap;
0055     if (colorMap.isEmpty()) {
0056         for (int i = 10; i < 100; i += 10) {
0057             const qreal f = i / 100.0;
0058             colorMap.insert(mixColor(Qt::black, Qt::white, f).rgb(), QString("black!%1").arg(i));
0059             colorMap.insert(mixColor(Qt::cyan, Qt::white, f).rgb(), QString("cyan!%1").arg(i));
0060             colorMap.insert(mixColor(Qt::cyan, Qt::black, f).rgb(), QString("cyan!%1!black").arg(i));
0061             colorMap.insert(mixColor(Qt::red, Qt::white, f).rgb(), QString("red!%1").arg(i));
0062             colorMap.insert(mixColor(Qt::red, Qt::black, f).rgb(), QString("red!%1!black").arg(i));
0063             colorMap.insert(mixColor(Qt::magenta, Qt::white, f).rgb(), QString("magenta!%1").arg(i));
0064             colorMap.insert(mixColor(Qt::magenta, Qt::black, f).rgb(), QString("magenta!%1!black").arg(i));
0065             colorMap.insert(mixColor(Qt::green, Qt::white, f).rgb(), QString("green!%1").arg(i));
0066             colorMap.insert(mixColor(Qt::green, Qt::black, f).rgb(), QString("green!%1!black").arg(i));
0067             colorMap.insert(mixColor(Qt::yellow, Qt::white, f).rgb(), QString("yellow!%1").arg(i));
0068             colorMap.insert(mixColor(Qt::yellow, Qt::black, f).rgb(), QString("yellow!%1!black").arg(i));
0069             colorMap.insert(mixColor(Qt::blue, Qt::white, f).rgb(), QString("blue!%1").arg(i));
0070             colorMap.insert(mixColor(Qt::blue, Qt::black, f).rgb(), QString("blue!%1!black").arg(i));
0071             colorMap.insert(mixColor(QColor(255, 128, 0), Qt::white, f).rgb(), QString("orange!%1").arg(i));
0072             colorMap.insert(mixColor(QColor(255, 128, 0), Qt::black, f).rgb(), QString("orange!%1!black").arg(i));
0073         }
0074 
0075         colorMap.insert(QColor(Qt::black).rgb(), "black");
0076         colorMap.insert(QColor(128, 128, 128).rgb(), "gray");
0077         colorMap.insert(QColor(64, 64, 64).rgb(), "darkgray");
0078         colorMap.insert(QColor(191, 191, 191).rgb(), "lightgray");
0079         colorMap.insert(QColor(Qt::white).rgb(), "white");
0080         colorMap.insert(QColor(Qt::cyan).rgb(), "cyan");
0081         colorMap.insert(QColor(Qt::red).rgb(), "red");
0082         colorMap.insert(QColor(Qt::magenta).rgb(), "magenta");
0083         colorMap.insert(QColor(Qt::green).rgb(), "green");
0084         colorMap.insert(QColor(Qt::yellow).rgb(), "yellow");
0085         colorMap.insert(QColor(Qt::blue).rgb(), "blue");
0086         colorMap.insert(QColor(191, 128, 64).rgb(), "brown");
0087         colorMap.insert(QColor(191, 255, 0).rgb(), "lime");
0088         colorMap.insert(QColor(255, 191, 191).rgb(), "pink");
0089         colorMap.insert(QColor(191, 0, 64).rgb(), "purple");
0090         colorMap.insert(QColor(0, 128, 128).rgb(), "teal");
0091         colorMap.insert(QColor(128, 0, 128).rgb(), "violet");
0092         colorMap.insert(QColor(128, 128, 0).rgb(), "olive");
0093     }
0094 
0095     // try to find a smart colorname
0096     auto it = colorMap.find(color.rgb());
0097     if (it != colorMap.end()) {
0098         return it.value();
0099     }
0100 
0101     // use generic fallback color schema
0102     return QString("{rgb,255:red,%1; green,%2; blue,%3}").arg(color.red()).arg(color.green()).arg(color.blue());
0103 }
0104 
0105 static QString lineWidthToString(const tikz::Value & lw)
0106 {
0107     if (lw == Value::ultraThin()) {
0108         return "ultra thin";
0109     } else if (lw == Value::veryThin()) {
0110         return "very thin";
0111     } else if (lw == Value::thin()) {
0112         return "thin";
0113     } else if (lw == Value::semiThick()) {
0114         return "semithick";
0115     } else if (lw == Value::thick()) {
0116         return "thick";
0117     } else if (lw == Value::veryThick()) {
0118         return "very thick";
0119     } else if (lw == Value::ultraThick()) {
0120         return "ultra thick";
0121     }
0122 
0123     return "line width=" + lw.toString();
0124 }
0125 
0126 TikzExportVisitor::TikzExportVisitor()
0127     : Visitor()
0128 {
0129 }
0130 
0131 TikzExportVisitor::~TikzExportVisitor()
0132 {
0133 }
0134 
0135 QString TikzExportVisitor::tikzCode()
0136 {
0137     return m_tikzExport.tikzCode();
0138 }
0139 
0140 void TikzExportVisitor::visit(Document * doc)
0141 {
0142     //
0143     // export the global options for the tikzpicture
0144     //
0145     QStringList options = styleOptions(doc->style());
0146     options << "align=center"; // FIXME: temporary hack to make text wrap in nodes work.
0147     m_tikzExport.setDocumentOptions(options.join(", "));
0148 }
0149 
0150 void TikzExportVisitor::visit(Node * node)
0151 {
0152     QString options = nodeStyleOptions(node->style()).join(", ");
0153 
0154     QString cmd = QString("\\node[%1,draw] (%2) at %3 {%4};")
0155         .arg(options)
0156         .arg(node->uid().toString())
0157         .arg(node->pos().toString())
0158         .arg(node->text());
0159 
0160     //
0161     // finally add node to picture
0162     //
0163     TikzLine line;
0164     line.contents = cmd;
0165     m_tikzExport.addTikzLine(line);
0166 }
0167 
0168 void TikzExportVisitor::visit(Path * path)
0169 {
0170     QString options = edgeStyleOptions(path->style()).join(", ");
0171     if (!options.isEmpty()) {
0172         options = "[" + options + "]";
0173     }
0174 
0175     QString cmd;
0176 
0177     switch (path->type()) {
0178         case PathType::Line:
0179         case PathType::HVLine:
0180         case PathType::VHLine: {
0181             auto edge = static_cast<tikz::core::EdgePath*>(path);
0182             //
0183             // compute start & end
0184             //
0185             const QString startCoord = edge->startMetaPos().toString();
0186             const QString endCoord = edge->endMetaPos().toString();
0187 
0188             //
0189             // build connection string
0190             //
0191             QString to;
0192             switch (edge->type()) {
0193                 case PathType::Line: to = "--"; break;
0194                 case PathType::HVLine: to = "-|"; break;
0195                 case PathType::VHLine: to = "|-"; break;
0196                 default: Q_ASSERT(false); break;
0197             }
0198             cmd = "\\draw" + options + " " + startCoord + " " + to + " " + endCoord + ";";
0199         }
0200         case PathType::BendCurve: {
0201             break;
0202         }
0203         case PathType::InOutCurve: {
0204             break;
0205         }
0206         case PathType::BezierCurve: {
0207             break;
0208         }
0209         case PathType::Ellipse: {
0210             auto ellipsePath = static_cast<tikz::core::EllipsePath*>(path);
0211 
0212             //
0213             // compute center
0214             //
0215             const QString center = ellipsePath->metaPos().toString();
0216 
0217             //
0218             // compute radius
0219             //
0220             QString radius;
0221             if (ellipsePath->style()->radiusX() == ellipsePath->style()->radiusY()) {
0222                 radius = "radius=" + ellipsePath->style()->radiusX().toString();
0223             } else {
0224                 radius = "x radius=" + ellipsePath->style()->radiusX().toString();
0225                 radius += ", y radius=" + ellipsePath->style()->radiusY().toString();
0226             }
0227 
0228             //
0229             // export rotation
0230             //
0231             if (ellipsePath->style()->rotationSet()) {
0232                 radius += QString(", rotate=%1").arg(ellipsePath->style()->rotation());
0233             }
0234 
0235             // build path
0236             cmd = "\\draw" + options + " " + center + " circle [" + radius + "];";
0237             break;
0238         }
0239         case PathType::Rectangle: {
0240             break;
0241         }
0242         case PathType::Grid: {
0243             break;
0244         }
0245         default: break;
0246     }
0247 
0248     //
0249     // finally add path to picture
0250     //
0251     TikzLine line;
0252     line.contents = cmd;
0253     m_tikzExport.addTikzLine(line);
0254 }
0255 
0256 void TikzExportVisitor::visit(Style * style)
0257 {
0258     Q_UNUSED(style)
0259 }
0260 
0261 QStringList TikzExportVisitor::styleOptions(Style * style)
0262 {
0263     QStringList options;
0264 
0265     //
0266     // export pen style
0267     //
0268     if (style->penStyleSet()) {
0269         options << toString(style->penStyle());
0270     }
0271 
0272     //
0273     // export line width
0274     //
0275     if (style->lineWidthSet()) {
0276         options << lineWidthToString(style->lineWidth());
0277     }
0278 
0279     //
0280     // export double lines
0281     //
0282     if (style->doubleLineSet()) {
0283         options << "double" + (style->innerLineColorSet()
0284             ? QString("=" + colorToString(style->innerLineColor()))
0285             : QString());
0286 
0287         if (style->innerLineWidthSet()) {
0288             options << "double distance=" + style->innerLineWidth().toString();
0289         }
0290     }
0291 
0292     //
0293     // export draw & fill opacity
0294     //
0295     if (style->penOpacitySet() && style->fillOpacitySet()
0296         && style->penOpacity() == style->fillOpacity())
0297     {
0298         options << QString("opacity=%1").arg(style->penOpacity());
0299     } else {
0300         if (style->penOpacitySet()) {
0301             options << QString("draw opacity=%1").arg(style->penOpacity());
0302         }
0303         if (style->fillOpacitySet()) {
0304             options << QString("fill opacity=%1").arg(style->fillOpacity());
0305         }
0306     }
0307 
0308     //
0309     // export draw & fill colors
0310     //
0311     if (style->penColorSet()) {
0312         options << "draw=" + colorToString(style->penColor());
0313     }
0314     if (style->fillColorSet()) {
0315         options << "fill=" + colorToString(style->fillColor());
0316     }
0317 
0318     return options;
0319 }
0320 
0321 QStringList TikzExportVisitor::edgeStyleOptions(Style * style)
0322 {
0323     QStringList options = styleOptions(style);
0324 
0325     //
0326     // export arrow tail
0327     //
0328     QString arrowTail;
0329     if (style->arrowTailSet()) {
0330         arrowTail = toString(style->arrowTail());
0331     }
0332 
0333     QString arrowHead;
0334     if (style->arrowHeadSet()) {
0335         arrowHead = toString(style->arrowHead());
0336     }
0337 
0338     if (style->arrowTailSet() || style->arrowHeadSet()) {
0339         options << arrowTail + "-" + arrowHead;
0340     }
0341 
0342     //
0343     // export shorten properties
0344     //
0345     if (style->shortenStartSet()) {
0346         options << "shorten <=" + style->shortenStart().toString();
0347     }
0348 
0349     if (style->shortenEndSet()) {
0350         options << "shorten >=" + style->shortenEnd().toString();
0351     }
0352 
0353 // FIXME
0354     if (style->bendAngleSet()) {
0355         const qreal angle = style->bendAngle();
0356         if (angle > 0) options << QString("bend left=%1").arg(angle);
0357         if (angle < 0) options << QString("bend right=%1").arg(-angle);
0358     }
0359 
0360 #if 0
0361     //
0362     // export curve mode
0363     //
0364     if (style->curveModeSet()) {
0365         const CurveMode cm = style->curveMode();
0366         switch (cm) {
0367             case LineTo: break;
0368             case HVLineTo: break;
0369             case VHLineTo: break;
0370             case BendCurve: {
0371                 if (style->bendAngleSet()) {
0372                     const qreal angle = style->bendAngle();
0373                     if (angle > 0) options << QString("bend left=%1").arg(angle);
0374                     if (angle < 0) options << QString("bend right=%1").arg(-angle);
0375                 }
0376                 break;
0377             }
0378             case InOutCurve: {
0379                 if (style->outAngleSet()) {
0380                     options << QString("out=%1").arg(style->outAngle());
0381                 }
0382                 if (style->inAngleSet()) {
0383                     options << QString("in=%1").arg(style->inAngle());
0384                 }
0385                 break;
0386             }
0387             case BezierCurve: {
0388                 // TODO ?
0389             }
0390             default: Q_ASSERT(false); break;
0391         }
0392 
0393     }
0394 #endif
0395     //
0396     // export looseness
0397     //
0398     if (style->loosenessSet()) {
0399 //         Q_ASSERT(cm == CurveMode::BendCurve || cm == CurveMode::InOutCurve || cm == CurveMode::BezierCurve);
0400         options << QString("looseness=%1").arg(style->looseness());
0401     }
0402 
0403 #if 0
0404         if (d->curveMode == BezierCurve) {
0405             vm.insert("control point 1", startControlPoint()); // FIXME: use d->cp1 ?
0406             vm.insert("control point 2", endControlPoint()); // FIXME: use d->cp2 ?
0407         }
0408     }
0409 #endif
0410 
0411     return options;
0412 }
0413 
0414 QStringList TikzExportVisitor::nodeStyleOptions(Style * style)
0415 {
0416     QStringList options = styleOptions(style);
0417 
0418     //
0419     // export align
0420     //
0421     if (style->textAlignSet()) {
0422         options << "align=" + toString(style->textAlign());
0423     }
0424 
0425     //
0426     // export shape
0427     //
0428     if (style->shapeSet()) {
0429         options << toString(style->shape());
0430     }
0431 
0432     //
0433     // export inner sep and outer sep
0434     //
0435     if (style->innerSepSet()) {
0436         options << "inner sep=" + style->innerSep().toString();
0437     }
0438 
0439     if (style->outerSepSet()) {
0440         options << "outer sep=" + style->outerSep().toString();
0441     }
0442 
0443     //
0444     // export minimum width and minimum height
0445     //
0446     if (style->minimumWidthSet() && style->minimumHeightSet()
0447         && style->minimumWidth() == style->minimumHeight())
0448     {
0449         options << "minimum size=" + style->minimumWidth().toString();
0450     } else {
0451         if (style->minimumWidthSet()) {
0452             options << "minimum width=" + style->minimumWidth().toString();
0453         }
0454         if (style->minimumHeightSet()) {
0455             options << "minimum height=" + style->minimumHeight().toString();
0456         }
0457     }
0458 
0459     //
0460     // export rotation
0461     //
0462     if (style->rotationSet()) {
0463         options << QString("rotate=%1").arg(style->rotation());
0464     }
0465 
0466     return options;
0467 }
0468 
0469 }
0470 }
0471 
0472 // kate: indent-width 4; replace-tabs on;