File indexing completed on 2024-05-12 04:06:19
0001 /* 0002 SPDX-FileCopyrightText: 2010 Johannes Loehnert <loehnert.kde@gmx.net> 0003 Based on the Jigsaw slicer by: 0004 SPDX-FileCopyrightText: 2009 Stefan Majewsky <majewsky@gmx.net> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "goldberg-engine.h" 0010 0011 #include <cmath> 0012 #include <QPainter> 0013 #include <QDebug> 0014 #include <QDir> 0015 #include <QRandomGenerator> 0016 #include "utilities.h" 0017 0018 0019 0020 GoldbergEngine::GoldbergEngine(Pala::SlicerJob *job) { 0021 m_dump_grid = false; 0022 m_job = job; 0023 // QImage uses memsharing, so this won't actually copy the img. 0024 m_image = m_job->image(); 0025 } 0026 0027 0028 void GoldbergEngine::set_dump_grid(bool dump) { 0029 if (m_dump_grid) { 0030 delete m_grid_image; 0031 } 0032 m_dump_grid = dump; 0033 if (m_dump_grid) { 0034 m_grid_image = new QImage(m_job->image().width(), m_job->image().height(), QImage::Format_RGB32); 0035 m_grid_image->fill(QColor(Qt::white).rgb()); 0036 } 0037 } 0038 0039 bool GoldbergEngine::get_dump_grid() { 0040 return m_dump_grid; 0041 } 0042 0043 int GoldbergEngine::get_image_width() { 0044 return m_image.width(); 0045 } 0046 0047 int GoldbergEngine::get_image_height() { 0048 return m_image.height(); 0049 } 0050 0051 void GoldbergEngine::dump_grid_image() { 0052 if (m_dump_grid) { 0053 QString path = QDir::home().filePath(QStringLiteral("goldberg-slicer-dump.png")); 0054 qDebug() << "Dumping grid image to" << path; 0055 m_grid_image->save(path, nullptr, -1); 0056 delete m_grid_image; 0057 m_dump_grid = false; 0058 } 0059 } 0060 0061 0062 GBClassicPlugParams GoldbergEngine::initEdge(bool is_straight) { 0063 GBClassicPlugParams r; 0064 r.size_correction = 1.0; 0065 r.flipped = (QRandomGenerator::global()->bounded(100) < m_flip_threshold); 0066 r.is_straight = is_straight; 0067 r.is_plugless = false; 0068 r.path_is_rendered = false; 0069 r.path = QPainterPath(); 0070 0071 if (is_straight) { 0072 // init the params to sensible values even when they are not needed 0073 // (some fool might reset is_straight) 0074 r.startangle=0; 0075 r.endangle=0; 0076 r.basepos=0.5; 0077 r.basewidth=0.1; 0078 r.knobsize = 0.2; 0079 r.knobangle = 25; 0080 r.knobtilt = 0; 0081 } 0082 else { 0083 reRandomizeEdge(r); 0084 } 0085 return r; 0086 } 0087 0088 0089 void GoldbergEngine::reRandomizeEdge(GBClassicPlugParams &r, bool keep_endangles) { 0090 0091 if (!keep_endangles) { 0092 qreal skew = (m_edge_curviness)/100. * 1.5; 0093 r.startangle = nonuniform_rand(2, -35, m_sigma_curviness, skew); 0094 r.endangle = nonuniform_rand(2, -35, m_sigma_curviness, skew); 0095 r.baseroundness = -dsin(fmin(r.startangle, r.endangle)); 0096 if (r.baseroundness < 0) r.baseroundness = 0; 0097 } 0098 0099 r.basepos = nonuniform_rand(0.2, 0.8, m_sigma_basepos, 0); 0100 r.basewidth = nonuniform_rand(0.1, 0.17, m_sigma_plugs, 0); // scales with knobscale 0101 r.knobsize = nonuniform_rand(0.17, 0.23, m_sigma_plugs, 0); // scales with knobscale 0102 r.knobangle = nonuniform_rand(10., 30., m_sigma_plugs, 0); 0103 r.knobtilt = nonuniform_rand(-20., 20., m_sigma_plugs, 0); 0104 0105 r.path_is_rendered = false; 0106 r.path = QPainterPath(); 0107 } 0108 0109 void GoldbergEngine::smooth_join(GBClassicPlugParams &border1, GBClassicPlugParams &border2) { 0110 bool found, b1end, b2end; 0111 found = false; 0112 if (border1.unit_x.p1() == border2.unit_x.p1()) { 0113 found=true; b1end = false; b2end = false; 0114 } 0115 if (border1.unit_x.p1() == border2.unit_x.p2()) { 0116 found=true; b1end = false; b2end = true; 0117 } 0118 if (border1.unit_x.p2() == border2.unit_x.p1()) { 0119 found=true; b1end = true; b2end = false; 0120 } 0121 if (border1.unit_x.p2() == border2.unit_x.p2()) { 0122 found=true; b1end = true; b2end = true; 0123 } 0124 0125 if (!found) { 0126 // no common endpoint. don't do anything 0127 qDebug() << "slicer-goldberg.cpp : smooth_join: was asked to smooth between non-adjacent borders."; 0128 return; 0129 } 0130 0131 b1end ^= border1.flipped; 0132 b2end ^= border2.flipped; 0133 0134 qreal a1 = b1end ? border1.endangle : border1.startangle; 0135 qreal a2 = b2end ? border2.endangle : border2.startangle; 0136 0137 if (b1end ^ b2end) { 0138 a1 = 0.5*(a1-a2); 0139 a2 = -a1; 0140 } 0141 else { 0142 a1 = 0.5*(a1+a2); 0143 a2 = a1; 0144 } 0145 0146 if (b1end) border1.endangle = a1; else border1.startangle = a1; 0147 if (b2end) border2.endangle = a2; else border2.startangle = a2; 0148 0149 border1.path_is_rendered = false; 0150 border1.path = QPainterPath(); 0151 border2.path_is_rendered = false; 0152 border2.path = QPainterPath(); 0153 0154 } 0155 0156 0157 bool GoldbergEngine::plugsIntersect(GBClassicPlugParams &candidate, GBClassicPlugParams &other, QList<GBClassicPlugParams*> *offenders) { 0158 if (!candidate.path_is_rendered) renderClassicPlug(candidate); 0159 if (!other.path_is_rendered) renderClassicPlug(other); 0160 0161 bool result = candidate.path.intersects(other.path); 0162 if (result && offenders!=nullptr) { 0163 offenders->append(&other); 0164 } 0165 return result; 0166 } 0167 0168 bool GoldbergEngine::plugOutOfBounds(GBClassicPlugParams &candidate) { 0169 if (!candidate.path_is_rendered) renderClassicPlug(candidate); 0170 0171 QPainterPath imagerect = QPainterPath(QPointF(0.0, 0.0)); 0172 imagerect.lineTo(QPointF(m_image.width(), 0.0)); 0173 imagerect.lineTo(QPointF(m_image.width(), m_image.height())); 0174 imagerect.lineTo(QPointF(0.0, m_image.height())); 0175 imagerect.closeSubpath(); 0176 0177 return (!imagerect.contains(candidate.path)); 0178 } 0179 0180 void GoldbergEngine::makePlugless(GBClassicPlugParams ¶meters){ 0181 parameters.is_plugless = true; 0182 parameters.size_correction = 1.0; 0183 parameters.path_is_rendered = false; 0184 parameters.path = QPainterPath(); 0185 } 0186 0187 void GoldbergEngine::makePieceFromPath(int piece_id, QPainterPath path) { 0188 0189 path.closeSubpath(); 0190 0191 //determine the required size of the mask 0192 const QRect maskRect = path.boundingRect().toAlignedRect(); 0193 //create the mask 0194 QImage mask(maskRect.size(), QImage::Format_ARGB32_Premultiplied); 0195 mask.fill(0x00000000); //fully transparent color 0196 QPainter painter(&mask); 0197 painter.translate(- maskRect.topLeft()); 0198 if (m_outlines) { 0199 painter.setPen(Qt::NoPen); 0200 } 0201 else { 0202 painter.setPen(QPen(Qt::black, 1.0)); //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) 0203 // 1.0 still leaves the slightest trace of a glitch. but making the stroke thicker makes the plugs appear non-matching even when they belong together. 0204 } 0205 painter.setBrush(Qt::black); 0206 painter.setRenderHint(QPainter::Antialiasing); 0207 painter.drawPath(path); 0208 painter.end(); 0209 0210 // create the piece (copied over from libpala) 0211 QPoint offset = maskRect.topLeft(); 0212 QImage pieceImage(mask); 0213 QPainter piecePainter(&pieceImage); 0214 piecePainter.setCompositionMode(QPainter::CompositionMode_SourceIn); 0215 piecePainter.drawImage(QPoint(), safeQImageCopy(m_image, QRect(offset, mask.size()))); 0216 0217 // Outline -- code was left in though option was removed (rendering done by palapeli itself now) 0218 if (m_outlines) { 0219 piecePainter.translate(-offset); 0220 piecePainter.setRenderHint(QPainter::Antialiasing); 0221 piecePainter.setCompositionMode(QPainter::CompositionMode_SourceAtop); 0222 piecePainter.setBrush(Qt::NoBrush); 0223 0224 QPen outlinePen = QPen(); 0225 outlinePen.setWidth(m_length_base / 33.0); 0226 QColor opColor = QColor(0,0,0, 64); 0227 outlinePen.setColor(opColor); 0228 piecePainter.setPen(outlinePen); 0229 piecePainter.drawPath(path); 0230 0231 } 0232 piecePainter.end(); 0233 0234 m_job->addPiece(piece_id, pieceImage, maskRect.topLeft()); 0235 } 0236 0237 //A modified version of QImage::copy, which avoids rendering errors even if rect is outside the bounds of the source image. 0238 QImage safeQImageCopy(const QImage& source, const QRect& rect) 0239 { 0240 QRect targetRect(QPoint(), rect.size()); 0241 //copy image 0242 QImage target(rect.size(), source.format()); 0243 QPainter p(&target); 0244 p.drawImage(targetRect, source, rect); 0245 p.end(); 0246 return target; 0247 //Strangely, source.copy(rect) does not work. It produces black borders. 0248 } 0249 0250 void GoldbergEngine::addRelation(int piece1, int piece2) { 0251 m_job->addRelation(piece1, piece2); 0252 } 0253 0254 0255 void GoldbergEngine::addPlugToPath(QPainterPath& path, bool reverse, GBClassicPlugParams ¶ms) { 0256 0257 if (!params.path_is_rendered) renderClassicPlug(params); 0258 0259 if (!reverse) { 0260 path.connectPath(params.path); 0261 0262 if (m_dump_grid) { 0263 // The idea here is that each border is drawn exactly twice - once forward, once reversed. 0264 // So if we catch the forward case, we will draw all borders once. 0265 QPainter borderPainter(m_grid_image); 0266 QPen outlinePen = QPen(); 0267 outlinePen.setWidth(m_length_base / 50.0); 0268 outlinePen.setColor(QColor(Qt::black)); 0269 0270 borderPainter.setPen(outlinePen); 0271 borderPainter.setRenderHint(QPainter::Antialiasing); 0272 borderPainter.setCompositionMode(QPainter::CompositionMode_SourceOver); 0273 borderPainter.setBrush(Qt::NoBrush); 0274 borderPainter.drawPath(path); 0275 } 0276 } 0277 else { 0278 path.connectPath(params.path.toReversed()); 0279 } 0280 } 0281 0282 void GoldbergEngine::renderClassicPlug(GBClassicPlugParams ¶ms) { 0283 // unit_x gives offset and direction of the x base vector. Start and end should be the grid points. 0284 0285 params.path_is_rendered = true; 0286 0287 // move the endpoints inwards an unnoticable bit, so that the intersection detector 0288 // won't trip on the common endpoint. 0289 QLineF u_x = QLineF(params.unit_x.pointAt(0.0001), params.unit_x.pointAt(0.9999)); 0290 //QLineF u_x = params.unit_x; 0291 0292 params.path.moveTo(u_x.p1()); 0293 0294 if (params.is_straight) { 0295 params.path.lineTo(u_x.p2()); 0296 return; 0297 } 0298 if (params.flipped) { 0299 u_x = QLineF(u_x.p2(), u_x.p1()); 0300 } 0301 0302 0303 QLineF u_y = u_x.normalVector(); 0304 // move y unit to start at (0,0). 0305 u_y.translate(-u_y.p1()); 0306 0307 qreal scaling = m_length_base / u_x.length() * params.size_correction; 0308 if (params.basewidth * scaling > 0.8) { 0309 // Plug is too large for the edge length. Make it smaller. 0310 scaling = 0.8 / params.basewidth; 0311 qDebug() << "shrinking a plug"; 0312 } 0313 0314 // some magic numbers here... carefully fine-tuned, better leave them as they are. 0315 qreal ends_ctldist = 0.4; 0316 //qreal base_lcdist = 0.1 * scaling; 0317 qreal base_ucdist = 0.05 * scaling; 0318 qreal knob_lcdist = 0.6 * params.knobsize * scaling; 0319 qreal knob_ucdist = 0.8 * params.knobsize * scaling; 0320 0321 // set up piece -- here is where the really interesting stuff happens. 0322 // We will work from the ends inwards, so that symmetry counterparts are adjacent. 0323 // The QLine.pointAt function is used to transform everything into the coordinate 0324 // space defined by the us. 0325 // -- end points 0326 0327 qreal r1y = ends_ctldist * params.basepos * dsin(params.startangle); 0328 qreal q6y = ends_ctldist * (1.-params.basepos) * dsin(params.endangle); 0329 QPointF p1 = u_x.p1(); 0330 QPointF p6 = u_x.p2(); 0331 QPointF r1 = u_x.pointAt(ends_ctldist * params.basepos * dcos(params.startangle)) + 0332 u_y.pointAt(r1y); 0333 QPointF q6 = u_x.pointAt(1. - ends_ctldist * (1.-params.basepos) * dcos(params.endangle)) + 0334 u_y.pointAt(q6y); 0335 0336 // -- base points 0337 qreal p2x = params.basepos - 0.5 * params.basewidth * scaling; 0338 qreal p5x = params.basepos + 0.5 * params.basewidth * scaling; 0339 0340 if (p2x < 0.1 || p5x > 0.9) { 0341 // knob to large. center knob on the edge. (params.basewidth * scaling < 0.8 -- see above) 0342 p2x = 0.5 - 0.5 * params.basewidth * scaling; 0343 p5x = 0.5 + 0.5 * params.basewidth * scaling; 0344 } 0345 0346 //qreal base_y = r1y > q6y ? r1y : q6y; 0347 //qreal base_y = 0.5*(r1y + q6y); 0348 qreal base_y = -params.baseroundness * ends_ctldist * fmin(p2x, 1.-p5x); 0349 if (base_y > 0) base_y = 0; 0350 0351 qreal base_lcy = base_y * 2.0; 0352 0353 base_y += base_ucdist/2; 0354 base_lcy -= base_ucdist/2; 0355 //qreal base_lcy = r1y; 0356 //if (q6y < r1y) base_lcy = q6y; 0357 0358 // at least -base_ucdist from base_y 0359 //if (base_lcy > base_y - base_ucdist) base_lcy = base_y-base_ucdist; 0360 0361 QPointF q2 = u_x.pointAt(p2x) + 0362 u_y.pointAt(base_lcy); 0363 QPointF r5 = u_x.pointAt(p5x) + 0364 u_y.pointAt(base_lcy); 0365 QPointF p2 = u_x.pointAt(p2x) + 0366 u_y.pointAt(base_y); 0367 QPointF p5 = u_x.pointAt(p5x) + 0368 u_y.pointAt(base_y); 0369 QPointF r2 = u_x.pointAt(p2x) + 0370 u_y.pointAt(base_y + base_ucdist); 0371 QPointF q5 = u_x.pointAt(p5x) + 0372 u_y.pointAt(base_y + base_ucdist); 0373 0374 if (params.is_plugless) { 0375 if (!params.flipped) { 0376 params.path.cubicTo(r1, q2, p2); 0377 params.path.cubicTo(r2, q5, p5); 0378 params.path.cubicTo(r5, q6, p6); 0379 } 0380 else { 0381 params.path.cubicTo(q6, r5, p5); 0382 params.path.cubicTo(q5, r2, p2); 0383 params.path.cubicTo(q2, r1, p1); 0384 } 0385 return; 0386 } 0387 0388 // -- knob points 0389 qreal p3x = p2x - params.knobsize * scaling * dsin(params.knobangle - params.knobtilt); 0390 qreal p4x = p5x + params.knobsize * scaling * dsin(params.knobangle + params.knobtilt); 0391 // for the y coordinate, knobtilt sign was swapped. Knobs look better this way... 0392 // like x offset from base points y, but that is 0. 0393 qreal p3y = params.knobsize * scaling * dcos(params.knobangle + params.knobtilt) + base_y; 0394 qreal p4y = params.knobsize * scaling * dcos(params.knobangle - params.knobtilt) + base_y; 0395 0396 QPointF q3 = u_x.pointAt(p3x) + 0397 u_y.pointAt(p3y - knob_lcdist); 0398 QPointF r4 = u_x.pointAt(p4x) + 0399 u_y.pointAt(p4y - knob_lcdist); 0400 QPointF p3 = u_x.pointAt(p3x) + 0401 u_y.pointAt(p3y); 0402 QPointF p4 = u_x.pointAt(p4x) + 0403 u_y.pointAt(p4y); 0404 QPointF r3 = u_x.pointAt(p3x) + 0405 u_y.pointAt(p3y + knob_ucdist); 0406 QPointF q4 = u_x.pointAt(p4x) + 0407 u_y.pointAt(p4y + knob_ucdist); 0408 0409 // done setting up. construct path. 0410 // if flipped, add points in reverse. 0411 if (!params.flipped) { 0412 params.path.cubicTo(r1, q2, p2); 0413 params.path.cubicTo(r2, q3, p3); 0414 params.path.cubicTo(r3, q4, p4); 0415 params.path.cubicTo(r4, q5, p5); 0416 params.path.cubicTo(r5, q6, p6); 0417 } 0418 else { 0419 params.path.cubicTo(q6, r5, p5); 0420 params.path.cubicTo(q5, r4, p4); 0421 params.path.cubicTo(q4, r3, p3); 0422 params.path.cubicTo(q3, r2, p2); 0423 params.path.cubicTo(q2, r1, p1); 0424 } 0425 } 0426