File indexing completed on 2024-04-28 04:05:13

0001 /*
0002     SPDX-FileCopyrightText: 2009 Stefan Majewsky <majewsky@gmx.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "slicer-jigsaw.h"
0008 
0009 #include <cmath>
0010 #include <QPainter>
0011 #include <QPainterPath>
0012 #include <KPluginFactory>
0013 #include <QRandomGenerator>
0014 
0015 //BEGIN utility functions
0016 
0017 qreal myrand(qreal min, qreal max)
0018 {
0019         const qreal randNum = qreal(QRandomGenerator::global()->bounded(10000)) / 10000; //a quite random number between 0 and 1
0020     return randNum * (max - min) + min;
0021 }
0022 
0023 inline qreal operator*(const QPointF& a, const QPointF& b)
0024 {
0025     return a.x() * b.x() + a.y() * b.y();
0026 }
0027 
0028 //NOTE: The lines in the following methods are always directed clockwise, which is an important property!
0029 
0030 QLineF topSide(const QRect& rect)
0031 {
0032     return QLineF(rect.topLeft(), rect.topRight());
0033 }
0034 
0035 QLineF rightSide(const QRect& rect)
0036 {
0037     return QLineF(rect.topRight(), rect.bottomRight());
0038 }
0039 
0040 QLineF bottomSide(const QRect& rect)
0041 {
0042     return QLineF(rect.bottomRight(), rect.bottomLeft());
0043 }
0044 
0045 QLineF leftSide(const QRect& rect)
0046 {
0047     return QLineF(rect.bottomLeft(), rect.topLeft());
0048 }
0049 
0050 //END utility functions
0051 
0052 /*static*/ JigsawPlugParams JigsawPlugParams::createRandomParams()
0053 {
0054     JigsawPlugParams result;
0055     result.plugPosition = myrand(0.35, 0.65);
0056     const qreal maxPlugLength = 0.4 - 0.88 * qAbs(0.5 - result.plugPosition);
0057     result.plugLength = myrand(0.75, 1.0) * maxPlugLength;
0058     result.plugWidth = myrand(0.18, 0.38);
0059     const qreal minDistortion1 = 0.75 * (0.7 + result.plugWidth);
0060     result.distortion1 = myrand(minDistortion1, minDistortion1 * 1.1);
0061     result.distortion2 = myrand(0.4, 1.0);
0062     result.baseHeight = myrand(0.0, 0.2);
0063     result.baseDistortion = myrand(0.0, 1.0);
0064     return result;
0065 }
0066 
0067 JigsawPlugParams JigsawPlugParams::mirrored()
0068 {
0069     JigsawPlugParams result(*this);
0070     result.plugPosition = 1 - result.plugPosition;
0071     return result;
0072 }
0073 
0074 K_PLUGIN_CLASS_WITH_JSON(JigsawSlicer, "palapeli_jigsawslicer.json")
0075 
0076 JigsawSlicer::JigsawSlicer(QObject* parent, const QVariantList& args)
0077     : Pala::Slicer(parent, args)
0078     , Pala::SimpleGridPropertySet(this)
0079 {
0080 }
0081 
0082 bool JigsawSlicer::run(Pala::SlicerJob* job)
0083 {
0084     //read job
0085     const QSize pieceCount = Pala::SimpleGridPropertySet::pieceCount(job);
0086     const int xCount = pieceCount.width();
0087     const int yCount = pieceCount.height();
0088     const QImage image = job->image();
0089     //calculate some metrics
0090     const int width = image.width(), height = image.height();
0091     const int pieceWidth = width / xCount, pieceHeight = height / yCount;
0092     const int plugPaddingX = pieceWidth / 2, plugPaddingY = pieceHeight / 2; //see below
0093     //find plug shape types
0094     JigsawPlugParams** horizontalPlugParams = new JigsawPlugParams*[xCount];
0095     JigsawPlugParams** verticalPlugParams = new JigsawPlugParams*[xCount];
0096     int** horizontalPlugDirections = new int*[xCount]; //+1: male is left, female is right, plug points to the right (-1 is the opposite direction)
0097     int** verticalPlugDirections = new int*[xCount]; //true: male is above female, plug points down
0098         auto *generator = QRandomGenerator::global();
0099     for (int x = 0; x < xCount; ++x)
0100     {
0101         horizontalPlugParams[x] = new JigsawPlugParams[yCount];
0102         verticalPlugParams[x] = new JigsawPlugParams[yCount];
0103         horizontalPlugDirections[x] = new int[yCount];
0104         verticalPlugDirections[x] = new int[yCount];
0105         for (int y = 0; y < yCount; ++y)
0106         {
0107             //plugs along X axis
0108             horizontalPlugParams[x][y] = JigsawPlugParams::createRandomParams();
0109                         horizontalPlugDirections[x][y] = (generator->bounded(2)) ? 1 : -1;
0110             //plugs along Y axis
0111             verticalPlugParams[x][y] = JigsawPlugParams::createRandomParams();
0112                         verticalPlugDirections[x][y] = (generator->bounded(2)) ? 1 : -1;
0113         }
0114     }
0115     //create pieces
0116     for (int x = 0; x < xCount; ++x)
0117     {
0118         for (int y = 0; y < yCount; ++y)
0119         {
0120             //some geometry
0121             const QRect pieceBaseRect( //piece without plugs
0122                 x * pieceWidth,
0123                 y * pieceHeight,
0124                 pieceWidth,
0125                 pieceHeight
0126             );
0127             QRect pieceRect( //piece with padding space for plugs (will overlap with neighbor piece rects)
0128                 x * pieceWidth - plugPaddingX,
0129                 y * pieceHeight - plugPaddingY,
0130                 pieceWidth + 2 * plugPaddingX,
0131                 pieceHeight + 2 * plugPaddingY
0132             );
0133             const QRect maskBaseRect( //the part of the mask that maps to pieceBaseRect
0134                 plugPaddingX,
0135                 plugPaddingY,
0136                 pieceWidth,
0137                 pieceHeight
0138             );
0139             const QRect maskRect( //the whole mask; maps to pieceRect
0140                 0,
0141                 0,
0142                 pieceWidth + 2 * plugPaddingX,
0143                 pieceHeight + 2 * plugPaddingY
0144             );
0145             //create the mask path
0146             QPainterPath path;
0147             path.moveTo(maskBaseRect.topLeft());
0148             //top plug
0149             if (y == 0)
0150                 path.lineTo(maskBaseRect.topRight());
0151             else
0152                 addPlugToPath(path, maskBaseRect.height(), topSide(maskBaseRect), QPointF(0, verticalPlugDirections[x][y - 1]), verticalPlugParams[x][y - 1]);
0153             //right plug
0154             if (x == xCount - 1)
0155                 path.lineTo(maskBaseRect.bottomRight());
0156             else
0157                 addPlugToPath(path, maskBaseRect.width(), rightSide(maskBaseRect), QPointF(horizontalPlugDirections[x][y], 0), horizontalPlugParams[x][y].mirrored());
0158             //bottom plug
0159             if (y == yCount - 1)
0160                 path.lineTo(maskBaseRect.bottomLeft());
0161             else
0162                 addPlugToPath(path, maskBaseRect.height(), bottomSide(maskBaseRect), QPointF(0, verticalPlugDirections[x][y]), verticalPlugParams[x][y].mirrored());
0163             //left plug
0164             if (x == 0)
0165                 path.lineTo(maskBaseRect.topLeft());
0166             else
0167                 addPlugToPath(path, maskBaseRect.width(), leftSide(maskBaseRect), QPointF(horizontalPlugDirections[x - 1][y], 0), horizontalPlugParams[x - 1][y]);
0168             //determine the required size of the mask
0169             path.closeSubpath();
0170             const QRect newMaskRect = path.boundingRect().toAlignedRect();
0171             pieceRect.adjust(
0172                 newMaskRect.left() - maskRect.left(),
0173                 newMaskRect.top() - maskRect.top(),
0174                 newMaskRect.right() - maskRect.right(),
0175                 newMaskRect.bottom() - maskRect.bottom()
0176             );
0177             //create the mask
0178             QImage mask(newMaskRect.size(), QImage::Format_ARGB32_Premultiplied);
0179             mask.fill(0x00000000); //fully transparent color
0180             QPainter painter(&mask);
0181             painter.translate(maskRect.topLeft() - newMaskRect.topLeft());
0182             painter.setPen(QPen(Qt::black, 1.5)); //we explicitly use a pen stroke in order to let the pieces overlap a bit (which reduces rendering glitches at the edges where puzzle pieces touch)
0183             painter.setBrush(Qt::black);
0184             painter.setRenderHint(QPainter::Antialiasing);
0185             painter.drawPath(path);
0186             painter.end();
0187             job->addPieceFromMask(x + y * xCount, mask, pieceRect.topLeft());
0188         }
0189     }
0190     //create relations
0191     for (int x = 0; x < xCount; ++x)
0192     {
0193         for (int y = 0; y < yCount; ++y)
0194         {
0195             //along X axis (pointing left)
0196             if (x != 0)
0197                 job->addRelation(x + y * xCount, (x - 1) + y * xCount);
0198             //along Y axis (pointing up)
0199             if (y != 0)
0200                 job->addRelation(x + y * xCount, x + (y - 1) * xCount);
0201         }
0202     }
0203     //cleanup
0204     for (int x = 0; x < xCount - 1; ++x)
0205     {
0206         delete[] horizontalPlugParams[x];
0207         delete[] verticalPlugParams[x];
0208         delete[] horizontalPlugDirections[x];
0209         delete[] verticalPlugDirections[x];
0210     }
0211     delete[] horizontalPlugParams;
0212     delete[] verticalPlugParams;
0213     delete[] horizontalPlugDirections;
0214     delete[] verticalPlugDirections;
0215     return true;
0216 }
0217 
0218 void JigsawSlicer::addPlugToPath(QPainterPath& path, qreal plugNormLength, const QLineF& line, const QPointF& plugDirection, const JigsawPlugParams& params)
0219 {
0220     //Naming convention: The path runs through five points (p1 through p5).
0221     //pNbase is the projection of pN to the line between p1 and p5.
0222     //tN is the parameter of pNbase on the line between p1 and p5 (with t1 = 0 and t5 = 1).
0223     //qN is the control point of pN on the cubic between p{N-1} and pN.
0224     //rN is the control point of pN on the cubic between pN and p{N+1}.
0225     const QPointF p1 = line.p1(), p5 = line.p2();
0226     const QPointF growthDirection = plugDirection / sqrt(plugDirection * plugDirection);
0227     //const qreal sizeFactor = line.length();
0228     //const QPointF growthVector = growthDirection * sizeFactor;
0229     const QPointF plugVector = params.plugLength * plugNormLength * growthDirection;
0230     //calculate points p2, p3, p4
0231     const qreal t3 = params.plugPosition;
0232     const QPointF p3base = (1.0 - t3) * p1 + t3 * p5;
0233     const QPointF p3 = p3base + plugVector;
0234     const qreal t2 = params.plugPosition - params.plugWidth / 2.0;
0235     const QPointF p2base = (1.0 - t2) * p1 + t2 * p5;
0236     const QPointF p2 = p2base + params.baseHeight * plugVector;
0237     const qreal t4 = params.plugPosition + params.plugWidth / 2.0;
0238     const QPointF p4base = (1.0 - t4) * p1 + t4 * p5;
0239     const QPointF p4 = p4base + params.baseHeight * plugVector;
0240     //calculate control points
0241     const QPointF r1 = p1;
0242     const qreal tr2 = params.distortion1 * t2;
0243     const QPointF r2base = (1.0 - tr2) * p1 + tr2 * p5;
0244     const QPointF r2 = r2base + params.distortion2 * plugVector;
0245     const QPointF q2 = p2 + params.baseDistortion * (p2 - r2);
0246     const QPointF q3 = p3 + (p2base - p3base);
0247     const QPointF r3 = p3 + (p4base - p3base);
0248     const qreal tq4 = 1 - (params.distortion1 * (1 - t4));
0249     const QPointF q4base = (1.0 - tq4) * p1 + tq4 * p5;
0250     const QPointF q4 = q4base + params.distortion2 * plugVector;
0251     const QPointF r4 = p4 + params.baseDistortion * (p4 - q4);
0252     const QPointF q5 = p5;
0253     //construct path
0254     path.lineTo(p1);
0255     path.cubicTo(r1, q2, p2);
0256     path.cubicTo(r2, q3, p3);
0257     path.cubicTo(r3, q4, p4);
0258     path.cubicTo(r4, q5, p5);
0259 }
0260 
0261 #include "moc_slicer-jigsaw.cpp"
0262 #include "slicer-jigsaw.moc"