[education/rkward] rkward/rbackend/rkwarddevice: Start implementing masks
Thomas Friedrichsmeier
null at kde.org
Thu Mar 24 21:02:51 GMT 2022
Git commit 40413e4ac83963e0510620252b699871169ed339 by Thomas Friedrichsmeier.
Committed on 23/03/2022 at 20:27.
Pushed by tfry into branch 'master'.
Start implementing masks
M +77 -5 rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp
M +7 -0 rkward/rbackend/rkwarddevice/rkgraphicsdevice.h
M +3 -2 rkward/rbackend/rkwarddevice/rkgraphicsdevice_backendtransmitter.h
M +25 -0 rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp
M +2 -0 rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.h
M +62 -4 rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h
M +46 -3 rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp
https://invent.kde.org/education/rkward/commit/40413e4ac83963e0510620252b699871169ed339
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp b/rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp
index 9ed62900..6c828c76 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp
@@ -17,9 +17,6 @@
#include "rkgraphicsdevice.h"
-#include <QGraphicsScene>
-#include <QGraphicsView>
-#include <QGraphicsRectItem>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QDialog>
@@ -92,9 +89,9 @@ void RKGraphicsDevice::pushContext(double width, double height, double x, double
c.record_path = record_path;
c.path_below = recorded_path;
// NOTE: R cairo device uses an all different method for pattern capture:
-// drawing is scaled up to full device coordinates, the shrunk and offset back to pattern size.
+// drawing is scaled up to full device coordinates, then shrunk and offset back to pattern size.
// probably due to cairo internals, somehow. Here, instead we paint on a separate surface with the same coords,
-// the extract the rectangle of interest.
+// then extract the rectangle of interest.
c.surface = QImage(area.width(), area.height(), QImage::Format_ARGB32);
if (width < 0) { // may happen, at least in R 4.1.2
width = -width;
@@ -313,6 +310,51 @@ bool RKGraphicsDevice::setClipToCachedPath(int index){
return false;
}
+void RKGraphicsDevice::startRecordMask() {
+ RK_TRACE (GRAPHICS_DEVICE);
+ pushContext(area.width(), area.height(), 0, 0, false);
+}
+
+QImage RKGraphicsDevice::endRecordMask(bool luminance) {
+ RK_TRACE (GRAPHICS_DEVICE);
+
+ QImage ret = popContext().surface;
+ // KF6 TODO: instead paint on a grayscale surface from the start?
+ if (luminance) {
+ for (int x = ret.width(); x >= 0; --x) {
+ for (int y = ret.height(); y >= 0; --y) {
+ // warning! inefficient!
+ QColor px = ret.pixelColor(x, y);
+ px.setAlpha(px.lightness());
+ ret.setPixelColor(x, y, px);
+ }
+ }
+ }
+ return ret;
+}
+
+int RKGraphicsDevice::registerMask(const QImage& mask) {
+ RK_TRACE (GRAPHICS_DEVICE);
+ static int id = 0;
+ cached_masks.insert(++id, mask);
+ return id;
+}
+
+void RKGraphicsDevice::destroyMask(int index) {
+ RK_TRACE (GRAPHICS_DEVICE);
+ if (index < 0) cached_paths.clear();
+ else cached_paths.remove(index);
+}
+
+bool RKGraphicsDevice::setMask(int index) {
+ RK_TRACE (GRAPHICS_DEVICE);
+
+ int set = 0;
+ if (index > 0 && cached_masks.contains(index)) set = index;
+ current_mask = set;
+ return set == index;
+}
+
void RKGraphicsDevice::setClip (const QRectF& new_clip) {
RK_TRACE (GRAPHICS_DEVICE);
@@ -357,6 +399,10 @@ void RKGraphicsDevice::line (double x1, double y1, double x2, double y2, const Q
void RKGraphicsDevice::rect (const QRectF& rec, const QPen& pen, const QBrush& brush) {
RK_TRACE (GRAPHICS_DEVICE);
+ if (current_mask) {
+ pushContext(area.width(), area.height(), 0, 0, false);
+ }
+
if (recording_path) {
recorded_path.addRect(rec);
return;
@@ -364,6 +410,21 @@ void RKGraphicsDevice::rect (const QRectF& rec, const QPen& pen, const QBrush& b
painter.setPen (pen);
painter.setBrush (brush);
painter.drawRect (rec);
+
+ if (current_mask) {
+ QImage mask = cached_masks.value(current_mask);
+ QImage masked = popContext().surface;
+ for (int x = masked.width(); x >= 0; --x) {
+ for (int y = masked.height(); y >= 0; --y) {
+ // warning! inefficient!
+ QColor px = masked.pixelColor(x, y);
+ px.setAlpha(qMin(px.alpha(), mask.pixelColor(x, y).alpha()));
+ masked.setPixelColor(x, y, px);
+ }
+ }
+ painter.drawImage(0, 0, masked);
+ }
+
triggerUpdate ();
}
@@ -378,6 +439,10 @@ QSizeF RKGraphicsDevice::strSize(const QString& text, const QFont& font) {
void RKGraphicsDevice::text(double x, double y, const QString& text, double rot, double hadj, const QColor& col, const QFont& font) {
RK_TRACE(GRAPHICS_DEVICE);
+ if (current_mask) {
+ pushContext(area.width(), area.height(), 0, 0, false);
+ }
+
painter.save();
QSizeF size = strSize(text, font); // NOTE: side-effect of setting font!
if (recording_path) {
@@ -396,6 +461,13 @@ void RKGraphicsDevice::text(double x, double y, const QString& text, double rot,
painter.drawText(-(hadj * size.width()), 0, text);
// painter.drawRect(painter.fontMetrics().boundingRect(text)); // for debugging
painter.restore(); // undo rotation / translation
+
+ if (current_mask) {
+ painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
+ painter.drawImage(0, 0, cached_masks.value(current_mask));
+ QImage masked = popContext().surface;
+ painter.drawImage(0, 0, masked);
+ }
triggerUpdate();
}
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice.h b/rkward/rbackend/rkwarddevice/rkgraphicsdevice.h
index 40efcaf5..b60a88a7 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice.h
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice.h
@@ -105,6 +105,11 @@ public:
void destroyCachedPath(int index);
bool setClipToCachedPath(int index);
void forceSync();
+ void startRecordMask();
+ QImage endRecordMask(bool luminance);
+ int registerMask(const QImage &mask);
+ void destroyMask(int index);
+ bool setMask(int index);
public slots:
void stopInteraction ();
signals:
@@ -134,10 +139,12 @@ private:
QDialog *dialog;
QHash<int, QBrush> patterns;
QHash<int, QPainterPath> cached_paths;
+ QHash<int, QImage> cached_masks;
// NOTE on path recording: In principle, we could really do _all_ painting on QPainterPath, but in regular operation stroke and fill right away.
// However, that is noticably slower.
QPainterPath recorded_path;
bool recording_path;
+ int current_mask;
int interaction_opcode; /**< Current interactive operation (from RKDOpcodes enum), or -1 is there is no current interactive operation */
quint32 id;
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_backendtransmitter.h b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_backendtransmitter.h
index 4fd2998e..e9876464 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_backendtransmitter.h
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_backendtransmitter.h
@@ -22,9 +22,10 @@
#include <QThread>
#include <QMutex>
-#include "rkgraphicsdevice_protocol_shared.h"
#include "../rkasyncdatastreamhelper.h"
+typedef quint32 RKGraphicsDeviceTransmittionLengthType;
+
/** This simple class is responsible for handling the backend side of transmitting data / requests for the RKGraphicsDevice
Also it provides the namespace for some statics.
As the protocol is really quite simple (only the backend send requests, only one request at a time), so is the transmitter. */
@@ -35,7 +36,7 @@ public:
static void kill ();
static bool connectionAlive ();
static RKGraphicsDeviceBackendTransmitter* instance ();
- static RKAsyncDataStreamHelper<RKGraphicsDeviceTransmittionLengthType> streamer;
+ static RKAsyncDataStreamHelper<quint32> streamer;
static QIODevice* connection;
static QMutex mutex;
private:
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp
index 22cc0734..c9f23f22 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp
@@ -400,6 +400,31 @@ void RKGraphicsDeviceFrontendTransmitter::newData () {
device->setClipToCachedPath(index);
streamer.outstream << (qint32) index;
streamer.writeOutBuffer();
+ } else if (opcode == RKDSetMask) {
+ qint32 index;
+ streamer.instream >> index;
+ qint8 ok = device->setMask(index) ? 1 : 0;
+ streamer.outstream << ok;
+ streamer.writeOutBuffer();
+ } else if (opcode == RKDReleaseMask) {
+ qint32 len;
+ streamer.instream >> len;
+ if (len < 0) device->destroyMask(-1);
+ for (int i = 0; i < len; ++i) {
+ qint32 index;
+ streamer.instream >> index;
+ device->destroyMask(index);
+ }
+ } else if (opcode == RKDStartRecordMask) {
+ device->startRecordMask();
+ } else if (opcode == RKDEndRecordMask) {
+ qint8 luminance_mask;
+ streamer.instream >> luminance_mask;
+ QImage m = device->endRecordMask((bool) luminance_mask);
+ qint32 index = device->registerMask(m);
+ device->setMask(index);
+ streamer.outstream << (qint32) index;
+ streamer.writeOutBuffer();
} else if (opcode == RKDCapture) {
QImage image = device->capture ();
quint32 w = image.width ();
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.h b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.h
index a11853e6..fae6afff 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.h
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.h
@@ -18,7 +18,9 @@
#ifndef RKGRAPHICSDEVICE_FRONTENDTRANSMITTER_H
#define RKGRAPHICSDEVICE_FRONTENDTRANSMITTER_H
+#include <QPainter> // for enums
#include "rkgraphicsdevice_protocol_shared.h"
+#include "rkgraphicsdevice_backendtransmitter.h"
#include "../rkasyncdatastreamhelper.h"
class QIODevice;
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h
index 12daa58d..07d4e462 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h
@@ -107,7 +107,9 @@ enum RKDOpcodes {
RKDReleasePattern,
RKDStartRecordTilingPattern, // part of setPattern in R
RKDReleaseClipPath,
- RKDStartRecordClipPath,
+ RKDStartRecordClipPath,//20
+ RKDReleaseMask,
+ RKDStartRecordMask,
// Synchronous operations
RKDFetchNextEvent = 100,
@@ -122,8 +124,10 @@ enum RKDOpcodes {
RKDEndRecordTilingPattern, // part of setPattern in R
RKDSetClipPath, // 110
RKDEndRecordClipPath,
+ RKDSetMask,
+ RKDEndRecordMask,
RKDForceSync,
- RKDClose,
+ RKDClose, // 115
// Protocol operations
RKDCancel = 200
@@ -145,7 +149,61 @@ enum RKDEventCodes {
// RKDMouseX2Button = 16
};
-#include <QtGlobal>
-typedef quint32 RKGraphicsDeviceTransmittionLengthType;
+/** Common problem is that we need to map an R parameter enum onto the corresponding Qt parameter enum, e.g. line ending style, etc.
+ * While in most cases there _is_ a direct correspondence, the underlying int values cannot be assumed to be the same (and in many cases differ).
+ * Thus we need a lot of code along the lines "if(value = r_enum_value) return qt_enum_value;".
+ *
+ * As a further complication, we cannot easily include R headers in frontend code, and not easily (arbitray) Qt headers in backend code, thus the
+ * above pseudo code will lack either the Qt or the R defintion. At some point we need to map to a value we set ourselves, either a third, rk-specific, enum,
+ * or a naked int.
+ *
+ * To make this readable and easy to handle, the MapEnum() macro helps with the mapping. Since only _either_ the left hand side (R) or the right hand side (Qt)
+ * is actually expanded, it allows us to write a readable mapping that can be included in both frontend and backend code.
+ *
+ * We still need to provide an interim value (naked int). By convention this uses the same value as the Qt enum, and this is checked by a static_assert, helping
+ * to catch conceivable mistakes at compile time.
+ */
+#if defined(RKD_BACKEND_CODE)
+#define MapEnum(Rval,Ival,Qval) case Rval: return Ival;
+#define MapDefault(Ival,Qval) return Ival;
+#else
+#define MapEnum(Rval,Ival,Qval) case Ival: static_assert(Ival == (int) Qval, "Enum mismatch"); return Qval;
+#define MapDefault(Ival,Qval) return Qval;
+#define RKD_RGE_VERSION 99999
+#endif
+
+#if RKD_RGE_VERSION >= 15
+static int mapCompostionModeEnum(int from) {
+ switch(from) {
+ MapEnum(R_GE_compositeClear, 2, QPainter::CompositionMode_Clear);
+ MapEnum(R_GE_compositeSource, 3, QPainter::CompositionMode_Source);
+ MapEnum(R_GE_compositeOver, 0, QPainter::CompositionMode_SourceOver);
+ MapEnum(R_GE_compositeIn, 5, QPainter::CompositionMode_SourceIn);
+ MapEnum(R_GE_compositeOut, 7, QPainter::CompositionMode_SourceOut);
+ MapEnum(R_GE_compositeAtop, 9, QPainter::CompositionMode_SourceAtop);
+ MapEnum(R_GE_compositeDest, 4, QPainter::CompositionMode_Destination);
+ MapEnum(R_GE_compositeDestOver, 1, QPainter::CompositionMode_DestinationOver);
+ MapEnum(R_GE_compositeDestIn, 6, QPainter::CompositionMode_DestinationIn);
+ MapEnum(R_GE_compositeDestOut, 8, QPainter::CompositionMode_DestinationOut);
+ MapEnum(R_GE_compositeDestAtop, 10, QPainter::CompositionMode_DestinationAtop);
+ MapEnum(R_GE_compositeXor, 11, QPainter::CompositionMode_Xor);
+ MapEnum(R_GE_compositeAdd, 12, QPainter::CompositionMode_Plus);
+ MapEnum(R_GE_compositeMultiply, 13, QPainter::CompositionMode_Multiply);
+ MapEnum(R_GE_compositeScreen, 14, QPainter::CompositionMode_Screen);
+ MapEnum(R_GE_compositeOverlay, 15, QPainter::CompositionMode_Overlay);
+ MapEnum(R_GE_compositeDarken, 16, QPainter::CompositionMode_Darken);
+ MapEnum(R_GE_compositeLighten, 17, QPainter::CompositionMode_Lighten);
+ MapEnum(R_GE_compositeColorDodge, 18, QPainter::CompositionMode_ColorDodge);
+ MapEnum(R_GE_compositeColorBurn, 19, QPainter::CompositionMode_ColorBurn);
+ MapEnum(R_GE_compositeHardLight, 20, QPainter::CompositionMode_HardLight);
+ MapEnum(R_GE_compositeSoftLight, 21, QPainter::CompositionMode_SoftLight);
+ MapEnum(R_GE_compositeDifference, 22, QPainter::CompositionMode_Difference);
+ MapEnum(R_GE_compositeExclusion, 23, QPainter::CompositionMode_Exclusion);
+// Unsupported in Qt:
+// MapEnum(R_GE_compositeSaturate, xx, yy)
+ }
+ MapDefault(0, QPainter::CompositionMode_SourceOver);
+}
+#endif
#endif
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp
index e5864351..5c1fe048 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp
@@ -19,6 +19,8 @@
* not a compilation unit of its own.
* It is meant to be included, there. */
+#define RKD_BACKEND_CODE
+#define RKD_RGE_VERSION R_GE_version
#include "rkgraphicsdevice_protocol_shared.h"
#include "rkgraphicsdevice_backendtransmitter.h"
#include "../rkrbackend.h"
@@ -166,6 +168,7 @@ public:
#define SAFE_LINE_JOIN(ljoin) (quint8) (ljoin == GE_ROUND_JOIN ? RoundJoin : (ljoin == GE_BEVEL_JOIN ? BevelJoin : MitreJoin))
// I'd love to convert to QColor, directly, but that's in QtGui, not QtCore. QRgb used different byte ordering
+// TODO: is the check against NA_INTEGER needed at all?
#define WRITE_COLOR_BYTES(col) \
RKD_OUT_STREAM << (quint8) R_RED (col) << (quint8) R_GREEN (col) << (quint8) R_BLUE (col) << (quint8) R_ALPHA (col)
#define WRITE_HEADER_NUM(x,devnum) \
@@ -826,11 +829,51 @@ void RKD_ReleaseClipPath (SEXP ref, pDevDesc dev) {
}
}
-SEXP RKD_SetMask (SEXP path, SEXP ref, pDevDesc dd) {
+SEXP RKD_SetMask (SEXP mask, SEXP ref, pDevDesc dev) {
RK_TRACE(GRAPHICS_DEVICE);
-#ifdef __GNUC__
-#warning implement me
+ // Same logic as RKD_SetClipPath
+
+ qint32 index = 0;
+ if (!Rf_isNull(ref)) index = INTEGER(ref)[0]; // ref==NULL means the mask is not yet registered, will be recorded, below
+ if (index > 0 || Rf_isNull(mask)) { // mask==NULL means to unset the current mask. signalled to the frontend as index=0
+ {
+ RKGraphicsDataStreamWriteGuard wguard;
+ WRITE_HEADER(RKDSetMask, dev);
+ RKD_OUT_STREAM << index;
+ }
+ {
+ RKGraphicsDataStreamReadGuard rguard;
+ qint8 ok;
+ RKD_IN_STREAM >> ok;
+ if (!ok) Rf_warning("Invalid reference to mask");
+ else return R_NilValue;
+ }
+ }
+
+ // No index, or not a valid index: create new mask
+ {
+ RKGraphicsDataStreamWriteGuard wguard;
+ WRITE_HEADER(RKDStartRecordMask, dev);
+ }
+ // Play generator function
+ int error;
+ SEXP mask_func = PROTECT(Rf_lang1(mask));
+ R_tryEval(mask_func, R_GlobalEnv, &error);
+ UNPROTECT(1);
+ {
+ RKGraphicsDataStreamWriteGuard wguard;
+ WRITE_HEADER(RKDEndRecordMask, dev);
+#if R_VERSION >= R_Version(4,2,0)
+ RKD_OUT_STREAM << (qint8) R_GE_maskType(mask) == R_GE_luminanceMask ? 1 : 0;
+#else
+ RKD_OUT_STREAM << 0;
#endif
+ }
+ {
+ RKGraphicsDataStreamReadGuard rguard;
+ RKD_IN_STREAM >> index;
+ }
+ return makeInt(index);
return R_NilValue;
}
More information about the rkward-tracker
mailing list