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"