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 &parameters){
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 &params) {
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 &params) {
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