Commit a5ab9cb8 authored by Alberto Mardegan's avatar Alberto Mardegan

Add Scaler class

Add a helper class to compute the transformations when scaling and
panning occurs.
parent 396a2de4
/*
* Copyright (C) 2020 Alberto Mardegan <mardy@users.sourceforge.net>
*
* This file is part of LomiriVNC.
*
* LomiriVNC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LomiriVNC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LomiriVNC. If not, see <http://www.gnu.org/licenses/>.
*/
#include "scaler.h"
#include <QDebug>
using namespace LomiriVNC;
/* Compute the minimum offset (from the origin) that needs to be applied to
* the rect `view` so that an object of size `objectSize` located at the
* origin would optimally fit into it.
* "Optimally" means:
* 1. `view` is as filled as possible with the object (minimize the empty
* area)
* 2. If the object is entirely visible, it should be centered into the view.
*/
QPointF Scaler::computeFitOffset(const QRectF &view, const QSizeF &objectSize)
{
double offsetX = 0, offsetY = 0;
if (view.width() > objectSize.width()) {
/* center the source objectSize within the view */
offsetX = -view.x() - (view.width() - objectSize.width()) / 2;
} else {
double leftBorder = -view.x();
double rightBorder = view.right() - objectSize.width();
if (leftBorder > 0) {
offsetX = leftBorder;
} else if (rightBorder > 0) {
offsetX = -rightBorder;
}
}
if (view.height() > objectSize.height()) {
/* center the source objectSize within the view */
offsetY = -view.y() - (view.height() - objectSize.height()) / 2;
} else {
double topBorder = -view.y();
double bottomBorder = view.bottom() - objectSize.height();
if (topBorder > 0) {
offsetY = topBorder;
} else if (bottomBorder > 0) {
offsetY = -bottomBorder;
}
}
return QPointF(offsetX, offsetY);
}
bool Scaler::updateMapping(const InputData &in, OutputData *out)
{
QTransform itemToSource;
const QSizeF &sourceSize = in.sourceSize;
const QSizeF &itemSize = in.itemSize;
if (Q_UNLIKELY(sourceSize.isEmpty() || itemSize.isEmpty())) {
return false;
}
const QRectF itemRect(QPointF(0, 0), itemSize);
// Compute the minimum scale
QSizeF scaledSize = sourceSize.scaled(itemSize, Qt::KeepAspectRatio);
double minScale = qMin(scaledSize.width() / sourceSize.width(), 1.0);
double scale = qMax(in.requestedScale, minScale);
// Compute the transformation matrix
QRectF sourceRect = QRectF(QPointF(0, 0), sourceSize);
scaledSize = sourceSize * scale;
QPointF center = in.requestedCenter;
QPointF itemRectCenter = itemRect.center();
QPointF sourceRectCenter = sourceRect.center();
itemToSource.translate(sourceRectCenter.x() + center.x(),
sourceRectCenter.y() + center.y());
itemToSource.scale(1.0 / scale, 1.0 / scale);
itemToSource.translate(-itemRectCenter.x(), -itemRectCenter.y());
/* Ensure that as much as possible of the source is visible. If the source
* is completely visible, make sure that it's centered in the view.
*/
QPointF centerAdjustment =
computeFitOffset(itemToSource.mapRect(itemRect), sourceSize);
if (!centerAdjustment.isNull()) {
// Apply the offset to the transformation matrix
QTransform adjustment =
QTransform::fromTranslate(centerAdjustment.x(),
centerAdjustment.y());
itemToSource *= adjustment;
}
/* Compute the painted rect: use the inverse transformation to figure out
* the position and size of the source item in view coordinates, and then
* clip it to the view.
*/
QTransform sourceToItem = itemToSource.inverted();
QRectF virtualSourceRect = sourceToItem.mapRect(sourceRect);
QRectF paintedRect = virtualSourceRect.intersected(itemRect);
// Compute the visible area of the source (in src coordinates)
QRectF sourceVisibleRect = itemToSource.mapRect(paintedRect);
// Prepare the return structure
out->center = center + centerAdjustment;
out->sourceVisibleRect = sourceVisibleRect;
out->itemPaintedRect = paintedRect;
out->scale = scale;
out->itemToSource = itemToSource;
out->sourceToItem = sourceToItem;
return true;
}
/*
* Copyright (C) 2020 Alberto Mardegan <mardy@users.sourceforge.net>
*
* This file is part of LomiriVNC.
*
* LomiriVNC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LomiriVNC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LomiriVNC. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LOMIRIVNC_VNC_SCALER_H
#define LOMIRIVNC_VNC_SCALER_H
#include <QPointF>
#include <QRect>
#include <QRectF>
#include <QSize>
#include <QSizeF>
#include <QTransform>
class ScalerTest;
namespace LomiriVNC {
class Scaler
{
public:
struct InputData {
QSizeF sourceSize;
QSizeF itemSize;
double requestedScale;
QPointF requestedCenter; /* as offset from the source center, in source
coordinates */
};
struct OutputData {
QRectF sourceVisibleRect;
QRectF itemPaintedRect;
double scale;
QPointF center;
QTransform itemToSource;
QTransform sourceToItem;
};
static bool updateMapping(const InputData &in, OutputData *out);
private:
friend class ::ScalerTest;
static QPointF computeFitOffset(const QRectF &rect, const QSizeF &size);
};
} // namespace
#endif // LOMIRIVNC_VNC_SCALER_H
......@@ -3,6 +3,15 @@ import qbs 1.0
Project {
condition: project.buildTests
Test {
name: "scaler-test"
files: [
"../src/scaler.cpp",
"../src/scaler.h",
"tst_scaler.cpp",
]
}
CoverageClean {
condition: project.enableCoverage
}
......
/*
* Copyright (C) 2020 Alberto Mardegan <mardy@users.sourceforge.net>
*
* This file is part of LomiriVNC.
*
* LomiriVNC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LomiriVNC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LomiriVNC. If not, see <http://www.gnu.org/licenses/>.
*/
#include "scaler.h"
#include <QByteArray>
#include <QTest>
Q_DECLARE_METATYPE(LomiriVNC::Scaler::InputData)
using namespace LomiriVNC;
struct OutputDataWithoutTransform: public Scaler::OutputData {
OutputDataWithoutTransform() = default;
OutputDataWithoutTransform(const QRectF &sourceVisibleRect,
const QRectF &itemPaintedRect,
double scale,
const QPointF &center):
OutputData{sourceVisibleRect,
itemPaintedRect,
scale,
center,
QTransform(),
QTransform()}
{
}
};
Q_DECLARE_METATYPE(OutputDataWithoutTransform)
class ScalerTest: public QObject
{
Q_OBJECT
using InputData = Scaler::InputData;
using OutputData = Scaler::OutputData;
private Q_SLOTS:
void testInvalidInput_data();
void testInvalidInput();
void testMapping_data();
void testMapping();
void testFitOffset_data();
void testFitOffset();
};
void ScalerTest::testInvalidInput_data()
{
QTest::addColumn<InputData>("inputData");
QTest::newRow("sourceSize") <<
InputData {
QSizeF(20, 0),
QSizeF(10, 10),
0.1,
QPointF(0, 0)
};
QTest::newRow("itemSize") <<
InputData {
QSizeF(20, 10),
QSizeF(0, 10),
0.1,
QPointF(0, 0)
};
}
void ScalerTest::testInvalidInput()
{
QFETCH(InputData, inputData);
OutputData out;
bool ok = Scaler::updateMapping(inputData, &out);
QVERIFY(!ok);
}
void ScalerTest::testMapping_data()
{
using OutputData = OutputDataWithoutTransform;
QTest::addColumn<InputData>("inputData");
QTest::addColumn<OutputData>("expected");
QTest::newRow("identity") <<
InputData {
QSizeF(100, 100),
QSizeF(100, 100),
1.0,
QPointF(0, 0)
} <<
OutputData {
QRectF(0, 0, 100, 100),
QRectF(0, 0, 100, 100),
1.0,
QPointF(0, 0),
};
QTest::newRow("larger source") <<
InputData {
QSizeF(1000, 200),
QSizeF(100, 100),
1.0,
QPointF(0, 0)
} <<
OutputData {
QRectF(450, 50, 100, 100),
QRectF(0, 0, 100, 100),
1.0,
QPointF(0, 0),
};
QTest::newRow("doubled") <<
InputData {
QSizeF(200, 1000),
QSizeF(60, 100),
2.0,
QPointF(0, 0)
} <<
OutputData {
QRectF(85, 475, 30, 50),
QRectF(0, 0, 60, 100),
2.0,
QPointF(0, 0),
};
QTest::newRow("moved") <<
InputData {
QSizeF(800, 600),
QSizeF(60, 120),
1.0,
QPointF(20, -30)
} <<
OutputData {
QRectF(390, 210, 60, 120),
QRectF(0, 0, 60, 120),
1.0,
QPointF(20, -30),
};
QTest::newRow("minscale, no borders") <<
InputData {
QSizeF(2000, 1000),
QSizeF(200, 100),
0.0,
QPointF(0, 0)
} <<
OutputData {
QRectF(0, 0, 2000, 1000),
QRectF(0, 0, 200, 100),
0.1,
QPointF(0, 0),
};
QTest::newRow("minscale, left&right borders") <<
InputData {
QSizeF(600, 1000),
QSizeF(500, 500),
0.0,
QPointF(0, 0)
} <<
OutputData {
QRectF(0, 0, 600, 1000),
QRectF(100, 0, 300, 500),
0.5,
QPointF(0, 0),
};
QTest::newRow("minscale, top&bottom borders") <<
InputData {
QSizeF(1000, 200),
QSizeF(400, 300),
0.0,
QPointF(0, 0)
} <<
OutputData {
QRectF(0, 0, 1000, 200),
QRectF(0, 110, 400, 80),
0.4,
QPointF(0, 0),
};
QTest::newRow("identity, with offset") <<
InputData {
QSizeF(100, 100),
QSizeF(100, 100),
1.0,
QPointF(10, -30)
} <<
OutputData {
QRectF(0, 0, 100, 100),
QRectF(0, 0, 100, 100),
1.0,
QPointF(0, 0),
};
QTest::newRow("scaled with offset") <<
InputData {
QSizeF(400, 200),
QSizeF(200, 100),
0.2,
QPointF(-20, 80)
} <<
OutputData {
QRectF(0, 0, 400, 200),
QRectF(0, 0, 200, 100),
0.5,
QPointF(0, 0),
};
QTest::newRow("scaled, show bottom right corner") <<
InputData {
QSizeF(800, 600),
QSizeF(100, 100),
0.5,
QPointF(400, 300) // bottom right corner
} <<
OutputData {
QRectF(600, 400, 200, 200),
QRectF(0, 0, 100, 100),
0.5,
QPointF(300, 200),
};
QTest::newRow("scaled, show left area, big offset") <<
InputData {
QSizeF(800, 600),
QSizeF(100, 100),
0.5,
QPointF(-4000, 0)
} <<
OutputData {
QRectF(0, 200, 200, 200),
QRectF(0, 0, 100, 100),
0.5,
QPointF(-300, 0),
};
}
void ScalerTest::testMapping()
{
using OutputData = OutputDataWithoutTransform;
QFETCH(InputData, inputData);
QFETCH(OutputData, expected);
OutputData output;
bool ok = Scaler::updateMapping(inputData, &output);
QVERIFY(ok);
QCOMPARE(output.sourceVisibleRect, expected.sourceVisibleRect);
QCOMPARE(output.itemPaintedRect, expected.itemPaintedRect);
QCOMPARE(output.scale, expected.scale);
QCOMPARE(output.center, expected.center);
}
void ScalerTest::testFitOffset_data()
{
QTest::addColumn<QRectF>("view");
QTest::addColumn<QSizeF>("objectSize");
QTest::addColumn<QPointF>("expectedOffset");
QTest::newRow("identity") <<
QRectF(0, 0, 100, 100) <<
QSizeF(100, 100) <<
QPointF(0, 0);
QTest::newRow("smaller object, internal") <<
QRectF(0, 0, 100, 100) <<
QSizeF(50, 50) <<
QPointF(-25, -25);
QTest::newRow("smaller object, partially out (x)") <<
QRectF(-80, 0, 100, 100) <<
QSizeF(50, 50) <<
QPointF(55, -25);
QTest::newRow("smaller object, totally out (y)") <<
QRectF(0, 1200, 100, 100) <<
QSizeF(10, 10) <<
QPointF(-45, -1245);
QTest::newRow("same size, partially out") <<
QRectF(10, -30, 100, 100) <<
QSizeF(100, 100) <<
QPointF(-10, 30);
}
void ScalerTest::testFitOffset()
{
QFETCH(QRectF, view);
QFETCH(QSizeF, objectSize);
QFETCH(QPointF, expectedOffset);
QPointF offset = Scaler::computeFitOffset(view, objectSize);
QCOMPARE(offset, expectedOffset);
}
QTEST_GUILESS_MAIN(ScalerTest)
#include "tst_scaler.moc"
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment